diff options
Diffstat (limited to '')
260 files changed, 8790 insertions, 1249 deletions
diff --git a/src/.clang-format b/src/.clang-format index 1c6b71b2e..f92771ec3 100644 --- a/src/.clang-format +++ b/src/.clang-format @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> +# SPDX-License-Identifier: GPL-2.0-or-later + --- Language: Cpp # BasedOnStyle: LLVM diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 39ae573b2..fc177fa52 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + # Enable modules to include each other's files include_directories(.) @@ -156,6 +159,7 @@ add_subdirectory(common) add_subdirectory(core) add_subdirectory(audio_core) add_subdirectory(video_core) +add_subdirectory(network) add_subdirectory(input_common) add_subdirectory(shader_recompiler) diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 2971c42a2..5fe1d5fa5 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_library(audio_core STATIC audio_core.cpp audio_core.h diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index d574e4b79..a6dc31b53 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + if (DEFINED ENV{AZURECIREPO}) set(BUILD_REPOSITORY $ENV{AZURECIREPO}) endif() @@ -41,6 +44,7 @@ add_custom_command(OUTPUT scm_rev.cpp add_library(common STATIC algorithm.h alignment.h + announce_multiplayer_room.h assert.cpp assert.h atomic_helpers.h diff --git a/src/common/alignment.h b/src/common/alignment.h index 8570c7d3c..7e897334b 100644 --- a/src/common/alignment.h +++ b/src/common/alignment.h @@ -1,4 +1,5 @@ -// This file is under the public domain. +// SPDX-FileCopyrightText: 2014 Jannik Vogel <email@jannikvogel.de> +// SPDX-License-Identifier: CC0-1.0 #pragma once diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h new file mode 100644 index 000000000..0ad9da2be --- /dev/null +++ b/src/common/announce_multiplayer_room.h @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <functional> +#include <string> +#include <vector> +#include "common/common_types.h" +#include "web_service/web_result.h" + +namespace AnnounceMultiplayerRoom { + +using MacAddress = std::array<u8, 6>; + +struct GameInfo { + std::string name{""}; + u64 id{0}; +}; + +struct Member { + std::string username; + std::string nickname; + std::string display_name; + std::string avatar_url; + MacAddress mac_address; + GameInfo game; +}; + +struct RoomInformation { + std::string name; ///< Name of the server + std::string description; ///< Server description + u32 member_slots; ///< Maximum number of members in this room + 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 { + RoomInformation information; + + std::string id; + std::string verify_uid; ///< UID used for verification + std::string ip; + u32 net_version; + bool has_password; + + std::vector<Member> members; +}; +using RoomList = std::vector<Room>; + +/** + * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should + * implement this interface. + */ +class Backend { +public: + virtual ~Backend() = default; + + /** + * Sets the Information that gets used for the announce + * @param uid The Id of the room + * @param name The name of the room + * @param description The room description + * @param port The port of the room + * @param net_version The version of the libNetwork that gets used + * @param has_password True if the room is passowrd protected + * @param preferred_game The preferred game of the room + * @param preferred_game_id The title id of the preferred game + */ + virtual void SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, const GameInfo& preferred_game) = 0; + /** + * Adds a player information to the data that gets announced + * @param nickname The nickname of the player + * @param mac_address The MAC Address of the player + * @param game_id The title id of the game the player plays + * @param game_name The name of the game the player plays + */ + virtual void AddPlayer(const Member& member) = 0; + + /** + * Updates the data in the announce service. Re-register the room when required. + * @result The result of the update attempt + */ + virtual WebService::WebResult Update() = 0; + + /** + * Registers the data in the announce service + * @result The result of the register attempt. When the result code is Success, A global Guid of + * the room which may be used for verification will be in the result's returned_data. + */ + virtual WebService::WebResult Register() = 0; + + /** + * Empties the stored players + */ + virtual void ClearPlayers() = 0; + + /** + * Get the room information from the announce service + * @result A list of all rooms the announce service has + */ + virtual RoomList GetRoomList() = 0; + + /** + * Sends a delete message to the announce service + */ + virtual void Delete() = 0; +}; + +/** + * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a + * functional backend implementation is not available. + */ +class NullBackend : public Backend { +public: + ~NullBackend() = default; + void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/, + const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/, + const bool /*has_password*/, + const GameInfo& /*preferred_game*/) override {} + void AddPlayer(const Member& /*member*/) override {} + WebService::WebResult Update() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing", ""}; + } + WebService::WebResult Register() override { + return WebService::WebResult{WebService::WebResult::Code::NoWebservice, + "WebService is missing", ""}; + } + void ClearPlayers() override {} + RoomList GetRoomList() override { + return RoomList{}; + } + + void Delete() override {} +}; + +} // namespace AnnounceMultiplayerRoom diff --git a/src/common/atomic_helpers.h b/src/common/atomic_helpers.h index 6d912b52e..bef5015c1 100644 --- a/src/common/atomic_helpers.h +++ b/src/common/atomic_helpers.h @@ -1,4 +1,7 @@ -// ©2013-2016 Cameron Desrochers. +// SPDX-FileCopyrightText: 2013-2016 Cameron Desrochers +// SPDX-FileCopyrightText: 2015 Jeff Preshing +// SPDX-License-Identifier: BSD-2-Clause AND Zlib + // Distributed under the simplified BSD license (see the license file that // should have come with this header). // Uses Jeff Preshing's semaphore implementation (under the terms of its diff --git a/src/common/detached_tasks.cpp b/src/common/detached_tasks.cpp index ec31d0b88..da64848da 100644 --- a/src/common/detached_tasks.cpp +++ b/src/common/detached_tasks.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <thread> #include "common/assert.h" diff --git a/src/common/detached_tasks.h b/src/common/detached_tasks.h index 5dd8fc27b..416a2d7f3 100644 --- a/src/common/detached_tasks.h +++ b/src/common/detached_tasks.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/error.cpp b/src/common/error.cpp index d4455e310..ddb03bd45 100644 --- a/src/common/error.cpp +++ b/src/common/error.cpp @@ -1,6 +1,6 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <cstddef> #ifdef _WIN32 diff --git a/src/common/error.h b/src/common/error.h index e084d4b0f..62a3bd835 100644 --- a/src/common/error.h +++ b/src/common/error.h @@ -1,6 +1,6 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/fixed_point.h b/src/common/fixed_point.h index 1d45e51b3..4a0f72cc9 100644 --- a/src/common/fixed_point.h +++ b/src/common/fixed_point.h @@ -1,28 +1,8 @@ +// SPDX-FileCopyrightText: 2015 Evan Teran +// SPDX-License-Identifier: MIT + // From: https://github.com/eteran/cpp-utilities/blob/master/fixed/include/cpp-utilities/fixed.h // See also: http://stackoverflow.com/questions/79677/whats-the-best-way-to-do-fixed-point-math -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 Evan Teran - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ #ifndef FIXED_H_ #define FIXED_H_ diff --git a/src/common/hash.h b/src/common/hash.h index 298930702..b6f3e6d6f 100644 --- a/src/common/hash.h +++ b/src/common/hash.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/input.h b/src/common/input.h index 995c35d9d..213aa2384 100644 --- a/src/common/input.h +++ b/src/common/input.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index b3793106d..8ce1c2fd1 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <atomic> #include <chrono> diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index a0e80fe3c..12e5e2498 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/logging/filter.cpp b/src/common/logging/filter.cpp index 6de9bacbf..a959acb74 100644 --- a/src/common/logging/filter.cpp +++ b/src/common/logging/filter.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include "common/logging/filter.h" diff --git a/src/common/logging/filter.h b/src/common/logging/filter.h index 29419f051..54d172cc0 100644 --- a/src/common/logging/filter.h +++ b/src/common/logging/filter.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/logging/log.h b/src/common/logging/log.h index 0c80d01ee..c00c01a9e 100644 --- a/src/common/logging/log.h +++ b/src/common/logging/log.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/logging/text_formatter.cpp b/src/common/logging/text_formatter.cpp index b2cad58d8..09398ea64 100644 --- a/src/common/logging/text_formatter.cpp +++ b/src/common/logging/text_formatter.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <cstdio> diff --git a/src/common/logging/text_formatter.h b/src/common/logging/text_formatter.h index 92c0bf0c5..0d0ec4370 100644 --- a/src/common/logging/text_formatter.h +++ b/src/common/logging/text_formatter.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/microprofile.cpp b/src/common/microprofile.cpp index ee25dd37f..e6657c82f 100644 --- a/src/common/microprofile.cpp +++ b/src/common/microprofile.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later // Includes the MicroProfile implementation in this file for compilation #define MICROPROFILE_IMPL 1 diff --git a/src/common/microprofile.h b/src/common/microprofile.h index 54e7f3cc4..91d14d5e1 100644 --- a/src/common/microprofile.h +++ b/src/common/microprofile.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/microprofileui.h b/src/common/microprofileui.h index 41abe6b75..39ed18ffa 100644 --- a/src/common/microprofileui.h +++ b/src/common/microprofileui.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/param_package.cpp b/src/common/param_package.cpp index 462502e34..629babb81 100644 --- a/src/common/param_package.cpp +++ b/src/common/param_package.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <stdexcept> diff --git a/src/common/param_package.h b/src/common/param_package.h index c13e45479..d7c13cb1f 100644 --- a/src/common/param_package.h +++ b/src/common/param_package.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/quaternion.h b/src/common/quaternion.h index 4d0871eb4..5bb5f2af0 100644 --- a/src/common/quaternion.h +++ b/src/common/quaternion.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/reader_writer_queue.h b/src/common/reader_writer_queue.h index 8d2c9408c..60c41a8cb 100644 --- a/src/common/reader_writer_queue.h +++ b/src/common/reader_writer_queue.h @@ -1,6 +1,5 @@ -// ©2013-2020 Cameron Desrochers. -// Distributed under the simplified BSD license (see the license file that -// should have come with this header). +// SPDX-FileCopyrightText: 2013-2020 Cameron Desrochers +// SPDX-License-Identifier: BSD-2-Clause #pragma once diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index cc88994c6..f0c124d69 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/scm_rev.h" diff --git a/src/common/scm_rev.h b/src/common/scm_rev.h index 563015ec9..88404316a 100644 --- a/src/common/scm_rev.h +++ b/src/common/scm_rev.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/scope_exit.h b/src/common/scope_exit.h index 35dac3a8f..e9c789c88 100644 --- a/src/common/scope_exit.h +++ b/src/common/scope_exit.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/telemetry.cpp b/src/common/telemetry.cpp index 67261c55b..d26394359 100644 --- a/src/common/telemetry.cpp +++ b/src/common/telemetry.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <cstring> diff --git a/src/common/telemetry.h b/src/common/telemetry.h index f9a824a7d..ba633d5a5 100644 --- a/src/common/telemetry.h +++ b/src/common/telemetry.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/x64/xbyak_abi.h b/src/common/x64/xbyak_abi.h index 87b3d63a4..67e6e63c8 100644 --- a/src/common/x64/xbyak_abi.h +++ b/src/common/x64/xbyak_abi.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/common/x64/xbyak_util.h b/src/common/x64/xbyak_util.h index 44d2558f1..250e5cddb 100644 --- a/src/common/x64/xbyak_util.h +++ b/src/common/x64/xbyak_util.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index db7bc7a8b..40b1ea4a2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,4 +1,9 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_library(core STATIC + announce_multiplayer_session.cpp + announce_multiplayer_session.h arm/arm_interface.h arm/arm_interface.cpp arm/cpu_interrupt_handler.cpp @@ -715,6 +720,11 @@ add_library(core STATIC hle/service/vi/vi_u.h hle/service/wlan/wlan.cpp hle/service/wlan/wlan.h + internal_network/network.cpp + internal_network/network.h + internal_network/network_interface.cpp + internal_network/network_interface.h + internal_network/sockets.h loader/deconstructed_rom_directory.cpp loader/deconstructed_rom_directory.h loader/elf.cpp @@ -742,11 +752,6 @@ add_library(core STATIC memory/dmnt_cheat_vm.h memory.cpp memory.h - network/network.cpp - network/network.h - network/network_interface.cpp - network/network_interface.h - network/sockets.h perf_stats.cpp perf_stats.h reporter.cpp @@ -781,7 +786,7 @@ endif() create_target_directory_groups(core) -target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) +target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core) target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus) if (MINGW) target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) diff --git a/src/core/announce_multiplayer_session.cpp b/src/core/announce_multiplayer_session.cpp new file mode 100644 index 000000000..d73a488cf --- /dev/null +++ b/src/core/announce_multiplayer_session.cpp @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <chrono> +#include <future> +#include <vector> +#include "announce_multiplayer_session.h" +#include "common/announce_multiplayer_room.h" +#include "common/assert.h" +#include "common/settings.h" +#include "network/network.h" + +#ifdef ENABLE_WEB_SERVICE +#include "web_service/announce_room_json.h" +#endif + +namespace Core { + +// Time between room is announced to web_service +static constexpr std::chrono::seconds announce_time_interval(15); + +AnnounceMultiplayerSession::AnnounceMultiplayerSession(Network::RoomNetwork& room_network_) + : room_network{room_network_} { +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#else + backend = std::make_unique<AnnounceMultiplayerRoom::NullBackend>(); +#endif +} + +WebService::WebResult AnnounceMultiplayerSession::Register() { + std::shared_ptr<Network::Room> room = room_network.GetRoom().lock(); + if (!room) { + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Network is not initialized", ""}; + } + if (room->GetState() != Network::Room::State::Open) { + return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open", ""}; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + LOG_INFO(WebService, "Room has been registered"); + room->SetVerifyUID(result.returned_data); + registered = true; + return WebService::WebResult{WebService::WebResult::Code::Success, "", ""}; +} + +void AnnounceMultiplayerSession::Start() { + if (announce_multiplayer_thread) { + Stop(); + } + shutdown_event.Reset(); + announce_multiplayer_thread = + std::make_unique<std::thread>(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this); +} + +void AnnounceMultiplayerSession::Stop() { + if (announce_multiplayer_thread) { + shutdown_event.Set(); + announce_multiplayer_thread->join(); + announce_multiplayer_thread.reset(); + backend->Delete(); + registered = false; + } +} + +AnnounceMultiplayerSession::CallbackHandle AnnounceMultiplayerSession::BindErrorCallback( + std::function<void(const WebService::WebResult&)> function) { + std::lock_guard lock(callback_mutex); + auto handle = std::make_shared<std::function<void(const WebService::WebResult&)>>(function); + error_callbacks.insert(handle); + return handle; +} + +void AnnounceMultiplayerSession::UnbindErrorCallback(CallbackHandle handle) { + std::lock_guard lock(callback_mutex); + error_callbacks.erase(handle); +} + +AnnounceMultiplayerSession::~AnnounceMultiplayerSession() { + Stop(); +} + +void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr<Network::Room> room) { + Network::RoomInformation room_information = room->GetRoomInformation(); + std::vector<AnnounceMultiplayerRoom::Member> memberlist = room->GetRoomMemberList(); + backend->SetRoomInformation(room_information.name, room_information.description, + room_information.port, room_information.member_slots, + Network::network_version, room->HasPassword(), + room_information.preferred_game); + backend->ClearPlayers(); + for (const auto& member : memberlist) { + backend->AddPlayer(member); + } +} + +void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() { + // Invokes all current bound error callbacks. + const auto ErrorCallback = [this](WebService::WebResult result) { + std::lock_guard<std::mutex> lock(callback_mutex); + for (auto callback : error_callbacks) { + (*callback)(result); + } + }; + + if (!registered) { + WebService::WebResult result = Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + return; + } + } + + auto update_time = std::chrono::steady_clock::now(); + std::future<WebService::WebResult> future; + while (!shutdown_event.WaitUntil(update_time)) { + update_time += announce_time_interval; + std::shared_ptr<Network::Room> room = room_network.GetRoom().lock(); + if (!room) { + break; + } + if (room->GetState() != Network::Room::State::Open) { + break; + } + UpdateBackendData(room); + WebService::WebResult result = backend->Update(); + if (result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(result); + } + if (result.result_string == "404") { + registered = false; + // Needs to register the room again + WebService::WebResult register_result = Register(); + if (register_result.result_code != WebService::WebResult::Code::Success) { + ErrorCallback(register_result); + } + } + } +} + +AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() { + return backend->GetRoomList(); +} + +bool AnnounceMultiplayerSession::IsRunning() const { + return announce_multiplayer_thread != nullptr; +} + +void AnnounceMultiplayerSession::UpdateCredentials() { + ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running"); + +#ifdef ENABLE_WEB_SERVICE + backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); +#endif +} + +} // namespace Core diff --git a/src/core/announce_multiplayer_session.h b/src/core/announce_multiplayer_session.h new file mode 100644 index 000000000..db790f7d2 --- /dev/null +++ b/src/core/announce_multiplayer_session.h @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <atomic> +#include <functional> +#include <memory> +#include <mutex> +#include <set> +#include <thread> +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "common/thread.h" + +namespace Network { +class Room; +class RoomNetwork; +} // namespace Network + +namespace Core { + +/** + * Instruments AnnounceMultiplayerRoom::Backend. + * Creates a thread that regularly updates the room information and submits them + * An async get of room information is also possible + */ +class AnnounceMultiplayerSession { +public: + using CallbackHandle = std::shared_ptr<std::function<void(const WebService::WebResult&)>>; + AnnounceMultiplayerSession(Network::RoomNetwork& room_network_); + ~AnnounceMultiplayerSession(); + + /** + * Allows to bind a function that will get called if the announce encounters an error + * @param function The function that gets called + * @return A handle that can be used the unbind the function + */ + CallbackHandle BindErrorCallback(std::function<void(const WebService::WebResult&)> function); + + /** + * Unbind a function from the error callbacks + * @param handle The handle for the function that should get unbind + */ + void UnbindErrorCallback(CallbackHandle handle); + + /** + * Registers a room to web services + * @return The result of the registration attempt. + */ + WebService::WebResult Register(); + + /** + * Starts the announce of a room to web services + */ + void Start(); + + /** + * Stops the announce to web services + */ + void Stop(); + + /** + * Returns a list of all room information the backend got + * @param func A function that gets executed when the async get finished, e.g. a signal + * @return a list of rooms received from the web service + */ + AnnounceMultiplayerRoom::RoomList GetRoomList(); + + /** + * Whether the announce session is still running + */ + bool IsRunning() const; + + /** + * Recreates the backend, updating the credentials. + * This can only be used when the announce session is not running. + */ + void UpdateCredentials(); + +private: + void UpdateBackendData(std::shared_ptr<Network::Room> room); + void AnnounceMultiplayerLoop(); + + Common::Event shutdown_event; + std::mutex callback_mutex; + std::set<CallbackHandle> error_callbacks; + std::unique_ptr<std::thread> announce_multiplayer_thread; + + /// Backend interface that logs fields + std::unique_ptr<AnnounceMultiplayerRoom::Backend> backend; + + std::atomic_bool registered = false; ///< Whether the room has been registered + + Network::RoomNetwork& room_network; +}; + +} // namespace Core diff --git a/src/core/arm/arm_interface.cpp b/src/core/arm/arm_interface.cpp index e72b250be..953d96439 100644 --- a/src/core/arm/arm_interface.cpp +++ b/src/core/arm/arm_interface.cpp @@ -154,9 +154,10 @@ void ARM_Interface::Run() { break; } - // Handle syscalls and scheduling (this may change the current thread) + // Handle syscalls and scheduling (this may change the current thread/core) if (Has(hr, svc_call)) { Kernel::Svc::Call(system, GetSvcNumber()); + break; } if (Has(hr, break_loop) || !uses_wall_clock) { break; diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index c092db9ff..73f259525 100644 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/arm/dynarmic/arm_dynarmic_cp15.cpp b/src/core/arm/dynarmic/arm_dynarmic_cp15.cpp index 6aae79c48..e9123c13d 100644 --- a/src/core/arm/dynarmic/arm_dynarmic_cp15.cpp +++ b/src/core/arm/dynarmic/arm_dynarmic_cp15.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <fmt/format.h> #include "common/logging/log.h" diff --git a/src/core/arm/dynarmic/arm_dynarmic_cp15.h b/src/core/arm/dynarmic/arm_dynarmic_cp15.h index f271b2070..5b2a51636 100644 --- a/src/core/arm/dynarmic/arm_dynarmic_cp15.h +++ b/src/core/arm/dynarmic/arm_dynarmic_cp15.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/core.cpp b/src/core/core.cpp index 0ede0d85c..ea32a4a8d 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <atomic> @@ -43,14 +42,15 @@ #include "core/hle/service/service.h" #include "core/hle/service/sm/sm.h" #include "core/hle/service/time/time_manager.h" +#include "core/internal_network/network.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/memory/cheat_engine.h" -#include "core/network/network.h" #include "core/perf_stats.h" #include "core/reporter.h" #include "core/telemetry_session.h" #include "core/tools/freezer.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "video_core/video_core.h" @@ -130,7 +130,7 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, struct System::Impl { explicit Impl(System& system) - : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, + : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, room_network{}, cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system} {} SystemResultStatus Run() { @@ -315,6 +315,17 @@ struct System::Impl { GetAndResetPerfStats(); perf_stats->BeginSystemFrame(); + std::string name = "Unknown Game"; + if (app_loader->ReadTitle(name) != Loader::ResultStatus::Success) { + LOG_ERROR(Core, "Failed to read title for ROM (Error {})", load_result); + } + if (auto room_member = room_network.GetRoomMember().lock()) { + Network::GameInfo game_info; + game_info.name = name; + game_info.id = program_id; + room_member->SendGameInfo(game_info); + } + status = SystemResultStatus::Success; return status; } @@ -362,6 +373,11 @@ struct System::Impl { memory.Reset(); applet_manager.ClearAll(); + if (auto room_member = room_network.GetRoomMember().lock()) { + Network::GameInfo game_info{}; + room_member->SendGameInfo(game_info); + } + LOG_DEBUG(Core, "Shutdown OK"); } @@ -434,6 +450,8 @@ struct System::Impl { std::unique_ptr<AudioCore::AudioCore> audio_core; Core::Memory::Memory memory; Core::HID::HIDCore hid_core; + Network::RoomNetwork room_network; + CpuManager cpu_manager; std::atomic_bool is_powered_on{}; bool exit_lock = false; @@ -879,6 +897,14 @@ const Core::Debugger& System::GetDebugger() const { return *impl->debugger; } +Network::RoomNetwork& System::GetRoomNetwork() { + return impl->room_network; +} + +const Network::RoomNetwork& System::GetRoomNetwork() const { + return impl->room_network; +} + void System::RegisterExecuteProgramCallback(ExecuteProgramCallback&& callback) { impl->execute_program_callback = std::move(callback); } diff --git a/src/core/core.h b/src/core/core.h index a49d1214b..0ce3b1d60 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -97,6 +96,10 @@ namespace Core::HID { class HIDCore; } +namespace Network { +class RoomNetwork; +} + namespace Core { class ARM_Interface; @@ -379,6 +382,12 @@ public: [[nodiscard]] Core::Debugger& GetDebugger(); [[nodiscard]] const Core::Debugger& GetDebugger() const; + /// Gets a mutable reference to the Room Network. + [[nodiscard]] Network::RoomNetwork& GetRoomNetwork(); + + /// Gets an immutable reference to the Room Network. + [[nodiscard]] const Network::RoomNetwork& GetRoomNetwork() const; + void SetExitLock(bool locked); [[nodiscard]] bool GetExitLock() const; diff --git a/src/core/cpu_manager.cpp b/src/core/cpu_manager.cpp index 37d3d83b9..9b1565ae1 100644 --- a/src/core/cpu_manager.cpp +++ b/src/core/cpu_manager.cpp @@ -8,6 +8,7 @@ #include "core/core.h" #include "core/core_timing.h" #include "core/cpu_manager.h" +#include "core/hle/kernel/k_interrupt_manager.h" #include "core/hle/kernel/k_scheduler.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" @@ -49,14 +50,6 @@ void CpuManager::GuestThreadFunction() { } } -void CpuManager::GuestRewindFunction() { - if (is_multicore) { - MultiCoreRunGuestLoop(); - } else { - SingleCoreRunGuestLoop(); - } -} - void CpuManager::IdleThreadFunction() { if (is_multicore) { MultiCoreRunIdleThread(); @@ -69,21 +62,21 @@ void CpuManager::ShutdownThreadFunction() { ShutdownThread(); } +void CpuManager::HandleInterrupt() { + auto& kernel = system.Kernel(); + auto core_index = kernel.CurrentPhysicalCoreIndex(); + + Kernel::KInterruptManager::HandleInterrupt(kernel, static_cast<s32>(core_index)); +} + /////////////////////////////////////////////////////////////////////////////// /// MultiCore /// /////////////////////////////////////////////////////////////////////////////// void CpuManager::MultiCoreRunGuestThread() { + // Similar to UserModeThreadStarter in HOS auto& kernel = system.Kernel(); kernel.CurrentScheduler()->OnThreadStart(); - auto* thread = kernel.CurrentScheduler()->GetSchedulerCurrentThread(); - auto& host_context = thread->GetHostContext(); - host_context->SetRewindPoint([this] { GuestRewindFunction(); }); - MultiCoreRunGuestLoop(); -} - -void CpuManager::MultiCoreRunGuestLoop() { - auto& kernel = system.Kernel(); while (true) { auto* physical_core = &kernel.CurrentPhysicalCore(); @@ -91,18 +84,26 @@ void CpuManager::MultiCoreRunGuestLoop() { physical_core->Run(); physical_core = &kernel.CurrentPhysicalCore(); } - { - Kernel::KScopedDisableDispatch dd(kernel); - physical_core->ArmInterface().ClearExclusiveState(); - } + + HandleInterrupt(); } } void CpuManager::MultiCoreRunIdleThread() { + // Not accurate to HOS. Remove this entire method when singlecore is removed. + // See notes in KScheduler::ScheduleImpl for more information about why this + // is inaccurate. + auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); + while (true) { - Kernel::KScopedDisableDispatch dd(kernel); - kernel.CurrentPhysicalCore().Idle(); + auto& physical_core = kernel.CurrentPhysicalCore(); + if (!physical_core.IsInterrupted()) { + physical_core.Idle(); + } + + HandleInterrupt(); } } @@ -113,80 +114,73 @@ void CpuManager::MultiCoreRunIdleThread() { void CpuManager::SingleCoreRunGuestThread() { auto& kernel = system.Kernel(); kernel.CurrentScheduler()->OnThreadStart(); - auto* thread = kernel.CurrentScheduler()->GetSchedulerCurrentThread(); - auto& host_context = thread->GetHostContext(); - host_context->SetRewindPoint([this] { GuestRewindFunction(); }); - SingleCoreRunGuestLoop(); -} -void CpuManager::SingleCoreRunGuestLoop() { - auto& kernel = system.Kernel(); while (true) { auto* physical_core = &kernel.CurrentPhysicalCore(); if (!physical_core->IsInterrupted()) { physical_core->Run(); physical_core = &kernel.CurrentPhysicalCore(); } + kernel.SetIsPhantomModeForSingleCore(true); system.CoreTiming().Advance(); kernel.SetIsPhantomModeForSingleCore(false); - physical_core->ArmInterface().ClearExclusiveState(); + PreemptSingleCore(); - auto& scheduler = kernel.Scheduler(current_core); - scheduler.RescheduleCurrentCore(); + HandleInterrupt(); } } void CpuManager::SingleCoreRunIdleThread() { auto& kernel = system.Kernel(); + kernel.CurrentScheduler()->OnThreadStart(); + while (true) { - auto& physical_core = kernel.CurrentPhysicalCore(); PreemptSingleCore(false); system.CoreTiming().AddTicks(1000U); idle_count++; - auto& scheduler = physical_core.Scheduler(); - scheduler.RescheduleCurrentCore(); + HandleInterrupt(); } } -void CpuManager::PreemptSingleCore(bool from_running_enviroment) { - { - auto& kernel = system.Kernel(); - auto& scheduler = kernel.Scheduler(current_core); - Kernel::KThread* current_thread = scheduler.GetSchedulerCurrentThread(); - if (idle_count >= 4 || from_running_enviroment) { - if (!from_running_enviroment) { - system.CoreTiming().Idle(); - idle_count = 0; - } - kernel.SetIsPhantomModeForSingleCore(true); - system.CoreTiming().Advance(); - kernel.SetIsPhantomModeForSingleCore(false); - } - current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); - system.CoreTiming().ResetTicks(); - scheduler.Unload(scheduler.GetSchedulerCurrentThread()); - - auto& next_scheduler = kernel.Scheduler(current_core); - Common::Fiber::YieldTo(current_thread->GetHostContext(), *next_scheduler.ControlContext()); - } +void CpuManager::PreemptSingleCore(bool from_running_environment) { + auto& kernel = system.Kernel(); - // May have changed scheduler - { - auto& scheduler = system.Kernel().Scheduler(current_core); - scheduler.Reload(scheduler.GetSchedulerCurrentThread()); - if (!scheduler.IsIdle()) { + if (idle_count >= 4 || from_running_environment) { + if (!from_running_environment) { + system.CoreTiming().Idle(); idle_count = 0; } + kernel.SetIsPhantomModeForSingleCore(true); + system.CoreTiming().Advance(); + kernel.SetIsPhantomModeForSingleCore(false); + } + current_core.store((current_core + 1) % Core::Hardware::NUM_CPU_CORES); + system.CoreTiming().ResetTicks(); + kernel.Scheduler(current_core).PreemptSingleCore(); + + // We've now been scheduled again, and we may have exchanged schedulers. + // Reload the scheduler in case it's different. + if (!kernel.Scheduler(current_core).IsIdle()) { + idle_count = 0; } } +void CpuManager::GuestActivate() { + // Similar to the HorizonKernelMain callback in HOS + auto& kernel = system.Kernel(); + auto* scheduler = kernel.CurrentScheduler(); + + scheduler->Activate(); + UNREACHABLE(); +} + void CpuManager::ShutdownThread() { auto& kernel = system.Kernel(); + auto* thread = kernel.GetCurrentEmuThread(); auto core = is_multicore ? kernel.CurrentPhysicalCoreIndex() : 0; - auto* current_thread = kernel.GetCurrentEmuThread(); - Common::Fiber::YieldTo(current_thread->GetHostContext(), *core_data[core].host_context); + Common::Fiber::YieldTo(thread->GetHostContext(), *core_data[core].host_context); UNREACHABLE(); } @@ -218,9 +212,12 @@ void CpuManager::RunThread(std::size_t core) { system.GPU().ObtainContext(); } - auto* current_thread = system.Kernel().CurrentScheduler()->GetIdleThread(); - Kernel::SetCurrentThread(system.Kernel(), current_thread); - Common::Fiber::YieldTo(data.host_context, *current_thread->GetHostContext()); + auto& kernel = system.Kernel(); + auto& scheduler = *kernel.CurrentScheduler(); + auto* thread = scheduler.GetSchedulerCurrentThread(); + Kernel::SetCurrentThread(kernel, thread); + + Common::Fiber::YieldTo(data.host_context, *thread->GetHostContext()); } } // namespace Core diff --git a/src/core/cpu_manager.h b/src/core/cpu_manager.h index 76dc58ee1..95ea3ef39 100644 --- a/src/core/cpu_manager.h +++ b/src/core/cpu_manager.h @@ -50,7 +50,10 @@ public: void Initialize(); void Shutdown(); - std::function<void()> GetGuestThreadStartFunc() { + std::function<void()> GetGuestActivateFunc() { + return [this] { GuestActivate(); }; + } + std::function<void()> GetGuestThreadFunc() { return [this] { GuestThreadFunction(); }; } std::function<void()> GetIdleThreadStartFunc() { @@ -68,20 +71,19 @@ public: private: void GuestThreadFunction(); - void GuestRewindFunction(); void IdleThreadFunction(); void ShutdownThreadFunction(); void MultiCoreRunGuestThread(); - void MultiCoreRunGuestLoop(); void MultiCoreRunIdleThread(); void SingleCoreRunGuestThread(); - void SingleCoreRunGuestLoop(); void SingleCoreRunIdleThread(); static void ThreadStart(std::stop_token stop_token, CpuManager& cpu_manager, std::size_t core); + void GuestActivate(); + void HandleInterrupt(); void ShutdownThread(); void RunThread(std::size_t core); diff --git a/src/core/file_sys/errors.h b/src/core/file_sys/errors.h index ff15b3e23..7cee0c7df 100644 --- a/src/core/file_sys/errors.h +++ b/src/core/file_sys/errors.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/frontend/applets/software_keyboard.h b/src/core/frontend/applets/software_keyboard.h index a405e3c94..094d1e713 100644 --- a/src/core/frontend/applets/software_keyboard.h +++ b/src/core/frontend/applets/software_keyboard.h @@ -17,6 +17,8 @@ struct KeyboardInitializeParameters { std::u16string sub_text; std::u16string guide_text; std::u16string initial_text; + char16_t left_optional_symbol_key; + char16_t right_optional_symbol_key; u32 max_text_length; u32 min_text_length; s32 initial_cursor_position; diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index 57c6ffc43..1be2dccb0 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <mutex> #include "core/frontend/emu_window.h" diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index b3bffecb2..ac1906d5e 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/ipc.h b/src/core/hle/ipc.h index 602e12606..416da15ec 100644 --- a/src/core/hle/ipc.h +++ b/src/core/hle/ipc.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/ipc_helpers.h b/src/core/hle/ipc_helpers.h index 004bb2005..d631c0357 100644 --- a/src/core/hle/ipc_helpers.h +++ b/src/core/hle/ipc_helpers.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/kernel/global_scheduler_context.cpp b/src/core/hle/kernel/global_scheduler_context.cpp index 164436b26..65576b8c4 100644 --- a/src/core/hle/kernel/global_scheduler_context.cpp +++ b/src/core/hle/kernel/global_scheduler_context.cpp @@ -41,12 +41,7 @@ void GlobalSchedulerContext::PreemptThreads() { ASSERT(IsLocked()); for (u32 core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { const u32 priority = preemption_priorities[core_id]; - kernel.Scheduler(core_id).RotateScheduledQueue(core_id, priority); - - // Signal an interrupt occurred. For core 3, this is a certainty, as preemption will result - // in the rotator thread being scheduled. For cores 0-2, this is to simulate or system - // interrupts that may have occurred. - kernel.PhysicalCore(core_id).Interrupt(); + KScheduler::RotateScheduledQueue(kernel, core_id, priority); } } diff --git a/src/core/hle/kernel/k_client_port.cpp b/src/core/hle/kernel/k_client_port.cpp index d63e77d15..3cb22ff4d 100644 --- a/src/core/hle/kernel/k_client_port.cpp +++ b/src/core/hle/kernel/k_client_port.cpp @@ -1,6 +1,5 @@ -// Copyright 2021 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2021 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/scope_exit.h" #include "core/hle/kernel/hle_ipc.h" diff --git a/src/core/hle/kernel/k_client_port.h b/src/core/hle/kernel/k_client_port.h index ef8583efc..e17eff28f 100644 --- a/src/core/hle/kernel/k_client_port.h +++ b/src/core/hle/kernel/k_client_port.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/kernel/k_interrupt_manager.cpp b/src/core/hle/kernel/k_interrupt_manager.cpp index d606a7f86..1b577a5b3 100644 --- a/src/core/hle/kernel/k_interrupt_manager.cpp +++ b/src/core/hle/kernel/k_interrupt_manager.cpp @@ -6,6 +6,7 @@ #include "core/hle/kernel/k_scheduler.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/physical_core.h" namespace Kernel::KInterruptManager { @@ -15,6 +16,9 @@ void HandleInterrupt(KernelCore& kernel, s32 core_id) { return; } + // Acknowledge the interrupt. + kernel.PhysicalCore(core_id).ClearInterrupt(); + auto& current_thread = GetCurrentThread(kernel); // If the user disable count is set, we may need to pin the current thread. @@ -27,6 +31,9 @@ void HandleInterrupt(KernelCore& kernel, s32 core_id) { // Set the interrupt flag for the thread. GetCurrentThread(kernel).SetInterruptFlag(); } + + // Request interrupt scheduling. + kernel.CurrentScheduler()->RequestScheduleOnInterrupt(); } } // namespace Kernel::KInterruptManager diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp index b662788b3..d3e99665f 100644 --- a/src/core/hle/kernel/k_process.cpp +++ b/src/core/hle/kernel/k_process.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <bitset> diff --git a/src/core/hle/kernel/k_process.h b/src/core/hle/kernel/k_process.h index 5e3e22ad8..d56d73bab 100644 --- a/src/core/hle/kernel/k_process.h +++ b/src/core/hle/kernel/k_process.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/kernel/k_scheduler.cpp b/src/core/hle/kernel/k_scheduler.cpp index d599d2bcb..c34ce7a17 100644 --- a/src/core/hle/kernel/k_scheduler.cpp +++ b/src/core/hle/kernel/k_scheduler.cpp @@ -27,69 +27,185 @@ static void IncrementScheduledCount(Kernel::KThread* thread) { } } -void KScheduler::RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule) { - auto scheduler = kernel.CurrentScheduler(); - - u32 current_core{0xF}; - bool must_context_switch{}; - if (scheduler) { - current_core = scheduler->core_id; - // TODO(bunnei): Should be set to true when we deprecate single core - must_context_switch = !kernel.IsPhantomModeForSingleCore(); - } - - while (cores_pending_reschedule != 0) { - const auto core = static_cast<u32>(std::countr_zero(cores_pending_reschedule)); - ASSERT(core < Core::Hardware::NUM_CPU_CORES); - if (!must_context_switch || core != current_core) { - auto& phys_core = kernel.PhysicalCore(core); - phys_core.Interrupt(); +KScheduler::KScheduler(KernelCore& kernel_) : kernel{kernel_} { + m_switch_fiber = std::make_shared<Common::Fiber>([this] { + while (true) { + ScheduleImplFiber(); } - cores_pending_reschedule &= ~(1ULL << core); + }); + + m_state.needs_scheduling = true; +} + +KScheduler::~KScheduler() = default; + +void KScheduler::SetInterruptTaskRunnable() { + m_state.interrupt_task_runnable = true; + m_state.needs_scheduling = true; +} + +void KScheduler::RequestScheduleOnInterrupt() { + m_state.needs_scheduling = true; + + if (CanSchedule(kernel)) { + ScheduleOnInterrupt(); } +} - for (std::size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; ++core_id) { - if (kernel.PhysicalCore(core_id).IsInterrupted()) { - KInterruptManager::HandleInterrupt(kernel, static_cast<s32>(core_id)); - } +void KScheduler::DisableScheduling(KernelCore& kernel) { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 0); + GetCurrentThread(kernel).DisableDispatch(); +} + +void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling) { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 1); + + auto* scheduler{kernel.CurrentScheduler()}; + + if (!scheduler || kernel.IsPhantomModeForSingleCore()) { + KScheduler::RescheduleCores(kernel, cores_needing_scheduling); + KScheduler::RescheduleCurrentHLEThread(kernel); + return; + } + + scheduler->RescheduleOtherCores(cores_needing_scheduling); + + if (GetCurrentThread(kernel).GetDisableDispatchCount() > 1) { + GetCurrentThread(kernel).EnableDispatch(); + } else { + scheduler->RescheduleCurrentCore(); + } +} + +void KScheduler::RescheduleCurrentHLEThread(KernelCore& kernel) { + // HACK: we cannot schedule from this thread, it is not a core thread + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + // Special case to ensure dummy threads that are waiting block + GetCurrentThread(kernel).IfDummyThreadTryWait(); + + ASSERT(GetCurrentThread(kernel).GetState() != ThreadState::Waiting); + GetCurrentThread(kernel).EnableDispatch(); +} + +u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { + if (IsSchedulerUpdateNeeded(kernel)) { + return UpdateHighestPriorityThreadsImpl(kernel); + } else { + return 0; + } +} + +void KScheduler::Schedule() { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + ASSERT(m_core_id == GetCurrentCoreId(kernel)); + + ScheduleImpl(); +} + +void KScheduler::ScheduleOnInterrupt() { + GetCurrentThread(kernel).DisableDispatch(); + Schedule(); + GetCurrentThread(kernel).EnableDispatch(); +} + +void KScheduler::PreemptSingleCore() { + GetCurrentThread(kernel).DisableDispatch(); + + auto* thread = GetCurrentThreadPointer(kernel); + auto& previous_scheduler = kernel.Scheduler(thread->GetCurrentCore()); + previous_scheduler.Unload(thread); + + Common::Fiber::YieldTo(thread->GetHostContext(), *m_switch_fiber); + + GetCurrentThread(kernel).EnableDispatch(); +} + +void KScheduler::RescheduleCurrentCore() { + ASSERT(!kernel.IsPhantomModeForSingleCore()); + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + GetCurrentThread(kernel).EnableDispatch(); + + if (m_state.needs_scheduling.load()) { + // Disable interrupts, and then check again if rescheduling is needed. + // KScopedInterruptDisable intr_disable; + + kernel.CurrentScheduler()->RescheduleCurrentCoreImpl(); } +} - if (must_context_switch) { - auto core_scheduler = kernel.CurrentScheduler(); - kernel.ExitSVCProfile(); - core_scheduler->RescheduleCurrentCore(); - kernel.EnterSVCProfile(); +void KScheduler::RescheduleCurrentCoreImpl() { + // Check that scheduling is needed. + if (m_state.needs_scheduling.load()) [[likely]] { + GetCurrentThread(kernel).DisableDispatch(); + Schedule(); + GetCurrentThread(kernel).EnableDispatch(); } } +void KScheduler::Initialize(KThread* main_thread, KThread* idle_thread, s32 core_id) { + // Set core ID/idle thread/interrupt task manager. + m_core_id = core_id; + m_idle_thread = idle_thread; + // m_state.idle_thread_stack = m_idle_thread->GetStackTop(); + // m_state.interrupt_task_manager = &kernel.GetInterruptTaskManager(); + + // Insert the main thread into the priority queue. + // { + // KScopedSchedulerLock lk{kernel}; + // GetPriorityQueue(kernel).PushBack(GetCurrentThreadPointer(kernel)); + // SetSchedulerUpdateNeeded(kernel); + // } + + // Bind interrupt handler. + // kernel.GetInterruptManager().BindHandler( + // GetSchedulerInterruptHandler(kernel), KInterruptName::Scheduler, m_core_id, + // KInterruptController::PriorityLevel::Scheduler, false, false); + + // Set the current thread. + m_current_thread = main_thread; +} + +void KScheduler::Activate() { + ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() == 1); + + // m_state.should_count_idle = KTargetSystem::IsDebugMode(); + m_is_active = true; + RescheduleCurrentCore(); +} + +void KScheduler::OnThreadStart() { + GetCurrentThread(kernel).EnableDispatch(); +} + u64 KScheduler::UpdateHighestPriorityThread(KThread* highest_thread) { - KScopedSpinLock lk{guard}; - if (KThread* prev_highest_thread = state.highest_priority_thread; - prev_highest_thread != highest_thread) { - if (prev_highest_thread != nullptr) { + if (KThread* prev_highest_thread = m_state.highest_priority_thread; + prev_highest_thread != highest_thread) [[likely]] { + if (prev_highest_thread != nullptr) [[likely]] { IncrementScheduledCount(prev_highest_thread); - prev_highest_thread->SetLastScheduledTick(system.CoreTiming().GetCPUTicks()); + prev_highest_thread->SetLastScheduledTick(kernel.System().CoreTiming().GetCPUTicks()); } - if (state.should_count_idle) { - if (highest_thread != nullptr) { + if (m_state.should_count_idle) { + if (highest_thread != nullptr) [[likely]] { if (KProcess* process = highest_thread->GetOwnerProcess(); process != nullptr) { - process->SetRunningThread(core_id, highest_thread, state.idle_count); + process->SetRunningThread(m_core_id, highest_thread, m_state.idle_count); } } else { - state.idle_count++; + m_state.idle_count++; } } - state.highest_priority_thread = highest_thread; - state.needs_scheduling.store(true); - return (1ULL << core_id); + m_state.highest_priority_thread = highest_thread; + m_state.needs_scheduling = true; + return (1ULL << m_core_id); } else { return 0; } } u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Clear that we need to update. ClearSchedulerUpdateNeeded(kernel); @@ -98,18 +214,20 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { KThread* top_threads[Core::Hardware::NUM_CPU_CORES]; auto& priority_queue = GetPriorityQueue(kernel); - /// We want to go over all cores, finding the highest priority thread and determining if - /// scheduling is needed for that core. + // We want to go over all cores, finding the highest priority thread and determining if + // scheduling is needed for that core. for (size_t core_id = 0; core_id < Core::Hardware::NUM_CPU_CORES; core_id++) { KThread* top_thread = priority_queue.GetScheduledFront(static_cast<s32>(core_id)); if (top_thread != nullptr) { - // If the thread has no waiters, we need to check if the process has a thread pinned. - if (top_thread->GetNumKernelWaiters() == 0) { - if (KProcess* parent = top_thread->GetOwnerProcess(); parent != nullptr) { - if (KThread* pinned = parent->GetPinnedThread(static_cast<s32>(core_id)); - pinned != nullptr && pinned != top_thread) { - // We prefer our parent's pinned thread if possible. However, we also don't - // want to schedule un-runnable threads. + // We need to check if the thread's process has a pinned thread. + if (KProcess* parent = top_thread->GetOwnerProcess()) { + // Check that there's a pinned thread other than the current top thread. + if (KThread* pinned = parent->GetPinnedThread(static_cast<s32>(core_id)); + pinned != nullptr && pinned != top_thread) { + // We need to prefer threads with kernel waiters to the pinned thread. + if (top_thread->GetNumKernelWaiters() == + 0 /* && top_thread != parent->GetExceptionThread() */) { + // If the pinned thread is runnable, use it. if (pinned->GetRawState() == ThreadState::Runnable) { top_thread = pinned; } else { @@ -129,7 +247,8 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // Idle cores are bad. We're going to try to migrate threads to each idle core in turn. while (idle_cores != 0) { - const auto core_id = static_cast<u32>(std::countr_zero(idle_cores)); + const s32 core_id = static_cast<s32>(std::countr_zero(idle_cores)); + if (KThread* suggested = priority_queue.GetSuggestedFront(core_id); suggested != nullptr) { s32 migration_candidates[Core::Hardware::NUM_CPU_CORES]; size_t num_candidates = 0; @@ -150,7 +269,6 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // The suggested thread isn't bound to its core, so we can migrate it! suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested); - top_threads[core_id] = suggested; cores_needing_scheduling |= kernel.Scheduler(core_id).UpdateHighestPriorityThread(top_threads[core_id]); @@ -183,7 +301,6 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { // Perform the migration. suggested->SetActiveCore(core_id); priority_queue.ChangeCore(candidate_core, suggested); - top_threads[core_id] = suggested; cores_needing_scheduling |= kernel.Scheduler(core_id).UpdateHighestPriorityThread( @@ -200,24 +317,210 @@ u64 KScheduler::UpdateHighestPriorityThreadsImpl(KernelCore& kernel) { return cores_needing_scheduling; } +void KScheduler::SwitchThread(KThread* next_thread) { + KProcess* const cur_process = kernel.CurrentProcess(); + KThread* const cur_thread = GetCurrentThreadPointer(kernel); + + // We never want to schedule a null thread, so use the idle thread if we don't have a next. + if (next_thread == nullptr) { + next_thread = m_idle_thread; + } + + if (next_thread->GetCurrentCore() != m_core_id) { + next_thread->SetCurrentCore(m_core_id); + } + + // If we're not actually switching thread, there's nothing to do. + if (next_thread == cur_thread) { + return; + } + + // Next thread is now known not to be nullptr, and must not be dispatchable. + ASSERT(next_thread->GetDisableDispatchCount() == 1); + ASSERT(!next_thread->IsDummyThread()); + + // Update the CPU time tracking variables. + const s64 prev_tick = m_last_context_switch_time; + const s64 cur_tick = kernel.System().CoreTiming().GetCPUTicks(); + const s64 tick_diff = cur_tick - prev_tick; + cur_thread->AddCpuTime(m_core_id, tick_diff); + if (cur_process != nullptr) { + cur_process->UpdateCPUTimeTicks(tick_diff); + } + m_last_context_switch_time = cur_tick; + + // Update our previous thread. + if (cur_process != nullptr) { + if (!cur_thread->IsTerminationRequested() && cur_thread->GetActiveCore() == m_core_id) + [[likely]] { + m_state.prev_thread = cur_thread; + } else { + m_state.prev_thread = nullptr; + } + } + + // Switch the current process, if we're switching processes. + // if (KProcess *next_process = next_thread->GetOwnerProcess(); next_process != cur_process) { + // KProcess::Switch(cur_process, next_process); + // } + + // Set the new thread. + SetCurrentThread(kernel, next_thread); + m_current_thread = next_thread; + + // Set the new Thread Local region. + // cpu::SwitchThreadLocalRegion(GetInteger(next_thread->GetThreadLocalRegionAddress())); +} + +void KScheduler::ScheduleImpl() { + // First, clear the needs scheduling bool. + m_state.needs_scheduling.store(false, std::memory_order_seq_cst); + + // Load the appropriate thread pointers for scheduling. + KThread* const cur_thread{GetCurrentThreadPointer(kernel)}; + KThread* highest_priority_thread{m_state.highest_priority_thread}; + + // Check whether there are runnable interrupt tasks. + if (m_state.interrupt_task_runnable) { + // The interrupt task is runnable. + // We want to switch to the interrupt task/idle thread. + highest_priority_thread = nullptr; + } + + // If there aren't, we want to check if the highest priority thread is the same as the current + // thread. + if (highest_priority_thread == cur_thread) { + // If they're the same, then we can just return. + return; + } + + // The highest priority thread is not the same as the current thread. + // Jump to the switcher and continue executing from there. + m_switch_cur_thread = cur_thread; + m_switch_highest_priority_thread = highest_priority_thread; + m_switch_from_schedule = true; + Common::Fiber::YieldTo(cur_thread->host_context, *m_switch_fiber); + + // Returning from ScheduleImpl occurs after this thread has been scheduled again. +} + +void KScheduler::ScheduleImplFiber() { + KThread* const cur_thread{m_switch_cur_thread}; + KThread* highest_priority_thread{m_switch_highest_priority_thread}; + + // If we're not coming from scheduling (i.e., we came from SC preemption), + // we should restart the scheduling loop directly. Not accurate to HOS. + if (!m_switch_from_schedule) { + goto retry; + } + + // Mark that we are not coming from scheduling anymore. + m_switch_from_schedule = false; + + // Save the original thread context. + Unload(cur_thread); + + // The current thread's context has been entirely taken care of. + // Now we want to loop until we successfully switch the thread context. + while (true) { + // We're starting to try to do the context switch. + // Check if the highest priority thread is null. + if (!highest_priority_thread) { + // The next thread is nullptr! + + // Switch to the idle thread. Note: HOS treats idling as a special case for + // performance. This is not *required* for yuzu's purposes, and for singlecore + // compatibility, we can just move the logic that would go here into the execution + // of the idle thread. If we ever remove singlecore, we should implement this + // accurately to HOS. + highest_priority_thread = m_idle_thread; + } + + // We want to try to lock the highest priority thread's context. + // Try to take it. + while (!highest_priority_thread->context_guard.try_lock()) { + // The highest priority thread's context is already locked. + // Check if we need scheduling. If we don't, we can retry directly. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // If we do, another core is interfering, and we must start again. + goto retry; + } + } + + // It's time to switch the thread. + // Switch to the highest priority thread. + SwitchThread(highest_priority_thread); + + // Check if we need scheduling. If we do, then we can't complete the switch and should + // retry. + if (m_state.needs_scheduling.load(std::memory_order_seq_cst)) { + // Our switch failed. + // We should unlock the thread context, and then retry. + highest_priority_thread->context_guard.unlock(); + goto retry; + } else { + break; + } + + retry: + + // We failed to successfully do the context switch, and need to retry. + // Clear needs_scheduling. + m_state.needs_scheduling.store(false, std::memory_order_seq_cst); + + // Refresh the highest priority thread. + highest_priority_thread = m_state.highest_priority_thread; + } + + // Reload the guest thread context. + Reload(highest_priority_thread); + + // Reload the host thread. + Common::Fiber::YieldTo(m_switch_fiber, *highest_priority_thread->host_context); +} + +void KScheduler::Unload(KThread* thread) { + auto& cpu_core = kernel.System().ArmInterface(m_core_id); + cpu_core.SaveContext(thread->GetContext32()); + cpu_core.SaveContext(thread->GetContext64()); + // Save the TPIDR_EL0 system register in case it was modified. + thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); + cpu_core.ClearExclusiveState(); + + // Check if the thread is terminated by checking the DPC flags. + if ((thread->GetStackParameters().dpc_flags & static_cast<u32>(DpcFlag::Terminated)) == 0) { + // The thread isn't terminated, so we want to unlock it. + thread->context_guard.unlock(); + } +} + +void KScheduler::Reload(KThread* thread) { + auto& cpu_core = kernel.System().ArmInterface(m_core_id); + cpu_core.LoadContext(thread->GetContext32()); + cpu_core.LoadContext(thread->GetContext64()); + cpu_core.SetTlsAddress(thread->GetTLSAddress()); + cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); + cpu_core.LoadWatchpointArray(thread->GetOwnerProcess()->GetWatchpoints()); + cpu_core.ClearExclusiveState(); +} + void KScheduler::ClearPreviousThread(KernelCore& kernel, KThread* thread) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; ++i) { // Get an atomic reference to the core scheduler's previous thread. - std::atomic_ref<KThread*> prev_thread(kernel.Scheduler(static_cast<s32>(i)).prev_thread); - static_assert(std::atomic_ref<KThread*>::is_always_lock_free); + auto& prev_thread{kernel.Scheduler(i).m_state.prev_thread}; // Atomically clear the previous thread if it's our target. KThread* compare = thread; - prev_thread.compare_exchange_strong(compare, nullptr); + prev_thread.compare_exchange_strong(compare, nullptr, std::memory_order_seq_cst); } } void KScheduler::OnThreadStateChanged(KernelCore& kernel, KThread* thread, ThreadState old_state) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Check if the state has changed, because if it hasn't there's nothing to do. - const auto cur_state = thread->GetRawState(); + const ThreadState cur_state = thread->GetRawState(); if (cur_state == old_state) { return; } @@ -237,12 +540,12 @@ void KScheduler::OnThreadStateChanged(KernelCore& kernel, KThread* thread, Threa } void KScheduler::OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s32 old_priority) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // If the thread is runnable, we want to change its priority in the queue. if (thread->GetRawState() == ThreadState::Runnable) { GetPriorityQueue(kernel).ChangePriority(old_priority, - thread == kernel.GetCurrentEmuThread(), thread); + thread == GetCurrentThreadPointer(kernel), thread); IncrementScheduledCount(thread); SetSchedulerUpdateNeeded(kernel); } @@ -250,7 +553,7 @@ void KScheduler::OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s3 void KScheduler::OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread, const KAffinityMask& old_affinity, s32 old_core) { - ASSERT(kernel.GlobalSchedulerContext().IsLocked()); + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // If the thread is runnable, we want to change its affinity in the queue. if (thread->GetRawState() == ThreadState::Runnable) { @@ -260,15 +563,14 @@ void KScheduler::OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread } } -void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { - ASSERT(system.GlobalSchedulerContext().IsLocked()); +void KScheduler::RotateScheduledQueue(KernelCore& kernel, s32 core_id, s32 priority) { + ASSERT(IsSchedulerLockedByCurrentThread(kernel)); // Get a reference to the priority queue. - auto& kernel = system.Kernel(); auto& priority_queue = GetPriorityQueue(kernel); // Rotate the front of the queue to the end. - KThread* top_thread = priority_queue.GetScheduledFront(cpu_core_id, priority); + KThread* top_thread = priority_queue.GetScheduledFront(core_id, priority); KThread* next_thread = nullptr; if (top_thread != nullptr) { next_thread = priority_queue.MoveToScheduledBack(top_thread); @@ -280,7 +582,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { // While we have a suggested thread, try to migrate it! { - KThread* suggested = priority_queue.GetSuggestedFront(cpu_core_id, priority); + KThread* suggested = priority_queue.GetSuggestedFront(core_id, priority); while (suggested != nullptr) { // Check if the suggested thread is the top thread on its core. const s32 suggested_core = suggested->GetActiveCore(); @@ -301,7 +603,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { // to the front of the queue. if (top_on_suggested_core == nullptr || top_on_suggested_core->GetPriority() >= HighestCoreMigrationAllowedPriority) { - suggested->SetActiveCore(cpu_core_id); + suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested, true); IncrementScheduledCount(suggested); break; @@ -309,22 +611,21 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { } // Get the next suggestion. - suggested = priority_queue.GetSamePriorityNext(cpu_core_id, suggested); + suggested = priority_queue.GetSamePriorityNext(core_id, suggested); } } // Now that we might have migrated a thread with the same priority, check if we can do better. - { - KThread* best_thread = priority_queue.GetScheduledFront(cpu_core_id); + KThread* best_thread = priority_queue.GetScheduledFront(core_id); if (best_thread == GetCurrentThreadPointer(kernel)) { - best_thread = priority_queue.GetScheduledNext(cpu_core_id, best_thread); + best_thread = priority_queue.GetScheduledNext(core_id, best_thread); } // If the best thread we can choose has a priority the same or worse than ours, try to // migrate a higher priority thread. if (best_thread != nullptr && best_thread->GetPriority() >= priority) { - KThread* suggested = priority_queue.GetSuggestedFront(cpu_core_id); + KThread* suggested = priority_queue.GetSuggestedFront(core_id); while (suggested != nullptr) { // If the suggestion's priority is the same as ours, don't bother. if (suggested->GetPriority() >= best_thread->GetPriority()) { @@ -343,7 +644,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { if (top_on_suggested_core == nullptr || top_on_suggested_core->GetPriority() >= HighestCoreMigrationAllowedPriority) { - suggested->SetActiveCore(cpu_core_id); + suggested->SetActiveCore(core_id); priority_queue.ChangeCore(suggested_core, suggested, true); IncrementScheduledCount(suggested); break; @@ -351,7 +652,7 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { } // Get the next suggestion. - suggested = priority_queue.GetSuggestedNext(cpu_core_id, suggested); + suggested = priority_queue.GetSuggestedNext(core_id, suggested); } } } @@ -360,64 +661,6 @@ void KScheduler::RotateScheduledQueue(s32 cpu_core_id, s32 priority) { SetSchedulerUpdateNeeded(kernel); } -bool KScheduler::CanSchedule(KernelCore& kernel) { - return kernel.GetCurrentEmuThread()->GetDisableDispatchCount() <= 1; -} - -bool KScheduler::IsSchedulerUpdateNeeded(const KernelCore& kernel) { - return kernel.GlobalSchedulerContext().scheduler_update_needed.load(std::memory_order_acquire); -} - -void KScheduler::SetSchedulerUpdateNeeded(KernelCore& kernel) { - kernel.GlobalSchedulerContext().scheduler_update_needed.store(true, std::memory_order_release); -} - -void KScheduler::ClearSchedulerUpdateNeeded(KernelCore& kernel) { - kernel.GlobalSchedulerContext().scheduler_update_needed.store(false, std::memory_order_release); -} - -void KScheduler::DisableScheduling(KernelCore& kernel) { - // If we are shutting down the kernel, none of this is relevant anymore. - if (kernel.IsShuttingDown()) { - return; - } - - ASSERT(GetCurrentThreadPointer(kernel)->GetDisableDispatchCount() >= 0); - GetCurrentThreadPointer(kernel)->DisableDispatch(); -} - -void KScheduler::EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling) { - // If we are shutting down the kernel, none of this is relevant anymore. - if (kernel.IsShuttingDown()) { - return; - } - - auto* current_thread = GetCurrentThreadPointer(kernel); - - ASSERT(current_thread->GetDisableDispatchCount() >= 1); - - if (current_thread->GetDisableDispatchCount() > 1) { - current_thread->EnableDispatch(); - } else { - RescheduleCores(kernel, cores_needing_scheduling); - } - - // Special case to ensure dummy threads that are waiting block. - current_thread->IfDummyThreadTryWait(); -} - -u64 KScheduler::UpdateHighestPriorityThreads(KernelCore& kernel) { - if (IsSchedulerUpdateNeeded(kernel)) { - return UpdateHighestPriorityThreadsImpl(kernel); - } else { - return 0; - } -} - -KSchedulerPriorityQueue& KScheduler::GetPriorityQueue(KernelCore& kernel) { - return kernel.GlobalSchedulerContext().priority_queue; -} - void KScheduler::YieldWithoutCoreMigration(KernelCore& kernel) { // Validate preconditions. ASSERT(CanSchedule(kernel)); @@ -437,7 +680,7 @@ void KScheduler::YieldWithoutCoreMigration(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -476,7 +719,7 @@ void KScheduler::YieldWithCoreMigration(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -496,7 +739,7 @@ void KScheduler::YieldWithCoreMigration(KernelCore& kernel) { if (KThread* running_on_suggested_core = (suggested_core >= 0) - ? kernel.Scheduler(suggested_core).state.highest_priority_thread + ? kernel.Scheduler(suggested_core).m_state.highest_priority_thread : nullptr; running_on_suggested_core != suggested) { // If the current thread's priority is higher than our suggestion's we prefer @@ -564,7 +807,7 @@ void KScheduler::YieldToAnyThread(KernelCore& kernel) { // Perform the yield. { - KScopedSchedulerLock lock(kernel); + KScopedSchedulerLock sl{kernel}; const auto cur_state = cur_thread.GetRawState(); if (cur_state == ThreadState::Runnable) { @@ -621,223 +864,19 @@ void KScheduler::YieldToAnyThread(KernelCore& kernel) { } } -KScheduler::KScheduler(Core::System& system_, s32 core_id_) : system{system_}, core_id{core_id_} { - switch_fiber = std::make_shared<Common::Fiber>([this] { SwitchToCurrent(); }); - state.needs_scheduling.store(true); - state.interrupt_task_thread_runnable = false; - state.should_count_idle = false; - state.idle_count = 0; - state.idle_thread_stack = nullptr; - state.highest_priority_thread = nullptr; -} - -void KScheduler::Finalize() { - if (idle_thread) { - idle_thread->Close(); - idle_thread = nullptr; - } -} - -KScheduler::~KScheduler() { - ASSERT(!idle_thread); -} - -KThread* KScheduler::GetSchedulerCurrentThread() const { - if (auto result = current_thread.load(); result) { - return result; +void KScheduler::RescheduleOtherCores(u64 cores_needing_scheduling) { + if (const u64 core_mask = cores_needing_scheduling & ~(1ULL << m_core_id); core_mask != 0) { + RescheduleCores(kernel, core_mask); } - return idle_thread; -} - -u64 KScheduler::GetLastContextSwitchTicks() const { - return last_context_switch_time; } -void KScheduler::RescheduleCurrentCore() { - ASSERT(GetCurrentThread(system.Kernel()).GetDisableDispatchCount() == 1); - - auto& phys_core = system.Kernel().PhysicalCore(core_id); - if (phys_core.IsInterrupted()) { - phys_core.ClearInterrupt(); - } - - guard.Lock(); - if (state.needs_scheduling.load()) { - Schedule(); - } else { - GetCurrentThread(system.Kernel()).EnableDispatch(); - guard.Unlock(); - } -} - -void KScheduler::OnThreadStart() { - SwitchContextStep2(); -} - -void KScheduler::Unload(KThread* thread) { - ASSERT(thread); - - LOG_TRACE(Kernel, "core {}, unload thread {}", core_id, thread ? thread->GetName() : "nullptr"); - - if (thread->IsCallingSvc()) { - thread->ClearIsCallingSvc(); - } - - auto& physical_core = system.Kernel().PhysicalCore(core_id); - if (!physical_core.IsInitialized()) { - return; - } - - Core::ARM_Interface& cpu_core = physical_core.ArmInterface(); - cpu_core.SaveContext(thread->GetContext32()); - cpu_core.SaveContext(thread->GetContext64()); - // Save the TPIDR_EL0 system register in case it was modified. - thread->SetTPIDR_EL0(cpu_core.GetTPIDR_EL0()); - cpu_core.ClearExclusiveState(); - - if (!thread->IsTerminationRequested() && thread->GetActiveCore() == core_id) { - prev_thread = thread; - } else { - prev_thread = nullptr; - } - - thread->context_guard.unlock(); -} - -void KScheduler::Reload(KThread* thread) { - LOG_TRACE(Kernel, "core {}, reload thread {}", core_id, thread->GetName()); - - Core::ARM_Interface& cpu_core = system.ArmInterface(core_id); - cpu_core.LoadContext(thread->GetContext32()); - cpu_core.LoadContext(thread->GetContext64()); - cpu_core.LoadWatchpointArray(thread->GetOwnerProcess()->GetWatchpoints()); - cpu_core.SetTlsAddress(thread->GetTLSAddress()); - cpu_core.SetTPIDR_EL0(thread->GetTPIDR_EL0()); - cpu_core.ClearExclusiveState(); -} - -void KScheduler::SwitchContextStep2() { - // Load context of new thread - Reload(GetCurrentThreadPointer(system.Kernel())); - - RescheduleCurrentCore(); -} - -void KScheduler::Schedule() { - ASSERT(GetCurrentThread(system.Kernel()).GetDisableDispatchCount() == 1); - this->ScheduleImpl(); -} - -void KScheduler::ScheduleImpl() { - KThread* previous_thread = GetCurrentThreadPointer(system.Kernel()); - KThread* next_thread = state.highest_priority_thread; - - state.needs_scheduling.store(false); - - // We never want to schedule a null thread, so use the idle thread if we don't have a next. - if (next_thread == nullptr) { - next_thread = idle_thread; - } - - if (next_thread->GetCurrentCore() != core_id) { - next_thread->SetCurrentCore(core_id); - } - - // We never want to schedule a dummy thread, as these are only used by host threads for locking. - if (next_thread->GetThreadType() == ThreadType::Dummy) { - ASSERT_MSG(false, "Dummy threads should never be scheduled!"); - next_thread = idle_thread; - } - - // If we're not actually switching thread, there's nothing to do. - if (next_thread == current_thread.load()) { - previous_thread->EnableDispatch(); - guard.Unlock(); - return; - } - - // Update the CPU time tracking variables. - KProcess* const previous_process = system.Kernel().CurrentProcess(); - UpdateLastContextSwitchTime(previous_thread, previous_process); - - // Save context for previous thread - Unload(previous_thread); - - std::shared_ptr<Common::Fiber>* old_context; - old_context = &previous_thread->GetHostContext(); - - // Set the new thread. - SetCurrentThread(system.Kernel(), next_thread); - current_thread.store(next_thread); - - guard.Unlock(); - - Common::Fiber::YieldTo(*old_context, *switch_fiber); - /// When a thread wakes up, the scheduler may have changed to other in another core. - auto& next_scheduler = *system.Kernel().CurrentScheduler(); - next_scheduler.SwitchContextStep2(); -} - -void KScheduler::SwitchToCurrent() { - while (true) { - { - KScopedSpinLock lk{guard}; - current_thread.store(state.highest_priority_thread); - state.needs_scheduling.store(false); +void KScheduler::RescheduleCores(KernelCore& kernel, u64 core_mask) { + // Send IPI + for (size_t i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { + if (core_mask & (1ULL << i)) { + kernel.PhysicalCore(i).Interrupt(); } - const auto is_switch_pending = [this] { - KScopedSpinLock lk{guard}; - return state.needs_scheduling.load(); - }; - do { - auto next_thread = current_thread.load(); - if (next_thread != nullptr) { - const auto locked = next_thread->context_guard.try_lock(); - if (state.needs_scheduling.load()) { - next_thread->context_guard.unlock(); - break; - } - if (next_thread->GetActiveCore() != core_id) { - next_thread->context_guard.unlock(); - break; - } - if (!locked) { - continue; - } - } - auto thread = next_thread ? next_thread : idle_thread; - SetCurrentThread(system.Kernel(), thread); - Common::Fiber::YieldTo(switch_fiber, *thread->GetHostContext()); - } while (!is_switch_pending()); } } -void KScheduler::UpdateLastContextSwitchTime(KThread* thread, KProcess* process) { - const u64 prev_switch_ticks = last_context_switch_time; - const u64 most_recent_switch_ticks = system.CoreTiming().GetCPUTicks(); - const u64 update_ticks = most_recent_switch_ticks - prev_switch_ticks; - - if (thread != nullptr) { - thread->AddCpuTime(core_id, update_ticks); - } - - if (process != nullptr) { - process->UpdateCPUTimeTicks(update_ticks); - } - - last_context_switch_time = most_recent_switch_ticks; -} - -void KScheduler::Initialize() { - idle_thread = KThread::Create(system.Kernel()); - ASSERT(KThread::InitializeIdleThread(system, idle_thread, core_id).IsSuccess()); - idle_thread->SetName(fmt::format("IdleThread:{}", core_id)); - idle_thread->EnableDispatch(); -} - -KScopedSchedulerLock::KScopedSchedulerLock(KernelCore& kernel) - : KScopedLock(kernel.GlobalSchedulerContext().SchedulerLock()) {} - -KScopedSchedulerLock::~KScopedSchedulerLock() = default; - } // namespace Kernel diff --git a/src/core/hle/kernel/k_scheduler.h b/src/core/hle/kernel/k_scheduler.h index 6a4760eca..534321d8d 100644 --- a/src/core/hle/kernel/k_scheduler.h +++ b/src/core/hle/kernel/k_scheduler.h @@ -11,6 +11,7 @@ #include "core/hle/kernel/k_scheduler_lock.h" #include "core/hle/kernel/k_scoped_lock.h" #include "core/hle/kernel/k_spin_lock.h" +#include "core/hle/kernel/k_thread.h" namespace Common { class Fiber; @@ -23,184 +24,150 @@ class System; namespace Kernel { class KernelCore; +class KInterruptTaskManager; class KProcess; -class SchedulerLock; class KThread; +class KScopedDisableDispatch; +class KScopedSchedulerLock; +class KScopedSchedulerLockAndSleep; class KScheduler final { public: - explicit KScheduler(Core::System& system_, s32 core_id_); - ~KScheduler(); - - void Finalize(); + YUZU_NON_COPYABLE(KScheduler); + YUZU_NON_MOVEABLE(KScheduler); - /// Reschedules to the next available thread (call after current thread is suspended) - void RescheduleCurrentCore(); + using LockType = KAbstractSchedulerLock<KScheduler>; - /// Reschedules cores pending reschedule, to be called on EnableScheduling. - static void RescheduleCores(KernelCore& kernel, u64 cores_pending_reschedule); + explicit KScheduler(KernelCore& kernel); + ~KScheduler(); - /// The next two are for SingleCore Only. - /// Unload current thread before preempting core. + void Initialize(KThread* main_thread, KThread* idle_thread, s32 core_id); + void Activate(); + void OnThreadStart(); void Unload(KThread* thread); - - /// Reload current thread after core preemption. void Reload(KThread* thread); - /// Gets the current running thread - [[nodiscard]] KThread* GetSchedulerCurrentThread() const; + void SetInterruptTaskRunnable(); + void RequestScheduleOnInterrupt(); + void PreemptSingleCore(); - /// Gets the idle thread - [[nodiscard]] KThread* GetIdleThread() const { - return idle_thread; + u64 GetIdleCount() { + return m_state.idle_count; } - /// Returns true if the scheduler is idle - [[nodiscard]] bool IsIdle() const { - return GetSchedulerCurrentThread() == idle_thread; + KThread* GetIdleThread() const { + return m_idle_thread; } - /// Gets the timestamp for the last context switch in ticks. - [[nodiscard]] u64 GetLastContextSwitchTicks() const; - - [[nodiscard]] bool ContextSwitchPending() const { - return state.needs_scheduling.load(std::memory_order_relaxed); + bool IsIdle() const { + return m_current_thread.load() == m_idle_thread; } - void Initialize(); + KThread* GetPreviousThread() const { + return m_state.prev_thread; + } - void OnThreadStart(); + KThread* GetSchedulerCurrentThread() const { + return m_current_thread.load(); + } - [[nodiscard]] std::shared_ptr<Common::Fiber>& ControlContext() { - return switch_fiber; + s64 GetLastContextSwitchTime() const { + return m_last_context_switch_time; } - [[nodiscard]] const std::shared_ptr<Common::Fiber>& ControlContext() const { - return switch_fiber; + // Static public API. + static bool CanSchedule(KernelCore& kernel) { + return GetCurrentThread(kernel).GetDisableDispatchCount() == 0; + } + static bool IsSchedulerLockedByCurrentThread(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().scheduler_lock.IsLockedByCurrentThread(); } - [[nodiscard]] u64 UpdateHighestPriorityThread(KThread* highest_thread); + static bool IsSchedulerUpdateNeeded(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().scheduler_update_needed; + } + static void SetSchedulerUpdateNeeded(KernelCore& kernel) { + kernel.GlobalSchedulerContext().scheduler_update_needed = true; + } + static void ClearSchedulerUpdateNeeded(KernelCore& kernel) { + kernel.GlobalSchedulerContext().scheduler_update_needed = false; + } - /** - * Takes a thread and moves it to the back of the it's priority list. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldWithoutCoreMigration(KernelCore& kernel); + static void DisableScheduling(KernelCore& kernel); + static void EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling); - /** - * Takes a thread and moves it to the back of the it's priority list. - * Afterwards, tries to pick a suggested thread from the suggested queue that has worse time or - * a better priority than the next thread in the core. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldWithCoreMigration(KernelCore& kernel); - - /** - * Takes a thread and moves it out of the scheduling queue. - * and into the suggested queue. If no thread can be scheduled afterwards in that core, - * a suggested thread is obtained instead. - * - * @note This operation can be redundant and no scheduling is changed if marked as so. - */ - static void YieldToAnyThread(KernelCore& kernel); + static u64 UpdateHighestPriorityThreads(KernelCore& kernel); static void ClearPreviousThread(KernelCore& kernel, KThread* thread); - /// Notify the scheduler a thread's status has changed. static void OnThreadStateChanged(KernelCore& kernel, KThread* thread, ThreadState old_state); - - /// Notify the scheduler a thread's priority has changed. static void OnThreadPriorityChanged(KernelCore& kernel, KThread* thread, s32 old_priority); - - /// Notify the scheduler a thread's core and/or affinity mask has changed. static void OnThreadAffinityMaskChanged(KernelCore& kernel, KThread* thread, const KAffinityMask& old_affinity, s32 old_core); - static bool CanSchedule(KernelCore& kernel); - static bool IsSchedulerUpdateNeeded(const KernelCore& kernel); - static void SetSchedulerUpdateNeeded(KernelCore& kernel); - static void ClearSchedulerUpdateNeeded(KernelCore& kernel); - static void DisableScheduling(KernelCore& kernel); - static void EnableScheduling(KernelCore& kernel, u64 cores_needing_scheduling); - [[nodiscard]] static u64 UpdateHighestPriorityThreads(KernelCore& kernel); + static void RotateScheduledQueue(KernelCore& kernel, s32 core_id, s32 priority); + static void RescheduleCores(KernelCore& kernel, u64 cores_needing_scheduling); + + static void YieldWithoutCoreMigration(KernelCore& kernel); + static void YieldWithCoreMigration(KernelCore& kernel); + static void YieldToAnyThread(KernelCore& kernel); private: - friend class GlobalSchedulerContext; - - /** - * Takes care of selecting the new scheduled threads in three steps: - * - * 1. First a thread is selected from the top of the priority queue. If no thread - * is obtained then we move to step two, else we are done. - * - * 2. Second we try to get a suggested thread that's not assigned to any core or - * that is not the top thread in that core. - * - * 3. Third is no suggested thread is found, we do a second pass and pick a running - * thread in another core and swap it with its current thread. - * - * returns the cores needing scheduling. - */ - [[nodiscard]] static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); - - [[nodiscard]] static KSchedulerPriorityQueue& GetPriorityQueue(KernelCore& kernel); - - void RotateScheduledQueue(s32 cpu_core_id, s32 priority); + // Static private API. + static KSchedulerPriorityQueue& GetPriorityQueue(KernelCore& kernel) { + return kernel.GlobalSchedulerContext().priority_queue; + } + static u64 UpdateHighestPriorityThreadsImpl(KernelCore& kernel); - void Schedule(); + static void RescheduleCurrentHLEThread(KernelCore& kernel); - /// Switches the CPU's active thread context to that of the specified thread + // Instanced private API. void ScheduleImpl(); + void ScheduleImplFiber(); + void SwitchThread(KThread* next_thread); - /// When a thread wakes up, it must run this through it's new scheduler - void SwitchContextStep2(); - - /** - * Called on every context switch to update the internal timestamp - * This also updates the running time ticks for the given thread and - * process using the following difference: - * - * ticks += most_recent_ticks - last_context_switch_ticks - * - * The internal tick timestamp for the scheduler is simply the - * most recent tick count retrieved. No special arithmetic is - * applied to it. - */ - void UpdateLastContextSwitchTime(KThread* thread, KProcess* process); - - void SwitchToCurrent(); + void Schedule(); + void ScheduleOnInterrupt(); - KThread* prev_thread{}; - std::atomic<KThread*> current_thread{}; + void RescheduleOtherCores(u64 cores_needing_scheduling); + void RescheduleCurrentCore(); + void RescheduleCurrentCoreImpl(); - KThread* idle_thread{}; + u64 UpdateHighestPriorityThread(KThread* thread); - std::shared_ptr<Common::Fiber> switch_fiber{}; +private: + friend class KScopedDisableDispatch; struct SchedulingState { - std::atomic<bool> needs_scheduling{}; - bool interrupt_task_thread_runnable{}; - bool should_count_idle{}; - u64 idle_count{}; - KThread* highest_priority_thread{}; - void* idle_thread_stack{}; + std::atomic<bool> needs_scheduling{false}; + bool interrupt_task_runnable{false}; + bool should_count_idle{false}; + u64 idle_count{0}; + KThread* highest_priority_thread{nullptr}; + void* idle_thread_stack{nullptr}; + std::atomic<KThread*> prev_thread{nullptr}; + KInterruptTaskManager* interrupt_task_manager{nullptr}; }; - SchedulingState state; - - Core::System& system; - u64 last_context_switch_time{}; - const s32 core_id; - - KSpinLock guard{}; + KernelCore& kernel; + SchedulingState m_state; + bool m_is_active{false}; + s32 m_core_id{0}; + s64 m_last_context_switch_time{0}; + KThread* m_idle_thread{nullptr}; + std::atomic<KThread*> m_current_thread{nullptr}; + + std::shared_ptr<Common::Fiber> m_switch_fiber{}; + KThread* m_switch_cur_thread{}; + KThread* m_switch_highest_priority_thread{}; + bool m_switch_from_schedule{}; }; -class [[nodiscard]] KScopedSchedulerLock : KScopedLock<GlobalSchedulerContext::LockType> { +class KScopedSchedulerLock : public KScopedLock<KScheduler::LockType> { public: - explicit KScopedSchedulerLock(KernelCore& kernel); - ~KScopedSchedulerLock(); + explicit KScopedSchedulerLock(KernelCore& kernel) + : KScopedLock(kernel.GlobalSchedulerContext().scheduler_lock) {} + ~KScopedSchedulerLock() = default; }; } // namespace Kernel diff --git a/src/core/hle/kernel/k_scheduler_lock.h b/src/core/hle/kernel/k_scheduler_lock.h index 4fa256970..73314b45e 100644 --- a/src/core/hle/kernel/k_scheduler_lock.h +++ b/src/core/hle/kernel/k_scheduler_lock.h @@ -5,9 +5,11 @@ #include <atomic> #include "common/assert.h" +#include "core/hle/kernel/k_interrupt_manager.h" #include "core/hle/kernel/k_spin_lock.h" #include "core/hle/kernel/k_thread.h" #include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/physical_core.h" namespace Kernel { diff --git a/src/core/hle/kernel/k_shared_memory.cpp b/src/core/hle/kernel/k_shared_memory.cpp index b77735736..8ff1545b6 100644 --- a/src/core/hle/kernel/k_shared_memory.cpp +++ b/src/core/hle/kernel/k_shared_memory.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/assert.h" #include "core/core.h" diff --git a/src/core/hle/kernel/k_shared_memory.h b/src/core/hle/kernel/k_shared_memory.h index 2c1db0e70..34cb98456 100644 --- a/src/core/hle/kernel/k_shared_memory.h +++ b/src/core/hle/kernel/k_shared_memory.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp index 90de86770..174afc80d 100644 --- a/src/core/hle/kernel/k_thread.cpp +++ b/src/core/hle/kernel/k_thread.cpp @@ -258,7 +258,18 @@ Result KThread::InitializeThread(KThread* thread, KThreadFunction func, uintptr_ } Result KThread::InitializeDummyThread(KThread* thread) { - return thread->Initialize({}, {}, {}, DummyThreadPriority, 3, {}, ThreadType::Dummy); + // Initialize the thread. + R_TRY(thread->Initialize({}, {}, {}, DummyThreadPriority, 3, {}, ThreadType::Dummy)); + + // Initialize emulation parameters. + thread->stack_parameters.disable_count = 0; + + return ResultSuccess; +} + +Result KThread::InitializeMainThread(Core::System& system, KThread* thread, s32 virt_core) { + return InitializeThread(thread, {}, {}, {}, IdleThreadPriority, virt_core, {}, ThreadType::Main, + system.GetCpuManager().GetGuestActivateFunc()); } Result KThread::InitializeIdleThread(Core::System& system, KThread* thread, s32 virt_core) { @@ -277,7 +288,7 @@ Result KThread::InitializeUserThread(Core::System& system, KThread* thread, KThr KProcess* owner) { system.Kernel().GlobalSchedulerContext().AddThread(thread); return InitializeThread(thread, func, arg, user_stack_top, prio, virt_core, owner, - ThreadType::User, system.GetCpuManager().GetGuestThreadStartFunc()); + ThreadType::User, system.GetCpuManager().GetGuestThreadFunc()); } void KThread::PostDestroy(uintptr_t arg) { @@ -1058,6 +1069,8 @@ void KThread::Exit() { // Register the thread as a work task. KWorkerTaskManager::AddTask(kernel, KWorkerTaskManager::WorkerType::Exit, this); } + + UNREACHABLE_MSG("KThread::Exit() would return"); } Result KThread::Sleep(s64 timeout) { @@ -1093,6 +1106,8 @@ void KThread::IfDummyThreadTryWait() { return; } + ASSERT(!kernel.IsPhantomModeForSingleCore()); + // Block until we are no longer waiting. std::unique_lock lk(dummy_wait_lock); dummy_wait_cv.wait( @@ -1197,16 +1212,13 @@ KScopedDisableDispatch::~KScopedDisableDispatch() { return; } - // Skip the reschedule if single-core, as dispatch tracking is disabled here. - if (!Settings::values.use_multi_core.GetValue()) { - return; - } - if (GetCurrentThread(kernel).GetDisableDispatchCount() <= 1) { - auto scheduler = kernel.CurrentScheduler(); + auto* scheduler = kernel.CurrentScheduler(); - if (scheduler) { + if (scheduler && !kernel.IsPhantomModeForSingleCore()) { scheduler->RescheduleCurrentCore(); + } else { + KScheduler::RescheduleCurrentHLEThread(kernel); } } else { GetCurrentThread(kernel).EnableDispatch(); diff --git a/src/core/hle/kernel/k_thread.h b/src/core/hle/kernel/k_thread.h index 28cd7ecb0..9ee20208e 100644 --- a/src/core/hle/kernel/k_thread.h +++ b/src/core/hle/kernel/k_thread.h @@ -413,6 +413,9 @@ public: [[nodiscard]] static Result InitializeDummyThread(KThread* thread); + [[nodiscard]] static Result InitializeMainThread(Core::System& system, KThread* thread, + s32 virt_core); + [[nodiscard]] static Result InitializeIdleThread(Core::System& system, KThread* thread, s32 virt_core); @@ -480,39 +483,16 @@ public: return per_core_priority_queue_entry[core]; } - [[nodiscard]] bool IsKernelThread() const { - return GetActiveCore() == 3; - } - - [[nodiscard]] bool IsDispatchTrackingDisabled() const { - return is_single_core || IsKernelThread(); - } - [[nodiscard]] s32 GetDisableDispatchCount() const { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return 1; - } - return this->GetStackParameters().disable_count; } void DisableDispatch() { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return; - } - ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() >= 0); this->GetStackParameters().disable_count++; } void EnableDispatch() { - if (IsDispatchTrackingDisabled()) { - // TODO(bunnei): Until kernel threads are emulated, we cannot enable/disable dispatch. - return; - } - ASSERT(GetCurrentThread(kernel).GetDisableDispatchCount() > 0); this->GetStackParameters().disable_count--; } diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index f23c629dc..f4072e1c3 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -64,8 +64,6 @@ struct KernelCore::Impl { is_phantom_mode_for_singlecore = false; - InitializePhysicalCores(); - // Derive the initial memory layout from the emulated board Init::InitializeSlabResourceCounts(kernel); DeriveInitialMemoryLayout(); @@ -75,9 +73,9 @@ struct KernelCore::Impl { InitializeSystemResourceLimit(kernel, system.CoreTiming()); InitializeMemoryLayout(); Init::InitializeKPageBufferSlabHeap(system); - InitializeSchedulers(); InitializeShutdownThreads(); InitializePreemption(kernel); + InitializePhysicalCores(); RegisterHostThread(); } @@ -136,7 +134,6 @@ struct KernelCore::Impl { shutdown_threads[core_id] = nullptr; } - schedulers[core_id]->Finalize(); schedulers[core_id].reset(); } @@ -199,14 +196,21 @@ struct KernelCore::Impl { exclusive_monitor = Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES); for (u32 i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { - schedulers[i] = std::make_unique<Kernel::KScheduler>(system, i); + const s32 core{static_cast<s32>(i)}; + + schedulers[i] = std::make_unique<Kernel::KScheduler>(system.Kernel()); cores.emplace_back(i, system, *schedulers[i], interrupts); - } - } - void InitializeSchedulers() { - for (u32 i = 0; i < Core::Hardware::NUM_CPU_CORES; i++) { - cores[i].Scheduler().Initialize(); + auto* main_thread{Kernel::KThread::Create(system.Kernel())}; + main_thread->SetName(fmt::format("MainThread:{}", core)); + main_thread->SetCurrentCore(core); + ASSERT(Kernel::KThread::InitializeMainThread(system, main_thread, core).IsSuccess()); + + auto* idle_thread{Kernel::KThread::Create(system.Kernel())}; + idle_thread->SetCurrentCore(core); + ASSERT(Kernel::KThread::InitializeIdleThread(system, idle_thread, core).IsSuccess()); + + schedulers[i]->Initialize(main_thread, idle_thread, core); } } @@ -1109,10 +1113,11 @@ void KernelCore::Suspend(bool suspended) { } void KernelCore::ShutdownCores() { + KScopedSchedulerLock lk{*this}; + for (auto* thread : impl->shutdown_threads) { void(thread->Run()); } - InterruptAllPhysicalCores(); } bool KernelCore::IsMulticore() const { diff --git a/src/core/hle/kernel/physical_core.cpp b/src/core/hle/kernel/physical_core.cpp index a5b16ae2e..6e7dacf97 100644 --- a/src/core/hle/kernel/physical_core.cpp +++ b/src/core/hle/kernel/physical_core.cpp @@ -43,6 +43,7 @@ void PhysicalCore::Initialize([[maybe_unused]] bool is_64_bit) { void PhysicalCore::Run() { arm_interface->Run(); + arm_interface->ClearExclusiveState(); } void PhysicalCore::Idle() { diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 8655506b0..27e5a805d 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -887,7 +887,7 @@ static Result GetInfo(Core::System& system, u64* result, u64 info_id, Handle han const auto* const current_thread = GetCurrentThreadPointer(system.Kernel()); const bool same_thread = current_thread == thread.GetPointerUnsafe(); - const u64 prev_ctx_ticks = scheduler.GetLastContextSwitchTicks(); + const u64 prev_ctx_ticks = scheduler.GetLastContextSwitchTime(); u64 out_ticks = 0; if (same_thread && info_sub_id == 0xFFFFFFFFFFFFFFFF) { const u64 thread_ticks = current_thread->GetCpuTime(); @@ -3026,11 +3026,6 @@ void Call(Core::System& system, u32 immediate) { } kernel.ExitSVCProfile(); - - if (!thread->IsCallingSvc()) { - auto* host_context = thread->GetHostContext().get(); - host_context->Rewind(); - } } } // namespace Kernel::Svc diff --git a/src/core/hle/result.h b/src/core/hle/result.h index aa9e5b89d..4de44cd06 100644 --- a/src/core/hle/result.h +++ b/src/core/hle/result.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/hle/service/am/applets/applet_software_keyboard.cpp b/src/core/hle/service/am/applets/applet_software_keyboard.cpp index faa092957..c18236045 100644 --- a/src/core/hle/service/am/applets/applet_software_keyboard.cpp +++ b/src/core/hle/service/am/applets/applet_software_keyboard.cpp @@ -536,6 +536,8 @@ void SoftwareKeyboard::InitializeFrontendNormalKeyboard() { .sub_text{std::move(sub_text)}, .guide_text{std::move(guide_text)}, .initial_text{initial_text}, + .left_optional_symbol_key{swkbd_config_common.left_optional_symbol_key}, + .right_optional_symbol_key{swkbd_config_common.right_optional_symbol_key}, .max_text_length{max_text_length}, .min_text_length{min_text_length}, .initial_cursor_position{initial_cursor_position}, @@ -591,6 +593,8 @@ void SoftwareKeyboard::InitializeFrontendInlineKeyboardOld() { .sub_text{}, .guide_text{}, .initial_text{current_text}, + .left_optional_symbol_key{appear_arg.left_optional_symbol_key}, + .right_optional_symbol_key{appear_arg.right_optional_symbol_key}, .max_text_length{max_text_length}, .min_text_length{min_text_length}, .initial_cursor_position{initial_cursor_position}, @@ -632,6 +636,8 @@ void SoftwareKeyboard::InitializeFrontendInlineKeyboardNew() { .sub_text{}, .guide_text{}, .initial_text{current_text}, + .left_optional_symbol_key{appear_arg.left_optional_symbol_key}, + .right_optional_symbol_key{appear_arg.right_optional_symbol_key}, .max_text_length{max_text_length}, .min_text_length{min_text_length}, .initial_cursor_position{initial_cursor_position}, diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 7055ea93e..2889973e4 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -18,8 +18,8 @@ namespace { } // Anonymous namespace -#include "core/network/network.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" namespace Service::NIFM { diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index 3e9dc4a13..c7194731e 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -13,8 +13,8 @@ #include "core/hle/kernel/k_thread.h" #include "core/hle/service/sockets/bsd.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index fed740d87..9ea36428d 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -16,7 +16,7 @@ class System; namespace Network { class Socket; -} +} // namespace Network namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.cpp b/src/core/hle/service/sockets/sockets_translate.cpp index 9c0936d97..2db10ec81 100644 --- a/src/core/hle/service/sockets/sockets_translate.cpp +++ b/src/core/hle/service/sockets/sockets_translate.cpp @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" #include "core/hle/service/sockets/sockets_translate.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/hle/service/sockets/sockets_translate.h b/src/core/hle/service/sockets/sockets_translate.h index 5e9809add..c93291d3e 100644 --- a/src/core/hle/service/sockets/sockets_translate.h +++ b/src/core/hle/service/sockets/sockets_translate.h @@ -7,7 +7,7 @@ #include "common/common_types.h" #include "core/hle/service/sockets/sockets.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" namespace Service::Sockets { diff --git a/src/core/network/network.cpp b/src/core/internal_network/network.cpp index fdafbea92..36c43cc8f 100644 --- a/src/core/network/network.cpp +++ b/src/core/internal_network/network.cpp @@ -29,9 +29,9 @@ #include "common/common_types.h" #include "common/logging/log.h" #include "common/settings.h" -#include "core/network/network.h" -#include "core/network/network_interface.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/network_interface.h" +#include "core/internal_network/sockets.h" namespace Network { diff --git a/src/core/network/network.h b/src/core/internal_network/network.h index 10e5ef10d..10e5ef10d 100644 --- a/src/core/network/network.h +++ b/src/core/internal_network/network.h diff --git a/src/core/network/network_interface.cpp b/src/core/internal_network/network_interface.cpp index 15ecc6abf..0f0a66160 100644 --- a/src/core/network/network_interface.cpp +++ b/src/core/internal_network/network_interface.cpp @@ -11,7 +11,7 @@ #include "common/logging/log.h" #include "common/settings.h" #include "common/string_util.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network_interface.h" #ifdef _WIN32 #include <iphlpapi.h> diff --git a/src/core/network/network_interface.h b/src/core/internal_network/network_interface.h index 9b98b6b42..9b98b6b42 100644 --- a/src/core/network/network_interface.h +++ b/src/core/internal_network/network_interface.h diff --git a/src/core/network/sockets.h b/src/core/internal_network/sockets.h index f889159f5..77e27e928 100644 --- a/src/core/network/sockets.h +++ b/src/core/internal_network/sockets.h @@ -3,6 +3,7 @@ #pragma once +#include <map> #include <memory> #include <utility> @@ -12,7 +13,7 @@ #endif #include "common/common_types.h" -#include "core/network/network.h" +#include "core/internal_network/network.h" // TODO: C++20 Replace std::vector usages with std::span diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 635449fce..1b44280b5 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <cstring> diff --git a/src/core/memory.h b/src/core/memory.h index 780c45385..2a21fbcfd 100644 --- a/src/core/memory.h +++ b/src/core/memory.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp index 6ef459b7a..f09c176f8 100644 --- a/src/core/perf_stats.cpp +++ b/src/core/perf_stats.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <chrono> diff --git a/src/core/perf_stats.h b/src/core/perf_stats.h index 816202588..dd6becc02 100644 --- a/src/core/perf_stats.h +++ b/src/core/perf_stats.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp index 654db0b52..abcf6eb11 100644 --- a/src/core/telemetry_session.cpp +++ b/src/core/telemetry_session.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> diff --git a/src/core/telemetry_session.h b/src/core/telemetry_session.h index 6f3d45bea..887dc98f3 100644 --- a/src/core/telemetry_session.h +++ b/src/core/telemetry_session.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 90dd629c6..4b91b88ce 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_library(input_common STATIC drivers/camera.cpp drivers/camera.h diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp index 00474ac77..de388ec4c 100644 --- a/src/input_common/drivers/sdl_driver.cpp +++ b/src/input_common/drivers/sdl_driver.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" #include "common/math_util.h" diff --git a/src/input_common/drivers/sdl_driver.h b/src/input_common/drivers/sdl_driver.h index 7dc7a93c7..fc3a44572 100644 --- a/src/input_common/drivers/sdl_driver.h +++ b/src/input_common/drivers/sdl_driver.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/input_common/drivers/tas_input.cpp b/src/input_common/drivers/tas_input.cpp index 66dbefe00..21c6ed405 100644 --- a/src/input_common/drivers/tas_input.cpp +++ b/src/input_common/drivers/tas_input.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later. +// SPDX-License-Identifier: GPL-2.0-or-later #include <cstring> #include <fmt/format.h> diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp index 825262a07..808b21069 100644 --- a/src/input_common/drivers/udp_client.cpp +++ b/src/input_common/drivers/udp_client.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <random> #include <boost/asio.hpp> diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h index dece2a45b..cea9f579a 100644 --- a/src/input_common/drivers/udp_client.h +++ b/src/input_common/drivers/udp_client.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/input_common/helpers/stick_from_buttons.cpp b/src/input_common/helpers/stick_from_buttons.cpp index 31e6f62ab..536d413a5 100644 --- a/src/input_common/helpers/stick_from_buttons.cpp +++ b/src/input_common/helpers/stick_from_buttons.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <chrono> #include <cmath> diff --git a/src/input_common/helpers/stick_from_buttons.h b/src/input_common/helpers/stick_from_buttons.h index 437ace4f7..e8d865743 100644 --- a/src/input_common/helpers/stick_from_buttons.h +++ b/src/input_common/helpers/stick_from_buttons.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/input_common/helpers/touch_from_buttons.cpp b/src/input_common/helpers/touch_from_buttons.cpp index f1b57d03a..da4a3dca5 100644 --- a/src/input_common/helpers/touch_from_buttons.cpp +++ b/src/input_common/helpers/touch_from_buttons.cpp @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include "common/settings.h" diff --git a/src/input_common/helpers/touch_from_buttons.h b/src/input_common/helpers/touch_from_buttons.h index 628f18215..c6cb3ab3c 100644 --- a/src/input_common/helpers/touch_from_buttons.h +++ b/src/input_common/helpers/touch_from_buttons.h @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/input_common/helpers/udp_protocol.cpp b/src/input_common/helpers/udp_protocol.cpp index cdeab7e11..994380d21 100644 --- a/src/input_common/helpers/udp_protocol.cpp +++ b/src/input_common/helpers/udp_protocol.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <cstddef> #include <cstring> diff --git a/src/input_common/helpers/udp_protocol.h b/src/input_common/helpers/udp_protocol.h index 597f51cd3..d9643ffe0 100644 --- a/src/input_common/helpers/udp_protocol.h +++ b/src/input_common/helpers/udp_protocol.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -85,7 +84,7 @@ enum RegisterFlags : u8 { struct Version {}; /** * Requests the server to send information about what controllers are plugged into the ports - * In citra's case, we only have one controller, so for simplicity's sake, we can just send a + * In yuzu's case, we only have one controller, so for simplicity's sake, we can just send a * request explicitly for the first controller port and leave it at that. In the future it would be * nice to make this configurable */ diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index ca1cb9542..75a57b9fc 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> #include "common/input.h" diff --git a/src/input_common/main.h b/src/input_common/main.h index b756bb5c6..9a969e747 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 000000000..312f79b68 --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2022 yuzu Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + +add_library(network STATIC + network.cpp + network.h + packet.cpp + packet.h + room.cpp + room.h + room_member.cpp + room_member.h + verify_user.cpp + verify_user.h +) + +create_target_directory_groups(network) + +target_link_libraries(network PRIVATE common enet Boost::boost) diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 000000000..0841e4134 --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/network.h" + +namespace Network { + +RoomNetwork::RoomNetwork() { + m_room = std::make_shared<Room>(); + m_room_member = std::make_shared<RoomMember>(); +} + +bool RoomNetwork::Init() { + if (enet_initialize() != 0) { + LOG_ERROR(Network, "Error initalizing ENet"); + return false; + } + m_room = std::make_shared<Room>(); + m_room_member = std::make_shared<RoomMember>(); + LOG_DEBUG(Network, "initialized OK"); + return true; +} + +std::weak_ptr<Room> RoomNetwork::GetRoom() { + return m_room; +} + +std::weak_ptr<RoomMember> RoomNetwork::GetRoomMember() { + return m_room_member; +} + +void RoomNetwork::Shutdown() { + if (m_room_member) { + if (m_room_member->IsConnected()) + m_room_member->Leave(); + m_room_member.reset(); + } + if (m_room) { + if (m_room->GetState() == Room::State::Open) + m_room->Destroy(); + m_room.reset(); + } + enet_deinitialize(); + LOG_DEBUG(Network, "shutdown OK"); +} + +} // namespace Network diff --git a/src/network/network.h b/src/network/network.h new file mode 100644 index 000000000..e4de207b2 --- /dev/null +++ b/src/network/network.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include "network/room.h" +#include "network/room_member.h" + +namespace Network { + +class RoomNetwork { +public: + RoomNetwork(); + + /// Initializes and registers the network device, the room, and the room member. + bool Init(); + + /// Returns a pointer to the room handle + std::weak_ptr<Room> GetRoom(); + + /// Returns a pointer to the room member handle + std::weak_ptr<RoomMember> GetRoomMember(); + + /// Unregisters the network device, the room, and the room member and shut them down. + void Shutdown(); + +private: + std::shared_ptr<RoomMember> m_room_member; ///< RoomMember (Client) for network games + std::shared_ptr<Room> m_room; ///< Room (Server) for network games +}; + +} // namespace Network diff --git a/src/network/packet.cpp b/src/network/packet.cpp new file mode 100644 index 000000000..0e22f1eb4 --- /dev/null +++ b/src/network/packet.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef _WIN32 +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include <cstring> +#include <string> +#include "network/packet.h" + +namespace Network { + +#ifndef htonll +static u64 htonll(u64 x) { + return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32)); +} +#endif + +#ifndef ntohll +static u64 ntohll(u64 x) { + return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32)); +} +#endif + +void Packet::Append(const void* in_data, std::size_t size_in_bytes) { + if (in_data && (size_in_bytes > 0)) { + std::size_t start = data.size(); + data.resize(start + size_in_bytes); + std::memcpy(&data[start], in_data, size_in_bytes); + } +} + +void Packet::Read(void* out_data, std::size_t size_in_bytes) { + if (out_data && CheckSize(size_in_bytes)) { + std::memcpy(out_data, &data[read_pos], size_in_bytes); + read_pos += size_in_bytes; + } +} + +void Packet::Clear() { + data.clear(); + read_pos = 0; + is_valid = true; +} + +const void* Packet::GetData() const { + return !data.empty() ? &data[0] : nullptr; +} + +void Packet::IgnoreBytes(u32 length) { + read_pos += length; +} + +std::size_t Packet::GetDataSize() const { + return data.size(); +} + +bool Packet::EndOfPacket() const { + return read_pos >= data.size(); +} + +Packet::operator bool() const { + return is_valid; +} + +Packet& Packet::Read(bool& out_data) { + u8 value{}; + if (Read(value)) { + out_data = (value != 0); + } + return *this; +} + +Packet& Packet::Read(s8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(u8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(s16& out_data) { + s16 value{}; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::Read(u16& out_data) { + u16 value{}; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::Read(s32& out_data) { + s32 value{}; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::Read(u32& out_data) { + u32 value{}; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::Read(s64& out_data) { + s64 value{}; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::Read(u64& out_data) { + u64 value{}; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::Read(float& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(double& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(char* out_data) { + // First extract string length + u32 length = 0; + Read(length); + + if ((length > 0) && CheckSize(length)) { + // Then extract characters + std::memcpy(out_data, &data[read_pos], length); + out_data[length] = '\0'; + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::Read(std::string& out_data) { + // First extract string length + u32 length = 0; + Read(length); + + out_data.clear(); + if ((length > 0) && CheckSize(length)) { + // Then extract characters + out_data.assign(&data[read_pos], length); + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::Write(bool in_data) { + Write(static_cast<u8>(in_data)); + return *this; +} + +Packet& Packet::Write(s8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(u8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(s16 in_data) { + s16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u16 in_data) { + u16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(s32 in_data) { + s32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u32 in_data) { + u32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(s64 in_data) { + s64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u64 in_data) { + u64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(float in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(double in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(const char* in_data) { + // First insert string length + u32 length = static_cast<u32>(std::strlen(in_data)); + Write(length); + + // Then insert characters + Append(in_data, length * sizeof(char)); + + return *this; +} + +Packet& Packet::Write(const std::string& in_data) { + // First insert string length + u32 length = static_cast<u32>(in_data.size()); + Write(length); + + // Then insert characters + if (length > 0) + Append(in_data.c_str(), length * sizeof(std::string::value_type)); + + return *this; +} + +bool Packet::CheckSize(std::size_t size) { + is_valid = is_valid && (read_pos + size <= data.size()); + + return is_valid; +} + +} // namespace Network diff --git a/src/network/packet.h b/src/network/packet.h new file mode 100644 index 000000000..e69217488 --- /dev/null +++ b/src/network/packet.h @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <vector> +#include "common/common_types.h" + +namespace Network { + +/// A class that serializes data for network transfer. It also handles endianess +class Packet { +public: + Packet() = default; + ~Packet() = default; + + /** + * Append data to the end of the packet + * @param data Pointer to the sequence of bytes to append + * @param size_in_bytes Number of bytes to append + */ + void Append(const void* data, std::size_t size_in_bytes); + + /** + * Reads data from the current read position of the packet + * @param out_data Pointer where the data should get written to + * @param size_in_bytes Number of bytes to read + */ + void Read(void* out_data, std::size_t size_in_bytes); + + /** + * Clear the packet + * After calling Clear, the packet is empty. + */ + void Clear(); + + /** + * Ignores bytes while reading + * @param length THe number of bytes to ignore + */ + void IgnoreBytes(u32 length); + + /** + * Get a pointer to the data contained in the packet + * @return Pointer to the data + */ + const void* GetData() const; + + /** + * This function returns the number of bytes pointed to by + * what getData returns. + * @return Data size, in bytes + */ + std::size_t GetDataSize() const; + + /** + * This function is useful to know if there is some data + * left to be read, without actually reading it. + * @return True if all data was read, false otherwise + */ + bool EndOfPacket() const; + + explicit operator bool() const; + + /// Overloads of read function to read data from the packet + Packet& Read(bool& out_data); + Packet& Read(s8& out_data); + Packet& Read(u8& out_data); + Packet& Read(s16& out_data); + Packet& Read(u16& out_data); + Packet& Read(s32& out_data); + Packet& Read(u32& out_data); + Packet& Read(s64& out_data); + Packet& Read(u64& out_data); + Packet& Read(float& out_data); + Packet& Read(double& out_data); + Packet& Read(char* out_data); + Packet& Read(std::string& out_data); + template <typename T> + Packet& Read(std::vector<T>& out_data); + template <typename T, std::size_t S> + Packet& Read(std::array<T, S>& out_data); + + /// Overloads of write function to write data into the packet + Packet& Write(bool in_data); + Packet& Write(s8 in_data); + Packet& Write(u8 in_data); + Packet& Write(s16 in_data); + Packet& Write(u16 in_data); + Packet& Write(s32 in_data); + Packet& Write(u32 in_data); + Packet& Write(s64 in_data); + Packet& Write(u64 in_data); + Packet& Write(float in_data); + Packet& Write(double in_data); + Packet& Write(const char* in_data); + Packet& Write(const std::string& in_data); + template <typename T> + Packet& Write(const std::vector<T>& in_data); + template <typename T, std::size_t S> + Packet& Write(const std::array<T, S>& data); + +private: + /** + * Check if the packet can extract a given number of bytes + * This function updates accordingly the state of the packet. + * @param size Size to check + * @return True if size bytes can be read from the packet + */ + bool CheckSize(std::size_t size); + + // Member data + std::vector<char> data; ///< Data stored in the packet + std::size_t read_pos = 0; ///< Current reading position in the packet + bool is_valid = true; ///< Reading state of the packet +}; + +template <typename T> +Packet& Packet::Read(std::vector<T>& out_data) { + // First extract the size + u32 size = 0; + Read(size); + out_data.resize(size); + + // Then extract the data + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + Read(character); + out_data[i] = character; + } + return *this; +} + +template <typename T, std::size_t S> +Packet& Packet::Read(std::array<T, S>& out_data) { + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + Read(character); + out_data[i] = character; + } + return *this; +} + +template <typename T> +Packet& Packet::Write(const std::vector<T>& in_data) { + // First insert the size + Write(static_cast<u32>(in_data.size())); + + // Then insert the data + for (std::size_t i = 0; i < in_data.size(); ++i) { + Write(in_data[i]); + } + return *this; +} + +template <typename T, std::size_t S> +Packet& Packet::Write(const std::array<T, S>& in_data) { + for (std::size_t i = 0; i < in_data.size(); ++i) { + Write(in_data[i]); + } + return *this; +} + +} // namespace Network diff --git a/src/network/room.cpp b/src/network/room.cpp new file mode 100644 index 000000000..3fc3a0383 --- /dev/null +++ b/src/network/room.cpp @@ -0,0 +1,1110 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <algorithm> +#include <atomic> +#include <iomanip> +#include <mutex> +#include <random> +#include <regex> +#include <shared_mutex> +#include <sstream> +#include <thread> +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room.h" +#include "network/verify_user.h" + +namespace Network { + +class Room::RoomImpl { +public: + // This MAC address is used to generate a 'Nintendo' like Mac address. + const MacAddress NintendoOUI; + std::mt19937 random_gen; ///< Random number generator. Used for GenerateMacAddress + + ENetHost* server = nullptr; ///< Network interface. + + std::atomic<State> state{State::Closed}; ///< Current state of the room. + RoomInformation room_information; ///< Information about this room. + + std::string verify_uid; ///< A GUID which may be used for verfication. + mutable std::mutex verify_uid_mutex; ///< Mutex for verify_uid + + std::string password; ///< The password required to connect to this room. + + struct Member { + std::string nickname; ///< The nickname of the member. + std::string console_id_hash; ///< A hash of the console ID of the member. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. + /// Data of the user, often including authenticated forum username. + VerifyUser::UserData user_data; + ENetPeer* peer; ///< The remote peer. + }; + using MemberList = std::vector<Member>; + MemberList members; ///< Information about the members of this room + mutable std::shared_mutex member_mutex; ///< Mutex for locking the members list + + UsernameBanList username_ban_list; ///< List of banned usernames + IPBanList ip_ban_list; ///< List of banned IP addresses + mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists + + RoomImpl() + : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {} + + /// Thread that receives and dispatches network packets + std::unique_ptr<std::thread> room_thread; + + /// Verification backend of the room + std::unique_ptr<VerifyUser::Backend> verify_backend; + + /// Thread function that will receive and dispatch messages until the room is destroyed. + void ServerLoop(); + void StartLoop(); + + /** + * Parses and answers a room join request from a client. + * Validates the uniqueness of the username and assigns the MAC address + * that the client will use for the remainder of the connection. + */ + void HandleJoinRequest(const ENetEvent* event); + + /** + * Parses and answers a kick request from a client. + * Validates the permissions and that the given user exists and then kicks the member. + */ + void HandleModKickPacket(const ENetEvent* event); + + /** + * Parses and answers a ban request from a client. + * Validates the permissions and bans the user (by forum username or IP). + */ + void HandleModBanPacket(const ENetEvent* event); + + /** + * Parses and answers a unban request from a client. + * Validates the permissions and unbans the address. + */ + void HandleModUnbanPacket(const ENetEvent* event); + + /** + * Parses and answers a get ban list request from a client. + * Validates the permissions and returns the ban list. + */ + void HandleModGetBanListPacket(const ENetEvent* event); + + /** + * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room. + */ + bool IsValidNickname(const std::string& nickname) const; + + /** + * Returns whether the MAC address is valid, ie. isn't already taken by someone else in the + * room. + */ + bool IsValidMacAddress(const MacAddress& address) const; + + /** + * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in + * the room. + */ + bool IsValidConsoleId(const std::string& console_id_hash) const; + + /** + * Returns whether a user has mod permissions. + */ + bool HasModPermission(const ENetPeer* client) const; + + /** + * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. + */ + void SendRoomIsFull(ENetPeer* client); + + /** + * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. + */ + void SendNameCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_MAC_COLLISION message telling the client that the MAC is invalid. + */ + void SendMacCollision(ENetPeer* client); + + /** + * Sends a IdConsoleIdCollison message telling the client that another member with the same + * console ID exists. + */ + void SendConsoleIdCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid. + */ + void SendVersionMismatch(ENetPeer* client); + + /** + * Sends a ID_ROOM_WRONG_PASSWORD message telling the client that the password is wrong. + */ + void SendWrongPassword(ENetPeer* client); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room. + */ + void SendJoinSuccess(ENetPeer* client, MacAddress mac_address); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room, and it has been granted mod permissions. + */ + void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address); + + /** + * Sends a IdHostKicked message telling the client that they have been kicked. + */ + void SendUserKicked(ENetPeer* client); + + /** + * Sends a IdHostBanned message telling the client that they have been banned. + */ + void SendUserBanned(ENetPeer* client); + + /** + * Sends a IdModPermissionDenied message telling the client that they do not have mod + * permission. + */ + void SendModPermissionDenied(ENetPeer* client); + + /** + * Sends a IdModNoSuchUser message telling the client that the given user could not be found. + */ + void SendModNoSuchUser(ENetPeer* client); + + /** + * Sends the ban list in response to a client's request for getting ban list. + */ + void SendModBanListResponse(ENetPeer* client); + + /** + * Notifies the members that the room is closed, + */ + void SendCloseMessage(); + + /** + * Sends a system message to all the connected clients. + */ + void SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip); + + /** + * Sends the information about the room, along with the list of members + * to every connected client in the room. + * The packet has the structure: + * <MessageID>ID_ROOM_INFORMATION + * <String> room_name + * <String> room_description + * <u32> member_slots: The max number of clients allowed in this room + * <String> uid + * <u16> port + * <u32> num_members: the number of currently joined clients + * This is followed by the following three values for each member: + * <String> nickname of that member + * <MacAddress> mac_address of that member + * <String> game_name of that member + */ + void BroadcastRoomInformation(); + + /** + * Generates a free MAC address to assign to a new client. + * The first 3 bytes are the NintendoOUI 0x00, 0x1F, 0x32 + */ + MacAddress GenerateMacAddress(); + + /** + * Broadcasts this packet to all members except the sender. + * @param event The ENet event containing the data + */ + void HandleWifiPacket(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts the game name from a received ENet packet and broadcasts it. + * @param event The ENet event that was received. + */ + void HandleGameNamePacket(const ENetEvent* event); + + /** + * Removes the client from the members list if it was in it and announces the change + * to all other clients. + */ + void HandleClientDisconnection(ENetPeer* client); +}; + +// RoomImpl +void Room::RoomImpl::ServerLoop() { + while (state != State::Closed) { + ENetEvent event; + if (enet_host_service(server, &event, 16) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdJoinRequest: + HandleJoinRequest(&event); + break; + case IdSetGameInfo: + HandleGameNamePacket(&event); + break; + case IdWifiPacket: + HandleWifiPacket(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + // Moderation + case IdModKick: + HandleModKickPacket(&event); + break; + case IdModBan: + HandleModBanPacket(&event); + break; + case IdModUnban: + HandleModUnbanPacket(&event); + break; + case IdModGetBanList: + HandleModGetBanListPacket(&event); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + HandleClientDisconnection(event.peer); + break; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + } + // Close the connection to all members: + SendCloseMessage(); +} + +void Room::RoomImpl::StartLoop() { + room_thread = std::make_unique<std::thread>(&Room::RoomImpl::ServerLoop, this); +} + +void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { + { + std::lock_guard lock(member_mutex); + if (members.size() >= room_information.member_slots) { + SendRoomIsFull(event->peer); + return; + } + } + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string nickname; + packet.Read(nickname); + + std::string console_id_hash; + packet.Read(console_id_hash); + + MacAddress preferred_mac; + packet.Read(preferred_mac); + + u32 client_version; + packet.Read(client_version); + + std::string pass; + packet.Read(pass); + + std::string token; + packet.Read(token); + + if (pass != password) { + SendWrongPassword(event->peer); + return; + } + + if (!IsValidNickname(nickname)) { + SendNameCollision(event->peer); + return; + } + + if (preferred_mac != NoPreferredMac) { + // Verify if the preferred mac is available + if (!IsValidMacAddress(preferred_mac)) { + SendMacCollision(event->peer); + return; + } + } else { + // Assign a MAC address of this client automatically + preferred_mac = GenerateMacAddress(); + } + + if (!IsValidConsoleId(console_id_hash)) { + SendConsoleIdCollision(event->peer); + return; + } + + if (client_version != network_version) { + SendVersionMismatch(event->peer); + return; + } + + // At this point the client is ready to be added to the room. + Member member{}; + member.mac_address = preferred_mac; + member.console_id_hash = console_id_hash; + member.nickname = nickname; + member.peer = event->peer; + + std::string uid; + { + std::lock_guard lock(verify_uid_mutex); + uid = verify_uid; + } + member.user_data = verify_backend->LoadUserData(uid, token); + + std::string ip; + { + std::lock_guard lock(ban_list_mutex); + + // Check username ban + if (!member.user_data.username.empty() && + std::find(username_ban_list.begin(), username_ban_list.end(), + member.user_data.username) != username_ban_list.end()) { + + SendUserBanned(event->peer); + return; + } + + // Check IP ban + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&event->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) { + SendUserBanned(event->peer); + return; + } + } + + // Notify everyone that the user has joined. + SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username, ip); + + { + std::lock_guard lock(member_mutex); + members.push_back(std::move(member)); + } + + // Notify everyone that the room information has changed. + BroadcastRoomInformation(); + if (HasModPermission(event->peer)) { + SendJoinSuccessAsMod(event->peer, preferred_mac); + } else { + SendJoinSuccess(event->peer, preferred_mac); + } +} + +void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet.Read(nickname); + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the kicked member + SendUserKicked(target_member->peer); + + username = target_member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberKicked, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet.Read(nickname); + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the banned member + SendUserBanned(target_member->peer); + + nickname = target_member->nickname; + username = target_member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + { + std::lock_guard lock(ban_list_mutex); + + if (!username.empty()) { + // Ban the forum username + if (std::find(username_ban_list.begin(), username_ban_list.end(), username) == + username_ban_list.end()) { + + username_ban_list.emplace_back(username); + } + } + + // Ban the member's IP as well + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) { + ip_ban_list.emplace_back(ip); + } + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberBanned, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string address; + packet.Read(address); + + bool unbanned = false; + { + std::lock_guard lock(ban_list_mutex); + + auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address); + if (it != username_ban_list.end()) { + unbanned = true; + username_ban_list.erase(it); + } + + it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address); + if (it != ip_ban_list.end()) { + unbanned = true; + ip_ban_list.erase(it); + } + } + + if (unbanned) { + SendStatusMessage(IdAddressUnbanned, address, "", ""); + } else { + SendModNoSuchUser(event->peer); + } +} + +void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + SendModBanListResponse(event->peer); +} + +bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const { + // A nickname is valid if it matches the regex and is not already taken by anybody else in the + // room. + const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$"); + if (!std::regex_match(nickname, nickname_regex)) + return false; + + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname != nickname; }); +} + +bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const { + // A MAC address is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&address](const auto& member) { return member.mac_address != address; }); +} + +bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const { + // A Console ID is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) { + return member.console_id_hash != console_id_hash; + }); +} + +bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { + std::lock_guard lock(member_mutex); + const auto sending_member = + std::find_if(members.begin(), members.end(), + [client](const auto& member) { return member.peer == client; }); + if (sending_member == members.end()) { + return false; + } + if (room_information.enable_yuzu_mods && + sending_member->user_data.moderator) { // Community moderator + + return true; + } + if (!room_information.host_username.empty() && + sending_member->user_data.username == room_information.host_username) { // Room host + + return true; + } + return false; +} + +void Room::RoomImpl::SendNameCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdNameCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendMacCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdMacCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdConsoleIdCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdWrongPassword)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdRoomIsFull)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdVersionMismatch)); + packet.Write(network_version); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinSuccess)); + packet.Write(mac_address); + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinSuccessAsMod)); + packet.Write(mac_address); + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserKicked(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdHostKicked)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserBanned(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdHostBanned)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModPermissionDenied)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModNoSuchUser)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModBanListResponse)); + { + std::lock_guard lock(ban_list_mutex); + packet.Write(username_ban_list); + packet.Write(ip_ban_list); + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendCloseMessage() { + Packet packet; + packet.Write(static_cast<u8>(IdCloseRoom)); + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + for (auto& member : members) { + enet_peer_disconnect(member.peer, 0); + } +} + +void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip) { + Packet packet; + packet.Write(static_cast<u8>(IdStatusMessage)); + packet.Write(static_cast<u8>(type)); + packet.Write(nickname); + packet.Write(username); + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + + const std::string display_name = + username.empty() ? nickname : fmt::format("{} ({})", nickname, username); + + switch (type) { + case IdMemberJoin: + LOG_INFO(Network, "[{}] {} has joined.", ip, display_name); + break; + case IdMemberLeave: + LOG_INFO(Network, "[{}] {} has left.", ip, display_name); + break; + case IdMemberKicked: + LOG_INFO(Network, "[{}] {} has been kicked.", ip, display_name); + break; + case IdMemberBanned: + LOG_INFO(Network, "[{}] {} has been banned.", ip, display_name); + break; + case IdAddressUnbanned: + LOG_INFO(Network, "{} has been unbanned.", display_name); + break; + } +} + +void Room::RoomImpl::BroadcastRoomInformation() { + Packet packet; + packet.Write(static_cast<u8>(IdRoomInformation)); + packet.Write(room_information.name); + packet.Write(room_information.description); + packet.Write(room_information.member_slots); + packet.Write(room_information.port); + packet.Write(room_information.preferred_game.name); + packet.Write(room_information.host_username); + + packet.Write(static_cast<u32>(members.size())); + { + std::lock_guard lock(member_mutex); + for (const auto& member : members) { + packet.Write(member.nickname); + packet.Write(member.mac_address); + packet.Write(member.game_info.name); + packet.Write(member.game_info.id); + packet.Write(member.user_data.username); + packet.Write(member.user_data.display_name); + packet.Write(member.user_data.avatar_url); + } + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(server, 0, enet_packet); + enet_host_flush(server); +} + +MacAddress Room::RoomImpl::GenerateMacAddress() { + MacAddress result_mac = + NintendoOUI; // The first three bytes of each MAC address will be the NintendoOUI + std::uniform_int_distribution<> dis(0x00, 0xFF); // Random byte between 0 and 0xFF + do { + for (std::size_t i = 3; i < result_mac.size(); ++i) { + result_mac[i] = dis(random_gen); + } + } while (!IsValidMacAddress(result_mac)); + return result_mac; +} + +void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + in_packet.IgnoreBytes(sizeof(u8)); // Message type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Channel + in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address + MacAddress destination_address; + in_packet.Read(destination_address); + + Packet out_packet; + out_packet.Append(event->packet->data, event->packet->dataLength); + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + + if (destination_address == BroadcastMac) { // Send the data to everyone except the sender + std::lock_guard lock(member_mutex); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + } else { // Send the data only to the destination client + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), + [destination_address](const Member& member_entry) -> bool { + return member_entry.mac_address == destination_address; + }); + if (member != members.end()) { + enet_peer_send(member->peer, 0, enet_packet); + } else { + LOG_ERROR(Network, + "Attempting to send to unknown MAC address: " + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + destination_address[0], destination_address[1], destination_address[2], + destination_address[3], destination_address[4], destination_address[5]); + enet_packet_destroy(enet_packet); + } + } + enet_host_flush(server); +} + +void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string message; + in_packet.Read(message); + auto CompareNetworkAddress = [event](const Member member) -> bool { + return member.peer == event->peer; + }; + + std::lock_guard lock(member_mutex); + const auto sending_member = std::find_if(members.begin(), members.end(), CompareNetworkAddress); + if (sending_member == members.end()) { + return; // Received a chat message from a unknown sender + } + + // Limit the size of chat messages to MaxMessageSize + message.resize(std::min(static_cast<u32>(message.size()), MaxMessageSize)); + + Packet out_packet; + out_packet.Write(static_cast<u8>(IdChatMessage)); + out_packet.Write(sending_member->nickname); + out_packet.Write(sending_member->user_data.username); + out_packet.Write(message); + + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + + enet_host_flush(server); + + if (sending_member->user_data.username.empty()) { + LOG_INFO(Network, "{}: {}", sending_member->nickname, message); + } else { + LOG_INFO(Network, "{} ({}): {}", sending_member->nickname, + sending_member->user_data.username, message); + } +} + +void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + GameInfo game_info; + in_packet.Read(game_info.name); + in_packet.Read(game_info.id); + + { + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), + [event](const Member& member_entry) -> bool { + return member_entry.peer == event->peer; + }); + if (member != members.end()) { + member->game_info = game_info; + + const std::string display_name = + member->user_data.username.empty() + ? member->nickname + : fmt::format("{} ({})", member->nickname, member->user_data.username); + + if (game_info.name.empty()) { + LOG_INFO(Network, "{} is not playing", display_name); + } else { + LOG_INFO(Network, "{} is playing {}", display_name, game_info.name); + } + } + } + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { + // Remove the client from the members list. + std::string nickname, username, ip; + { + std::lock_guard lock(member_mutex); + auto member = + std::find_if(members.begin(), members.end(), [client](const Member& member_entry) { + return member_entry.peer == client; + }); + if (member != members.end()) { + nickname = member->nickname; + username = member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + members.erase(member); + } + } + + // Announce the change to all clients. + enet_peer_disconnect(client, 0); + if (!nickname.empty()) + SendStatusMessage(IdMemberLeave, nickname, username, ip); + BroadcastRoomInformation(); +} + +// Room +Room::Room() : room_impl{std::make_unique<RoomImpl>()} {} + +Room::~Room() = default; + +bool Room::Create(const std::string& name, const std::string& description, + const std::string& server_address, u16 server_port, const std::string& password, + const u32 max_connections, const std::string& host_username, + const GameInfo preferred_game, + std::unique_ptr<VerifyUser::Backend> verify_backend, + const Room::BanList& ban_list, bool enable_yuzu_mods) { + ENetAddress address; + address.host = ENET_HOST_ANY; + if (!server_address.empty()) { + enet_address_set_host(&address, server_address.c_str()); + } + address.port = server_port; + + // In order to send the room is full message to the connecting client, we need to leave one + // slot open so enet won't reject the incoming connection without telling us + room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); + if (!room_impl->server) { + return false; + } + room_impl->state = State::Open; + + room_impl->room_information.name = name; + room_impl->room_information.description = description; + room_impl->room_information.member_slots = max_connections; + room_impl->room_information.port = server_port; + room_impl->room_information.preferred_game = preferred_game; + room_impl->room_information.host_username = host_username; + room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods; + room_impl->password = password; + room_impl->verify_backend = std::move(verify_backend); + room_impl->username_ban_list = ban_list.first; + room_impl->ip_ban_list = ban_list.second; + + room_impl->StartLoop(); + return true; +} + +Room::State Room::GetState() const { + return room_impl->state; +} + +const RoomInformation& Room::GetRoomInformation() const { + return room_impl->room_information; +} + +std::string Room::GetVerifyUID() const { + std::lock_guard lock(room_impl->verify_uid_mutex); + return room_impl->verify_uid; +} + +Room::BanList Room::GetBanList() const { + std::lock_guard lock(room_impl->ban_list_mutex); + return {room_impl->username_ban_list, room_impl->ip_ban_list}; +} + +std::vector<Member> Room::GetRoomMemberList() const { + std::vector<Member> member_list; + std::lock_guard lock(room_impl->member_mutex); + for (const auto& member_impl : room_impl->members) { + Member member; + member.nickname = member_impl.nickname; + member.username = member_impl.user_data.username; + member.display_name = member_impl.user_data.display_name; + member.avatar_url = member_impl.user_data.avatar_url; + member.mac_address = member_impl.mac_address; + member.game = member_impl.game_info; + member_list.push_back(member); + } + return member_list; +} + +bool Room::HasPassword() const { + return !room_impl->password.empty(); +} + +void Room::SetVerifyUID(const std::string& uid) { + std::lock_guard lock(room_impl->verify_uid_mutex); + room_impl->verify_uid = uid; +} + +void Room::Destroy() { + room_impl->state = State::Closed; + room_impl->room_thread->join(); + room_impl->room_thread.reset(); + + if (room_impl->server) { + enet_host_destroy(room_impl->server); + } + room_impl->room_information = {}; + room_impl->server = nullptr; + { + std::lock_guard lock(room_impl->member_mutex); + room_impl->members.clear(); + } + room_impl->room_information.member_slots = 0; + room_impl->room_information.name.clear(); +} + +} // namespace Network diff --git a/src/network/room.h b/src/network/room.h new file mode 100644 index 000000000..6f7e3b5b5 --- /dev/null +++ b/src/network/room.h @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <memory> +#include <string> +#include <vector> +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "network/verify_user.h" + +namespace Network { + +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::MacAddress; +using AnnounceMultiplayerRoom::Member; +using AnnounceMultiplayerRoom::RoomInformation; + +constexpr u32 network_version = 1; ///< The version of this Room and RoomMember + +constexpr u16 DefaultRoomPort = 24872; + +constexpr u32 MaxMessageSize = 500; + +/// Maximum number of concurrent connections allowed to this room. +static constexpr u32 MaxConcurrentConnections = 254; + +constexpr std::size_t NumChannels = 1; // Number of channels used for the connection + +/// A special MAC address that tells the room we're joining to assign us a MAC address +/// automatically. +constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// 802.11 broadcast MAC address +constexpr MacAddress BroadcastMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// The different types of messages that can be sent. The first byte of each packet defines the type +enum RoomMessageTypes : u8 { + IdJoinRequest = 1, + IdJoinSuccess, + IdRoomInformation, + IdSetGameInfo, + IdWifiPacket, + IdChatMessage, + IdNameCollision, + IdMacCollision, + IdVersionMismatch, + IdWrongPassword, + IdCloseRoom, + IdRoomIsFull, + IdConsoleIdCollision, + IdStatusMessage, + IdHostKicked, + IdHostBanned, + /// Moderation requests + IdModKick, + IdModBan, + IdModUnban, + IdModGetBanList, + // Moderation responses + IdModBanListResponse, + IdModPermissionDenied, + IdModNoSuchUser, + IdJoinSuccessAsMod, +}; + +/// Types of system status messages +enum StatusMessageTypes : u8 { + IdMemberJoin = 1, ///< Member joining + IdMemberLeave, ///< Member leaving + IdMemberKicked, ///< A member is kicked from the room + IdMemberBanned, ///< A member is banned from the room + IdAddressUnbanned, ///< A username / ip address is unbanned from the room +}; + +/// This is what a server [person creating a server] would use. +class Room final { +public: + enum class State : u8 { + Open, ///< The room is open and ready to accept connections. + Closed, ///< The room is not opened and can not accept connections. + }; + + Room(); + ~Room(); + + /** + * Gets the current state of the room. + */ + State GetState() const; + + /** + * Gets the room information of the room. + */ + const RoomInformation& GetRoomInformation() const; + + /** + * Gets the verify UID of this room. + */ + std::string GetVerifyUID() const; + + /** + * Gets a list of the mbmers connected to the room. + */ + std::vector<Member> GetRoomMemberList() const; + + /** + * Checks if the room is password protected + */ + bool HasPassword() const; + + using UsernameBanList = std::vector<std::string>; + using IPBanList = std::vector<std::string>; + + using BanList = std::pair<UsernameBanList, IPBanList>; + + /** + * Creates the socket for this room. Will bind to default address if + * server is empty string. + */ + bool Create(const std::string& name, const std::string& description = "", + const std::string& server = "", u16 server_port = DefaultRoomPort, + const std::string& password = "", + const u32 max_connections = MaxConcurrentConnections, + const std::string& host_username = "", const GameInfo = {}, + std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr, + const BanList& ban_list = {}, bool enable_yuzu_mods = false); + + /** + * Sets the verification GUID of the room. + */ + void SetVerifyUID(const std::string& uid); + + /** + * Gets the ban list (including banned forum usernames and IPs) of the room. + */ + BanList GetBanList() const; + + /** + * Destroys the socket + */ + void Destroy(); + +private: + class RoomImpl; + std::unique_ptr<RoomImpl> room_impl; +}; + +} // namespace Network diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp new file mode 100644 index 000000000..e4f823e98 --- /dev/null +++ b/src/network/room_member.cpp @@ -0,0 +1,696 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <atomic> +#include <list> +#include <mutex> +#include <set> +#include <thread> +#include "common/assert.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room_member.h" + +namespace Network { + +constexpr u32 ConnectionTimeoutMs = 5000; + +class RoomMember::RoomMemberImpl { +public: + ENetHost* client = nullptr; ///< ENet network interface. + ENetPeer* server = nullptr; ///< The server peer the client is connected to + + /// Information about the clients connected to the same room as us. + MemberList member_information; + /// Information about the room we're connected to. + RoomInformation room_information; + + /// The current game name, id and version + GameInfo current_game_info; + + std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember. + void SetState(const State new_state); + void SetError(const Error new_error); + bool IsConnected() const; + + std::string nickname; ///< The nickname of this member. + + std::string username; ///< The username of this member. + mutable std::mutex username_mutex; ///< Mutex for locking username. + + MacAddress mac_address; ///< The mac_address of this member. + + std::mutex network_mutex; ///< Mutex that controls access to the `client` variable. + /// Thread that receives and dispatches network packets + std::unique_ptr<std::thread> loop_thread; + std::mutex send_list_mutex; ///< Mutex that controls access to the `send_list` variable. + std::list<Packet> send_list; ///< A list that stores all packets to send the async + + template <typename T> + using CallbackSet = std::set<CallbackHandle<T>>; + std::mutex callback_mutex; ///< The mutex used for handling callbacks + + class Callbacks { + public: + template <typename T> + CallbackSet<T>& Get(); + + private: + CallbackSet<WifiPacket> callback_set_wifi_packet; + CallbackSet<ChatEntry> callback_set_chat_messages; + CallbackSet<StatusMessageEntry> callback_set_status_messages; + CallbackSet<RoomInformation> callback_set_room_information; + CallbackSet<State> callback_set_state; + CallbackSet<Error> callback_set_error; + CallbackSet<Room::BanList> callback_set_ban_list; + }; + Callbacks callbacks; ///< All CallbackSets to all events + + void MemberLoop(); + + void StartLoop(); + + /** + * Sends data to the room. It will be send on channel 0 with flag RELIABLE + * @param packet The data to send + */ + void Send(Packet&& packet); + + /** + * Sends a request to the server, asking for permission to join a room with the specified + * nickname and preferred mac. + * @params nickname The desired nickname. + * @params console_id_hash A hash of the Console ID. + * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells + * @params password The password for the room + * the server to assign one for us. + */ + void SendJoinRequest(const std::string& nickname_, const std::string& console_id_hash, + const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Extracts a MAC Address from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleJoinPacket(const ENetEvent* event); + /** + * Extracts RoomInformation and MemberInformation from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleRoomInformationPacket(const ENetEvent* event); + + /** + * Extracts a WifiPacket from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleWifiPackets(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts a system message entry from a received ENet packet and adds it to the system message + * queue. + * @param event The ENet event that was received. + */ + void HandleStatusMessagePacket(const ENetEvent* event); + + /** + * Extracts a ban list request response from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleModBanListResponsePacket(const ENetEvent* event); + + /** + * Disconnects the RoomMember from the Room + */ + void Disconnect(); + + template <typename T> + void Invoke(const T& data); + + template <typename T> + CallbackHandle<T> Bind(std::function<void(const T&)> callback); +}; + +// RoomMemberImpl +void RoomMember::RoomMemberImpl::SetState(const State new_state) { + if (state != new_state) { + state = new_state; + Invoke<State>(state); + } +} + +void RoomMember::RoomMemberImpl::SetError(const Error new_error) { + Invoke<Error>(new_error); +} + +bool RoomMember::RoomMemberImpl::IsConnected() const { + return state == State::Joining || state == State::Joined || state == State::Moderator; +} + +void RoomMember::RoomMemberImpl::MemberLoop() { + // Receive packets while the connection is open + while (IsConnected()) { + std::lock_guard lock(network_mutex); + ENetEvent event; + if (enet_host_service(client, &event, 16) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdWifiPacket: + HandleWifiPackets(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + case IdStatusMessage: + HandleStatusMessagePacket(&event); + break; + case IdRoomInformation: + HandleRoomInformationPacket(&event); + break; + case IdJoinSuccess: + case IdJoinSuccessAsMod: + // The join request was successful, we are now in the room. + // If we joined successfully, there must be at least one client in the room: us. + ASSERT_MSG(member_information.size() > 0, + "We have not yet received member information."); + HandleJoinPacket(&event); // Get the MAC Address for the client + if (event.packet->data[0] == IdJoinSuccessAsMod) { + SetState(State::Moderator); + } else { + SetState(State::Joined); + } + break; + case IdModBanListResponse: + HandleModBanListResponsePacket(&event); + break; + case IdRoomIsFull: + SetState(State::Idle); + SetError(Error::RoomIsFull); + break; + case IdNameCollision: + SetState(State::Idle); + SetError(Error::NameCollision); + break; + case IdMacCollision: + SetState(State::Idle); + SetError(Error::MacCollision); + break; + case IdConsoleIdCollision: + SetState(State::Idle); + SetError(Error::ConsoleIdCollision); + break; + case IdVersionMismatch: + SetState(State::Idle); + SetError(Error::WrongVersion); + break; + case IdWrongPassword: + SetState(State::Idle); + SetError(Error::WrongPassword); + break; + case IdCloseRoom: + SetState(State::Idle); + SetError(Error::LostConnection); + break; + case IdHostKicked: + SetState(State::Idle); + SetError(Error::HostKicked); + break; + case IdHostBanned: + SetState(State::Idle); + SetError(Error::HostBanned); + break; + case IdModPermissionDenied: + SetError(Error::PermissionDenied); + break; + case IdModNoSuchUser: + SetError(Error::NoSuchUser); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + if (state == State::Joined || state == State::Moderator) { + SetState(State::Idle); + SetError(Error::LostConnection); + } + break; + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're + // already connected + ASSERT_MSG(false, "Received unexpected connect event while already connected"); + break; + } + } + std::list<Packet> packets; + { + std::lock_guard send_lock(send_list_mutex); + packets.swap(send_list); + } + for (const auto& packet : packets) { + ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(server, 0, enetPacket); + } + enet_host_flush(client); + } + Disconnect(); +}; + +void RoomMember::RoomMemberImpl::StartLoop() { + loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this); +} + +void RoomMember::RoomMemberImpl::Send(Packet&& packet) { + std::lock_guard lock(send_list_mutex); + send_list.push_back(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_, + const std::string& console_id_hash, + const MacAddress& preferred_mac, + const std::string& password, + const std::string& token) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinRequest)); + packet.Write(nickname_); + packet.Write(console_id_hash); + packet.Write(preferred_mac); + packet.Write(network_version); + packet.Write(password); + packet.Write(token); + Send(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + RoomInformation info{}; + packet.Read(info.name); + packet.Read(info.description); + packet.Read(info.member_slots); + packet.Read(info.port); + packet.Read(info.preferred_game.name); + packet.Read(info.host_username); + room_information.name = info.name; + room_information.description = info.description; + room_information.member_slots = info.member_slots; + room_information.port = info.port; + room_information.preferred_game = info.preferred_game; + room_information.host_username = info.host_username; + + u32 num_members; + packet.Read(num_members); + member_information.resize(num_members); + + for (auto& member : member_information) { + packet.Read(member.nickname); + packet.Read(member.mac_address); + packet.Read(member.game_info.name); + packet.Read(member.game_info.id); + packet.Read(member.username); + packet.Read(member.display_name); + packet.Read(member.avatar_url); + + { + std::lock_guard lock(username_mutex); + if (member.nickname == nickname) { + username = member.username; + } + } + } + Invoke(room_information); +} + +void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the MAC Address from the packet + packet.Read(mac_address); +} + +void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { + WifiPacket wifi_packet{}; + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the WifiPacket from the packet + u8 frame_type; + packet.Read(frame_type); + WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type); + + wifi_packet.type = type; + packet.Read(wifi_packet.channel); + packet.Read(wifi_packet.transmitter_address); + packet.Read(wifi_packet.destination_address); + packet.Read(wifi_packet.data); + + Invoke<WifiPacket>(wifi_packet); +} + +void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + ChatEntry chat_entry{}; + packet.Read(chat_entry.nickname); + packet.Read(chat_entry.username); + packet.Read(chat_entry.message); + Invoke<ChatEntry>(chat_entry); +} + +void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + StatusMessageEntry status_message_entry{}; + u8 type{}; + packet.Read(type); + status_message_entry.type = static_cast<StatusMessageTypes>(type); + packet.Read(status_message_entry.nickname); + packet.Read(status_message_entry.username); + Invoke<StatusMessageEntry>(status_message_entry); +} + +void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + Room::BanList ban_list = {}; + packet.Read(ban_list.first); + packet.Read(ban_list.second); + Invoke<Room::BanList>(ban_list); +} + +void RoomMember::RoomMemberImpl::Disconnect() { + member_information.clear(); + room_information.member_slots = 0; + room_information.name.clear(); + + if (!server) { + return; + } + enet_peer_disconnect(server, 0); + + ENetEvent event; + while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + enet_packet_destroy(event.packet); // Ignore all incoming data + break; + case ENET_EVENT_TYPE_DISCONNECT: + server = nullptr; + return; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + // didn't disconnect gracefully force disconnect + enet_peer_reset(server); + server = nullptr; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_wifi_packet; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_state; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_error; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_room_information; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_chat_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_status_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_ban_list; +} + +template <typename T> +void RoomMember::RoomMemberImpl::Invoke(const T& data) { + std::lock_guard lock(callback_mutex); + CallbackSet<T> callback_set = callbacks.Get<T>(); + for (auto const& callback : callback_set) { + (*callback)(data); + } +} + +template <typename T> +RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind( + std::function<void(const T&)> callback) { + std::lock_guard lock(callback_mutex); + CallbackHandle<T> handle; + handle = std::make_shared<std::function<void(const T&)>>(callback); + callbacks.Get<T>().insert(handle); + return handle; +} + +// RoomMember +RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {} + +RoomMember::~RoomMember() { + ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected"); + if (room_member_impl->loop_thread) { + Leave(); + } +} + +RoomMember::State RoomMember::GetState() const { + return room_member_impl->state; +} + +const RoomMember::MemberList& RoomMember::GetMemberInformation() const { + return room_member_impl->member_information; +} + +const std::string& RoomMember::GetNickname() const { + return room_member_impl->nickname; +} + +const std::string& RoomMember::GetUsername() const { + std::lock_guard lock(room_member_impl->username_mutex); + return room_member_impl->username; +} + +const MacAddress& RoomMember::GetMacAddress() const { + ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); + return room_member_impl->mac_address; +} + +RoomInformation RoomMember::GetRoomInformation() const { + return room_member_impl->room_information; +} + +void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, + const char* server_addr, u16 server_port, u16 client_port, + const MacAddress& preferred_mac, const std::string& password, + const std::string& token) { + // If the member is connected, kill the connection first + if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { + Leave(); + } + // If the thread isn't running but the ptr still exists, reset it + else if (room_member_impl->loop_thread) { + room_member_impl->loop_thread.reset(); + } + + if (!room_member_impl->client) { + room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0); + ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); + } + + room_member_impl->SetState(State::Joining); + + ENetAddress address{}; + enet_address_set_host(&address, server_addr); + address.port = server_port; + room_member_impl->server = + enet_host_connect(room_member_impl->client, &address, NumChannels, 0); + + if (!room_member_impl->server) { + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::UnknownError); + return; + } + + ENetEvent event{}; + int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs); + if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { + room_member_impl->nickname = nick; + room_member_impl->StartLoop(); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); + SendGameInfo(room_member_impl->current_game_info); + } else { + enet_peer_disconnect(room_member_impl->server, 0); + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::CouldNotConnect); + } +} + +bool RoomMember::IsConnected() const { + return room_member_impl->IsConnected(); +} + +void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) { + Packet packet; + packet.Write(static_cast<u8>(IdWifiPacket)); + packet.Write(static_cast<u8>(wifi_packet.type)); + packet.Write(wifi_packet.channel); + packet.Write(wifi_packet.transmitter_address); + packet.Write(wifi_packet.destination_address); + packet.Write(wifi_packet.data); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendChatMessage(const std::string& message) { + Packet packet; + packet.Write(static_cast<u8>(IdChatMessage)); + packet.Write(message); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendGameInfo(const GameInfo& game_info) { + room_member_impl->current_game_info = game_info; + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(IdSetGameInfo)); + packet.Write(game_info.name); + packet.Write(game_info.id); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) { + ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban, + "type is not a moderation request"); + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(type)); + packet.Write(nickname); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::RequestBanList() { + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(IdModGetBanList)); + room_member_impl->Send(std::move(packet)); +} + +RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged( + std::function<void(const RoomMember::State&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError( + std::function<void(const RoomMember::Error&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived( + std::function<void(const WifiPacket&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged( + std::function<void(const RoomInformation&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved( + std::function<void(const ChatEntry&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived( + std::function<void(const StatusMessageEntry&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived( + std::function<void(const Room::BanList&)> callback) { + return room_member_impl->Bind(callback); +} + +template <typename T> +void RoomMember::Unbind(CallbackHandle<T> handle) { + std::lock_guard lock(room_member_impl->callback_mutex); + room_member_impl->callbacks.Get<T>().erase(handle); +} + +void RoomMember::Leave() { + room_member_impl->SetState(State::Idle); + room_member_impl->loop_thread->join(); + room_member_impl->loop_thread.reset(); + + enet_host_destroy(room_member_impl->client); + room_member_impl->client = nullptr; +} + +template void RoomMember::Unbind(CallbackHandle<WifiPacket>); +template void RoomMember::Unbind(CallbackHandle<RoomMember::State>); +template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>); +template void RoomMember::Unbind(CallbackHandle<RoomInformation>); +template void RoomMember::Unbind(CallbackHandle<ChatEntry>); +template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>); +template void RoomMember::Unbind(CallbackHandle<Room::BanList>); + +} // namespace Network diff --git a/src/network/room_member.h b/src/network/room_member.h new file mode 100644 index 000000000..bbb7d13d4 --- /dev/null +++ b/src/network/room_member.h @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <functional> +#include <memory> +#include <string> +#include <vector> +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "network/room.h" + +namespace Network { + +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::RoomInformation; + +/// Information about the received WiFi packets. +/// Acts as our own 802.11 header. +struct WifiPacket { + enum class PacketType : u8 { + Beacon, + Data, + Authentication, + AssociationResponse, + Deauthentication, + NodeMap + }; + PacketType type; ///< The type of 802.11 frame. + std::vector<u8> data; ///< Raw 802.11 frame data, starting at the management frame header + /// for management frames. + MacAddress transmitter_address; ///< Mac address of the transmitter. + MacAddress destination_address; ///< Mac address of the receiver. + u8 channel; ///< WiFi channel where this frame was transmitted. +}; + +/// Represents a chat message. +struct ChatEntry { + std::string nickname; ///< Nickname of the client who sent this message. + /// Web services username of the client who sent this message, can be empty. + std::string username; + std::string message; ///< Body of the message. +}; + +/// Represents a system status message. +struct StatusMessageEntry { + StatusMessageTypes type; ///< Type of the message + /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc. + std::string nickname; + std::string username; +}; + +/** + * This is what a client [person joining a server] would use. + * It also has to be used if you host a game yourself (You'd create both, a Room and a + * RoomMembership for yourself) + */ +class RoomMember final { +public: + enum class State : u8 { + Uninitialized, ///< Not initialized + Idle, ///< Default state (i.e. not connected) + Joining, ///< The client is attempting to join a room. + Joined, ///< The client is connected to the room and is ready to send/receive packets. + Moderator, ///< The client is connnected to the room and is granted mod permissions. + }; + + enum class Error : u8 { + // Reasons why connection was closed + LostConnection, ///< Connection closed + HostKicked, ///< Kicked by the host + + // Reasons why connection was rejected + UnknownError, ///< Some error [permissions to network device missing or something] + NameCollision, ///< Somebody is already using this name + MacCollision, ///< Somebody is already using that mac-address + ConsoleIdCollision, ///< Somebody in the room has the same Console ID + WrongVersion, ///< The room version is not the same as for this RoomMember + WrongPassword, ///< The password doesn't match the one from the Room + CouldNotConnect, ///< The room is not responding to a connection attempt + RoomIsFull, ///< Room is already at the maximum number of players + HostBanned, ///< The user is banned by the host + + // Reasons why moderation request failed + PermissionDenied, ///< The user does not have mod permissions + NoSuchUser, ///< The nickname the user attempts to kick/ban does not exist + }; + + struct MemberInformation { + std::string nickname; ///< Nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're + /// not playing anything. + MacAddress mac_address; ///< MAC address associated with this member. + }; + using MemberList = std::vector<MemberInformation>; + + // The handle for the callback functions + template <typename T> + using CallbackHandle = std::shared_ptr<std::function<void(const T&)>>; + + /** + * Unbinds a callback function from the events. + * @param handle The connection handle to disconnect + */ + template <typename T> + void Unbind(CallbackHandle<T> handle); + + RoomMember(); + ~RoomMember(); + + /** + * Returns the status of our connection to the room. + */ + State GetState() const; + + /** + * Returns information about the members in the room we're currently connected to. + */ + const MemberList& GetMemberInformation() const; + + /** + * Returns the nickname of the RoomMember. + */ + const std::string& GetNickname() const; + + /** + * Returns the username of the RoomMember. + */ + const std::string& GetUsername() const; + + /** + * Returns the MAC address of the RoomMember. + */ + const MacAddress& GetMacAddress() const; + + /** + * Returns information about the room we're currently connected to. + */ + RoomInformation GetRoomInformation() const; + + /** + * Returns whether we're connected to a server or not. + */ + bool IsConnected() const; + + /** + * Attempts to join a room at the specified address and port, using the specified nickname. + * A console ID hash is passed in to check console ID conflicts. + * This may fail if the username or console ID is already taken. + */ + void Join(const std::string& nickname, const std::string& console_id_hash, + const char* server_addr = "127.0.0.1", u16 server_port = DefaultRoomPort, + u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Sends a WiFi packet to the room. + * @param packet The WiFi packet to send. + */ + void SendWifiPacket(const WifiPacket& packet); + + /** + * Sends a chat message to the room. + * @param message The contents of the message. + */ + void SendChatMessage(const std::string& message); + + /** + * Sends the current game info to the room. + * @param game_info The game information. + */ + void SendGameInfo(const GameInfo& game_info); + + /** + * Sends a moderation request to the room. + * @param type Moderation request type. + * @param nickname The subject of the request. (i.e. the user you want to kick/ban) + */ + void SendModerationRequest(RoomMessageTypes type, const std::string& nickname); + + /** + * Attempts to retrieve ban list from the room. + * If success, the ban list callback would be called. Otherwise an error would be emitted. + */ + void RequestBanList(); + + /** + * Binds a function to an event that will be triggered every time the State of the member + * changed. The function wil be called every time the event is triggered. The callback function + * must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<State> BindOnStateChanged(std::function<void(const State&)> callback); + + /** + * Binds a function to an event that will be triggered every time an error happened. The + * function wil be called every time the event is triggered. The callback function must not bind + * or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<Error> BindOnError(std::function<void(const Error&)> callback); + + /** + * Binds a function to an event that will be triggered every time a WifiPacket is received. + * The function wil be called everytime the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<WifiPacket> BindOnWifiPacketReceived( + std::function<void(const WifiPacket&)> callback); + + /** + * Binds a function to an event that will be triggered every time the RoomInformation changes. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<RoomInformation> BindOnRoomInformationChanged( + std::function<void(const RoomInformation&)> callback); + + /** + * Binds a function to an event that will be triggered every time a ChatMessage is received. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<ChatEntry> BindOnChatMessageRecieved( + std::function<void(const ChatEntry&)> callback); + + /** + * Binds a function to an event that will be triggered every time a StatusMessage is + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<StatusMessageEntry> BindOnStatusMessageReceived( + std::function<void(const StatusMessageEntry&)> callback); + + /** + * Binds a function to an event that will be triggered every time a requested ban list + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<Room::BanList> BindOnBanListReceived( + std::function<void(const Room::BanList&)> callback); + + /** + * Leaves the current room. + */ + void Leave(); + +private: + class RoomMemberImpl; + std::unique_ptr<RoomMemberImpl> room_member_impl; +}; + +inline const char* GetStateStr(const RoomMember::State& s) { + switch (s) { + case RoomMember::State::Uninitialized: + return "Uninitialized"; + case RoomMember::State::Idle: + return "Idle"; + case RoomMember::State::Joining: + return "Joining"; + case RoomMember::State::Joined: + return "Joined"; + case RoomMember::State::Moderator: + return "Moderator"; + } + return "Unknown"; +} + +inline const char* GetErrorStr(const RoomMember::Error& e) { + switch (e) { + case RoomMember::Error::LostConnection: + return "LostConnection"; + case RoomMember::Error::HostKicked: + return "HostKicked"; + case RoomMember::Error::UnknownError: + return "UnknownError"; + case RoomMember::Error::NameCollision: + return "NameCollision"; + case RoomMember::Error::MacCollision: + return "MaxCollision"; + case RoomMember::Error::ConsoleIdCollision: + return "ConsoleIdCollision"; + case RoomMember::Error::WrongVersion: + return "WrongVersion"; + case RoomMember::Error::WrongPassword: + return "WrongPassword"; + case RoomMember::Error::CouldNotConnect: + return "CouldNotConnect"; + case RoomMember::Error::RoomIsFull: + return "RoomIsFull"; + case RoomMember::Error::HostBanned: + return "HostBanned"; + case RoomMember::Error::PermissionDenied: + return "PermissionDenied"; + case RoomMember::Error::NoSuchUser: + return "NoSuchUser"; + default: + return "Unknown"; + } +} + +} // namespace Network diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp new file mode 100644 index 000000000..f84cfe59b --- /dev/null +++ b/src/network/verify_user.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "network/verify_user.h" + +namespace Network::VerifyUser { + +Backend::~Backend() = default; + +NullBackend::~NullBackend() = default; + +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_uid, + [[maybe_unused]] const std::string& token) { + return {}; +} + +} // namespace Network::VerifyUser diff --git a/src/network/verify_user.h b/src/network/verify_user.h new file mode 100644 index 000000000..6fc64d8a3 --- /dev/null +++ b/src/network/verify_user.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <string> +#include "common/logging/log.h" + +namespace Network::VerifyUser { + +struct UserData { + std::string username; + std::string display_name; + std::string avatar_url; + bool moderator = false; ///< Whether the user is a yuzu Moderator. +}; + +/** + * A backend used for verifying users and loading user data. + */ +class Backend { +public: + virtual ~Backend(); + + /** + * Verifies the given token and loads the information into a UserData struct. + * @param verify_uid A GUID that may be used for verification. + * @param token A token that contains user data and verification data. The format and content is + * decided by backends. + */ + virtual UserData LoadUserData(const std::string& verify_uid, const std::string& token) = 0; +}; + +/** + * A null backend where the token is ignored. + * No verification is performed here and the function returns an empty UserData. + */ +class NullBackend final : public Backend { +public: + ~NullBackend(); + + UserData LoadUserData(const std::string& verify_uid, const std::string& token) override; +}; + +} // namespace Network::VerifyUser diff --git a/src/shader_recompiler/CMakeLists.txt b/src/shader_recompiler/CMakeLists.txt index ae1dbe619..af8e51fe8 100644 --- a/src/shader_recompiler/CMakeLists.txt +++ b/src/shader_recompiler/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_library(shader_recompiler STATIC backend/bindings.h backend/glasm/emit_glasm.cpp diff --git a/src/shader_recompiler/frontend/maxwell/translate/impl/logic_operation_three_input_lut3.py b/src/shader_recompiler/frontend/maxwell/translate/impl/logic_operation_three_input_lut3.py index 8f547c266..e66d50d61 100644 --- a/src/shader_recompiler/frontend/maxwell/translate/impl/logic_operation_three_input_lut3.py +++ b/src/shader_recompiler/frontend/maxwell/translate/impl/logic_operation_three_input_lut3.py @@ -1,7 +1,5 @@ -# Copyright © 2022 degasus <markus@selfnet.de> -# This work is free. You can redistribute it and/or modify it under the -# terms of the Do What The Fuck You Want To Public License, Version 2, -# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. +# SPDX-FileCopyrightText: 2022 degasus <markus@selfnet.de> +# SPDX-License-Identifier: WTFPL from itertools import product diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a69ccb264..43ad2c7ff 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_executable(tests common/bit_field.cpp common/cityhash.cpp @@ -7,7 +10,7 @@ add_executable(tests common/ring_buffer.cpp common/unique_function.cpp core/core_timing.cpp - core/network/network.cpp + core/internal_network/network.cpp tests.cpp video_core/buffer_base.cpp input_common/calibration_configuration_job.cpp diff --git a/src/tests/common/bit_field.cpp b/src/tests/common/bit_field.cpp index 182638000..0071ae52e 100644 --- a/src/tests/common/bit_field.cpp +++ b/src/tests/common/bit_field.cpp @@ -1,6 +1,5 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2019 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <cstring> diff --git a/src/tests/common/param_package.cpp b/src/tests/common/param_package.cpp index e31ca3544..d036cc83a 100644 --- a/src/tests/common/param_package.cpp +++ b/src/tests/common/param_package.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <catch2/catch.hpp> #include <math.h> diff --git a/src/tests/core/network/network.cpp b/src/tests/core/internal_network/network.cpp index 1bbb8372f..164b0ff24 100644 --- a/src/tests/core/network/network.cpp +++ b/src/tests/core/internal_network/network.cpp @@ -3,8 +3,8 @@ #include <catch2/catch.hpp> -#include "core/network/network.h" -#include "core/network/sockets.h" +#include "core/internal_network/network.h" +#include "core/internal_network/sockets.h" TEST_CASE("Network::Errors", "[core]") { Network::NetworkInstance network_instance; // initialize network diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 275b430d9..3f905c05c 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #define CATCH_CONFIG_MAIN #include <catch2/catch.hpp> diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 14de7bc89..5b3808351 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_subdirectory(host_shaders) if(LIBVA_FOUND) diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 190fc6aea..2149ab93e 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + set(FIDELITYFX_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-FSR/ffx-fsr) set(GLSL_INCLUDES diff --git a/src/video_core/host_shaders/StringShaderHeader.cmake b/src/video_core/host_shaders/StringShaderHeader.cmake index 1b4bc6103..9f7525535 100644 --- a/src/video_core/host_shaders/StringShaderHeader.cmake +++ b/src/video_core/host_shaders/StringShaderHeader.cmake @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2020 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + set(SOURCE_FILE ${CMAKE_ARGV3}) set(HEADER_FILE ${CMAKE_ARGV4}) set(INPUT_FILE ${CMAKE_ARGV5}) diff --git a/src/video_core/host_shaders/source_shader.h.in b/src/video_core/host_shaders/source_shader.h.in index 929dec39b..f189ee06b 100644 --- a/src/video_core/host_shaders/source_shader.h.in +++ b/src/video_core/host_shaders/source_shader.h.in @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #pragma once #include <string_view> diff --git a/src/video_core/host_shaders/vulkan_present_scaleforce_fp16.frag b/src/video_core/host_shaders/vulkan_present_scaleforce_fp16.frag index 924c03060..3dc9c0df5 100644 --- a/src/video_core/host_shaders/vulkan_present_scaleforce_fp16.frag +++ b/src/video_core/host_shaders/vulkan_present_scaleforce_fp16.frag @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #version 460 #extension GL_GOOGLE_include_directive : enable diff --git a/src/video_core/host_shaders/vulkan_present_scaleforce_fp32.frag b/src/video_core/host_shaders/vulkan_present_scaleforce_fp32.frag index a594b83ca..77ed07552 100644 --- a/src/video_core/host_shaders/vulkan_present_scaleforce_fp32.frag +++ b/src/video_core/host_shaders/vulkan_present_scaleforce_fp32.frag @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #version 460 #extension GL_GOOGLE_include_directive : enable diff --git a/src/video_core/renderer_base.cpp b/src/video_core/renderer_base.cpp index 9756a81d6..45791aa75 100644 --- a/src/video_core/renderer_base.cpp +++ b/src/video_core/renderer_base.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/logging/log.h" #include "core/frontend/emu_window.h" diff --git a/src/video_core/renderer_base.h b/src/video_core/renderer_base.h index 30d19b178..8d20cbece 100644 --- a/src/video_core/renderer_base.h +++ b/src/video_core/renderer_base.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index 159b71161..a0d048b0b 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <array> diff --git a/src/video_core/renderer_opengl/gl_rasterizer.h b/src/video_core/renderer_opengl/gl_rasterizer.h index c79461d59..31a16fcba 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.h +++ b/src/video_core/renderer_opengl/gl_rasterizer.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/renderer_opengl/gl_resource_manager.cpp b/src/video_core/renderer_opengl/gl_resource_manager.cpp index f6839a657..3a664fdec 100644 --- a/src/video_core/renderer_opengl/gl_resource_manager.cpp +++ b/src/video_core/renderer_opengl/gl_resource_manager.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <string_view> #include <glad/glad.h> diff --git a/src/video_core/renderer_opengl/gl_resource_manager.h b/src/video_core/renderer_opengl/gl_resource_manager.h index 84e07f8bd..bc05ba4bd 100644 --- a/src/video_core/renderer_opengl/gl_resource_manager.h +++ b/src/video_core/renderer_opengl/gl_resource_manager.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/renderer_opengl/gl_shader_util.cpp b/src/video_core/renderer_opengl/gl_shader_util.cpp index 129966e72..f83ad0a5b 100644 --- a/src/video_core/renderer_opengl/gl_shader_util.cpp +++ b/src/video_core/renderer_opengl/gl_shader_util.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <string_view> #include <vector> diff --git a/src/video_core/renderer_opengl/gl_shader_util.h b/src/video_core/renderer_opengl/gl_shader_util.h index a64ef37dc..43ebcdeba 100644 --- a/src/video_core/renderer_opengl/gl_shader_util.h +++ b/src/video_core/renderer_opengl/gl_shader_util.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 9a9243544..01028cee0 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <cstddef> diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index ae9558a33..1a32e739d 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp index eecd0deff..079d5f028 100644 --- a/src/video_core/surface.cpp +++ b/src/video_core/surface.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "common/common_types.h" #include "common/math_util.h" diff --git a/src/video_core/surface.h b/src/video_core/surface.h index 0175432ff..16273f185 100644 --- a/src/video_core/surface.h +++ b/src/video_core/surface.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index 2f2594585..04ac4af11 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h index 084df641f..f8e2444f3 100644 --- a/src/video_core/video_core.h +++ b/src/video_core/video_core.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt index ae85a72ea..3f75d97d1 100644 --- a/src/web_service/CMakeLists.txt +++ b/src/web_service/CMakeLists.txt @@ -1,12 +1,19 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + add_library(web_service STATIC + announce_room_json.cpp + announce_room_json.h telemetry_json.cpp telemetry_json.h verify_login.cpp verify_login.h + verify_user_jwt.cpp + verify_user_jwt.h web_backend.cpp web_backend.h web_result.h ) create_target_directory_groups(web_service) -target_link_libraries(web_service PRIVATE common nlohmann_json::nlohmann_json httplib) +target_link_libraries(web_service PRIVATE common network nlohmann_json::nlohmann_json httplib cpp-jwt) diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp new file mode 100644 index 000000000..4c3195efd --- /dev/null +++ b/src/web_service/announce_room_json.cpp @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <future> +#include <nlohmann/json.hpp> +#include "common/detached_tasks.h" +#include "common/logging/log.h" +#include "web_service/announce_room_json.h" +#include "web_service/web_backend.h" + +namespace AnnounceMultiplayerRoom { + +static void to_json(nlohmann::json& json, const Member& member) { + if (!member.username.empty()) { + json["username"] = member.username; + } + json["nickname"] = member.nickname; + if (!member.avatar_url.empty()) { + json["avatarUrl"] = member.avatar_url; + } + json["gameName"] = member.game.name; + json["gameId"] = member.game.id; +} + +static void from_json(const nlohmann::json& json, Member& member) { + member.nickname = json.at("nickname").get<std::string>(); + member.game.name = json.at("gameName").get<std::string>(); + member.game.id = json.at("gameId").get<u64>(); + try { + member.username = json.at("username").get<std::string>(); + member.avatar_url = json.at("avatarUrl").get<std::string>(); + } catch (const nlohmann::detail::out_of_range&) { + member.username = member.avatar_url = ""; + LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname); + } +} + +static void to_json(nlohmann::json& json, const Room& room) { + json["port"] = room.information.port; + json["name"] = room.information.name; + if (!room.information.description.empty()) { + json["description"] = room.information.description; + } + json["preferredGameName"] = room.information.preferred_game.name; + json["preferredGameId"] = room.information.preferred_game.id; + json["maxPlayers"] = room.information.member_slots; + json["netVersion"] = room.net_version; + json["hasPassword"] = room.has_password; + if (room.members.size() > 0) { + nlohmann::json member_json = room.members; + json["players"] = member_json; + } +} + +static void from_json(const nlohmann::json& json, Room& room) { + room.verify_uid = json.at("externalGuid").get<std::string>(); + room.ip = json.at("address").get<std::string>(); + room.information.name = json.at("name").get<std::string>(); + try { + room.information.description = json.at("description").get<std::string>(); + } catch (const nlohmann::detail::out_of_range&) { + room.information.description = ""; + LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.information.name); + } + room.information.host_username = json.at("owner").get<std::string>(); + room.information.port = json.at("port").get<u16>(); + room.information.preferred_game.name = json.at("preferredGameName").get<std::string>(); + room.information.preferred_game.id = json.at("preferredGameId").get<u64>(); + room.information.member_slots = json.at("maxPlayers").get<u32>(); + room.net_version = json.at("netVersion").get<u32>(); + room.has_password = json.at("hasPassword").get<bool>(); + try { + room.members = json.at("players").get<std::vector<Member>>(); + } catch (const nlohmann::detail::out_of_range& e) { + LOG_DEBUG(Network, "Out of range {}", e.what()); + } +} + +} // namespace AnnounceMultiplayerRoom + +namespace WebService { + +void RoomJson::SetRoomInformation(const std::string& name, const std::string& description, + const u16 port, const u32 max_player, const u32 net_version, + const bool has_password, + const AnnounceMultiplayerRoom::GameInfo& preferred_game) { + room.information.name = name; + room.information.description = description; + room.information.port = port; + room.information.member_slots = max_player; + room.net_version = net_version; + room.has_password = has_password; + room.information.preferred_game = preferred_game; +} +void RoomJson::AddPlayer(const AnnounceMultiplayerRoom::Member& member) { + room.members.push_back(member); +} + +WebService::WebResult RoomJson::Update() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be updated"); + return WebService::WebResult{WebService::WebResult::Code::LibError, + "Room is not registered", ""}; + } + nlohmann::json json{{"players", room.members}}; + return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false); +} + +WebService::WebResult RoomJson::Register() { + nlohmann::json json = room; + auto result = client.PostJson("/lobby", json.dump(), false); + if (result.result_code != WebService::WebResult::Code::Success) { + return result; + } + auto reply_json = nlohmann::json::parse(result.returned_data); + room = reply_json.get<AnnounceMultiplayerRoom::Room>(); + room_id = reply_json.at("id").get<std::string>(); + return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_uid}; +} + +void RoomJson::ClearPlayers() { + room.members.clear(); +} + +AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() { + auto reply = client.GetJson("/lobby", true).returned_data; + if (reply.empty()) { + return {}; + } + return nlohmann::json::parse(reply).at("rooms").get<AnnounceMultiplayerRoom::RoomList>(); +} + +void RoomJson::Delete() { + if (room_id.empty()) { + LOG_ERROR(WebService, "Room must be registered to be deleted"); + return; + } + Common::DetachedTasks::AddTask( + [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() { + // create a new client here because the this->client might be destroyed. + Client{host, username, token}.DeleteJson(fmt::format("/lobby/{}", room_id), "", false); + }); +} + +} // namespace WebService diff --git a/src/web_service/announce_room_json.h b/src/web_service/announce_room_json.h new file mode 100644 index 000000000..32c08858d --- /dev/null +++ b/src/web_service/announce_room_json.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <functional> +#include <string> +#include "common/announce_multiplayer_room.h" +#include "web_service/web_backend.h" + +namespace WebService { + +/** + * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from + * JSON, and submits/gets it to/from the yuzu web service + */ +class RoomJson : public AnnounceMultiplayerRoom::Backend { +public: + RoomJson(const std::string& host_, const std::string& username_, const std::string& token_) + : client(host_, username_, token_), host(host_), username(username_), token(token_) {} + ~RoomJson() = default; + void SetRoomInformation(const std::string& name, const std::string& description, const u16 port, + const u32 max_player, const u32 net_version, const bool has_password, + const AnnounceMultiplayerRoom::GameInfo& preferred_game) override; + void AddPlayer(const AnnounceMultiplayerRoom::Member& member) override; + WebResult Update() override; + WebResult Register() override; + void ClearPlayers() override; + AnnounceMultiplayerRoom::RoomList GetRoomList() override; + void Delete() override; + +private: + AnnounceMultiplayerRoom::Room room; + Client client; + std::string host; + std::string username; + std::string token; + std::string room_id; +}; + +} // namespace WebService diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp index 46faddb61..51c792004 100644 --- a/src/web_service/telemetry_json.cpp +++ b/src/web_service/telemetry_json.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <nlohmann/json.hpp> #include "common/detached_tasks.h" diff --git a/src/web_service/telemetry_json.h b/src/web_service/telemetry_json.h index df51e00f8..504002c04 100644 --- a/src/web_service/telemetry_json.h +++ b/src/web_service/telemetry_json.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp index ceb55ca6b..050080278 100644 --- a/src/web_service/verify_login.cpp +++ b/src/web_service/verify_login.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <nlohmann/json.hpp> #include "web_service/verify_login.h" diff --git a/src/web_service/verify_login.h b/src/web_service/verify_login.h index 821b345d7..8d0adce74 100644 --- a/src/web_service/verify_login.h +++ b/src/web_service/verify_login.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/web_service/verify_user_jwt.cpp b/src/web_service/verify_user_jwt.cpp new file mode 100644 index 000000000..3bff46f0a --- /dev/null +++ b/src/web_service/verify_user_jwt.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +#endif +#include <jwt/jwt.hpp> +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#include <system_error> +#include "common/logging/log.h" +#include "web_service/verify_user_jwt.h" +#include "web_service/web_backend.h" +#include "web_service/web_result.h" + +namespace WebService { + +static std::string public_key; +std::string GetPublicKey(const std::string& host) { + if (public_key.empty()) { + Client client(host, "", ""); // no need for credentials here + public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data; + if (public_key.empty()) { + LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail"); + } else { + LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size()); + } + } + return public_key; +} + +VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {} + +Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_uid, + const std::string& token) { + const std::string audience = fmt::format("external-{}", verify_uid); + using namespace jwt::params; + std::error_code error; + auto decoded = + jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("yuzu-core"), + aud(audience), validate_iat(true), validate_jti(true)); + if (error) { + LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}", + error.category().name(), error.value(), error.message()); + return {}; + } + Network::VerifyUser::UserData user_data{}; + if (decoded.payload().has_claim("username")) { + user_data.username = decoded.payload().get_claim_value<std::string>("username"); + } + if (decoded.payload().has_claim("displayName")) { + user_data.display_name = decoded.payload().get_claim_value<std::string>("displayName"); + } + if (decoded.payload().has_claim("avatarUrl")) { + user_data.avatar_url = decoded.payload().get_claim_value<std::string>("avatarUrl"); + } + if (decoded.payload().has_claim("roles")) { + auto roles = decoded.payload().get_claim_value<std::vector<std::string>>("roles"); + user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end(); + } + return user_data; +} + +} // namespace WebService diff --git a/src/web_service/verify_user_jwt.h b/src/web_service/verify_user_jwt.h new file mode 100644 index 000000000..27b0a100c --- /dev/null +++ b/src/web_service/verify_user_jwt.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <fmt/format.h> +#include "network/verify_user.h" +#include "web_service/web_backend.h" + +namespace WebService { + +std::string GetPublicKey(const std::string& host); + +class VerifyUserJWT final : public Network::VerifyUser::Backend { +public: + VerifyUserJWT(const std::string& host); + ~VerifyUserJWT() = default; + + Network::VerifyUser::UserData LoadUserData(const std::string& verify_uid, + const std::string& token) override; + +private: + std::string pub_key; +}; + +} // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index dce9772fe..378804c08 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <mutex> diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index 81f58583c..11b5f558c 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index a11a3b908..f6b389ede 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) @@ -30,8 +33,6 @@ add_executable(yuzu applets/qt_web_browser_scripts.h bootmanager.cpp bootmanager.h - check_vulkan.cpp - check_vulkan.h compatdb.ui compatibility_list.cpp compatibility_list.h @@ -158,8 +159,36 @@ add_executable(yuzu main.cpp main.h main.ui + multiplayer/chat_room.cpp + multiplayer/chat_room.h + multiplayer/chat_room.ui + multiplayer/client_room.h + multiplayer/client_room.cpp + multiplayer/client_room.ui + multiplayer/direct_connect.cpp + multiplayer/direct_connect.h + multiplayer/direct_connect.ui + multiplayer/host_room.cpp + multiplayer/host_room.h + multiplayer/host_room.ui + multiplayer/lobby.cpp + multiplayer/lobby.h + multiplayer/lobby.ui + multiplayer/lobby_p.h + multiplayer/message.cpp + multiplayer/message.h + multiplayer/moderation_dialog.cpp + multiplayer/moderation_dialog.h + multiplayer/moderation_dialog.ui + multiplayer/state.cpp + multiplayer/state.h + multiplayer/validation.h + startup_checks.cpp + startup_checks.h uisettings.cpp uisettings.h + util/clickable_label.cpp + util/clickable_label.h util/controller_navigation.cpp util/controller_navigation.h util/limitable_input_dialog.cpp @@ -256,7 +285,7 @@ endif() create_target_directory_groups(yuzu) -target_link_libraries(yuzu PRIVATE common core input_common video_core) +target_link_libraries(yuzu PRIVATE common core input_common network video_core) target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) @@ -300,6 +329,10 @@ if (USE_DISCORD_PRESENCE) target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) endif() +if (ENABLE_WEB_SERVICE) + target_compile_definitions(yuzu PRIVATE -DENABLE_WEB_SERVICE) +endif() + if (YUZU_USE_QT_WEB_ENGINE) target_link_libraries(yuzu PRIVATE Qt::WebEngineCore Qt::WebEngineWidgets) target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) diff --git a/src/yuzu/Info.plist b/src/yuzu/Info.plist index 5f1c95d54..0eb377926 100644 --- a/src/yuzu/Info.plist +++ b/src/yuzu/Info.plist @@ -1,4 +1,10 @@ <?xml version="1.0" encoding="UTF-8"?> + +<!-- +SPDX-FileCopyrightText: 2015 Pierre de La Morinerie <kemenaran@gmail.com> +SPDX-License-Identifier: GPL-2.0-or-later +--> + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> diff --git a/src/yuzu/aboutdialog.ui b/src/yuzu/aboutdialog.ui index 1dd7b74bf..c4ffb293e 100644 --- a/src/yuzu/aboutdialog.ui +++ b/src/yuzu/aboutdialog.ui @@ -127,7 +127,7 @@ p, li { white-space: pre-wrap; } <item> <widget class="QLabel" name="labelLinks"> <property name="text"> - <string><html><head/><body><p><a href="https://yuzu-emu.org/"><span style=" text-decoration: underline; color:#039be5;">Website</span></a> | <a href="https://github.com/yuzu-emu"><span style=" text-decoration: underline; color:#039be5;">Source Code</span></a> | <a href="https://github.com/yuzu-emu/yuzu/graphs/contributors"><span style=" text-decoration: underline; color:#039be5;">Contributors</span></a> | <a href="https://github.com/yuzu-emu/yuzu/blob/master/license.txt"><span style=" text-decoration: underline; color:#039be5;">License</span></a></p></body></html></string> + <string><html><head/><body><p><a href="https://yuzu-emu.org/"><span style=" text-decoration: underline; color:#039be5;">Website</span></a> | <a href="https://github.com/yuzu-emu"><span style=" text-decoration: underline; color:#039be5;">Source Code</span></a> | <a href="https://github.com/yuzu-emu/yuzu/graphs/contributors"><span style=" text-decoration: underline; color:#039be5;">Contributors</span></a> | <a href="https://github.com/yuzu-emu/yuzu/blob/master/LICENSE.txt"><span style=" text-decoration: underline; color:#039be5;">License</span></a></p></body></html></string> </property> <property name="openExternalLinks"> <bool>true</bool> diff --git a/src/yuzu/applets/qt_software_keyboard.cpp b/src/yuzu/applets/qt_software_keyboard.cpp index e8b217d90..e60506197 100644 --- a/src/yuzu/applets/qt_software_keyboard.cpp +++ b/src/yuzu/applets/qt_software_keyboard.cpp @@ -213,9 +213,9 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog( ui->button_ok_num, }, { - nullptr, + ui->button_left_optional_num, ui->button_0_num, - nullptr, + ui->button_right_optional_num, ui->button_ok_num, }, }}; @@ -330,7 +330,9 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog( ui->button_7_num, ui->button_8_num, ui->button_9_num, + ui->button_left_optional_num, ui->button_0_num, + ui->button_right_optional_num, }; SetupMouseHover(); @@ -342,6 +344,9 @@ QtSoftwareKeyboardDialog::QtSoftwareKeyboardDialog( ui->label_header->setText(QString::fromStdU16String(initialize_parameters.header_text)); ui->label_sub->setText(QString::fromStdU16String(initialize_parameters.sub_text)); + ui->button_left_optional_num->setText(QChar{initialize_parameters.left_optional_symbol_key}); + ui->button_right_optional_num->setText(QChar{initialize_parameters.right_optional_symbol_key}); + current_text = initialize_parameters.initial_text; cursor_position = initialize_parameters.initial_cursor_position; @@ -932,6 +937,15 @@ void QtSoftwareKeyboardDialog::DisableKeyboardButtons() { button->setEnabled(true); } } + + const auto enable_left_optional = initialize_parameters.left_optional_symbol_key != '\0'; + const auto enable_right_optional = initialize_parameters.right_optional_symbol_key != '\0'; + + ui->button_left_optional_num->setEnabled(enable_left_optional); + ui->button_left_optional_num->setVisible(enable_left_optional); + + ui->button_right_optional_num->setEnabled(enable_right_optional); + ui->button_right_optional_num->setVisible(enable_right_optional); break; } } @@ -1019,7 +1033,10 @@ bool QtSoftwareKeyboardDialog::ValidateInputText(const QString& input_text) { } if (bottom_osk_index == BottomOSKIndex::NumberPad && - std::any_of(input_text.begin(), input_text.end(), [](QChar c) { return !c.isDigit(); })) { + std::any_of(input_text.begin(), input_text.end(), [this](QChar c) { + return !c.isDigit() && c != QChar{initialize_parameters.left_optional_symbol_key} && + c != QChar{initialize_parameters.right_optional_symbol_key}; + })) { return false; } @@ -1384,6 +1401,10 @@ void QtSoftwareKeyboardDialog::MoveButtonDirection(Direction direction) { } }; + // Store the initial row and column. + const auto initial_row = row; + const auto initial_column = column; + switch (bottom_osk_index) { case BottomOSKIndex::LowerCase: case BottomOSKIndex::UpperCase: { @@ -1394,6 +1415,11 @@ void QtSoftwareKeyboardDialog::MoveButtonDirection(Direction direction) { auto* curr_button = keyboard_buttons[index][row][column]; while (!curr_button || !curr_button->isEnabled() || curr_button == prev_button) { + // If we returned back to where we started from, break the loop. + if (row == initial_row && column == initial_column) { + break; + } + move_direction(NUM_ROWS_NORMAL, NUM_COLUMNS_NORMAL); curr_button = keyboard_buttons[index][row][column]; } @@ -1408,6 +1434,11 @@ void QtSoftwareKeyboardDialog::MoveButtonDirection(Direction direction) { auto* curr_button = numberpad_buttons[row][column]; while (!curr_button || !curr_button->isEnabled() || curr_button == prev_button) { + // If we returned back to where we started from, break the loop. + if (row == initial_row && column == initial_column) { + break; + } + move_direction(NUM_ROWS_NUMPAD, NUM_COLUMNS_NUMPAD); curr_button = numberpad_buttons[row][column]; } diff --git a/src/yuzu/applets/qt_software_keyboard.h b/src/yuzu/applets/qt_software_keyboard.h index 1c489fbb6..35d4ee2ef 100644 --- a/src/yuzu/applets/qt_software_keyboard.h +++ b/src/yuzu/applets/qt_software_keyboard.h @@ -211,7 +211,7 @@ private: std::array<std::array<QPushButton*, NUM_COLUMNS_NUMPAD>, NUM_ROWS_NUMPAD> numberpad_buttons; // Contains a set of all buttons used in keyboard_buttons and numberpad_buttons. - std::array<QPushButton*, 110> all_buttons; + std::array<QPushButton*, 112> all_buttons; std::size_t row{0}; std::size_t column{0}; diff --git a/src/yuzu/applets/qt_software_keyboard.ui b/src/yuzu/applets/qt_software_keyboard.ui index b0a1fcde9..9661cb260 100644 --- a/src/yuzu/applets/qt_software_keyboard.ui +++ b/src/yuzu/applets/qt_software_keyboard.ui @@ -3298,6 +3298,24 @@ p, li { white-space: pre-wrap; } </property> </widget> </item> + <item row="4" column="2"> + <widget class="QPushButton" name="button_left_optional_num"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>1</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>28</pointsize> + </font> + </property> + <property name="text"> + <string notr="true"></string> + </property> + </widget> + </item> <item row="4" column="3"> <widget class="QPushButton" name="button_0_num"> <property name="sizePolicy"> @@ -3316,6 +3334,24 @@ p, li { white-space: pre-wrap; } </property> </widget> </item> + <item row="4" column="4"> + <widget class="QPushButton" name="button_right_optional_num"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>1</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>28</pointsize> + </font> + </property> + <property name="text"> + <string notr="true"></string> + </property> + </widget> + </item> <item row="1" column="4"> <widget class="QPushButton" name="button_3_num"> <property name="sizePolicy"> @@ -3494,7 +3530,9 @@ p, li { white-space: pre-wrap; } <tabstop>button_7_num</tabstop> <tabstop>button_8_num</tabstop> <tabstop>button_9_num</tabstop> + <tabstop>button_left_optional_num</tabstop> <tabstop>button_0_num</tabstop> + <tabstop>button_right_optional_num</tabstop> </tabstops> <resources> <include location="../../../dist/icons/overlay/overlay.qrc"/> diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 8feb85b71..ef3bdfb1a 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <glad/glad.h> diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index b4781e697..c45ebf1a2 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/check_vulkan.cpp b/src/yuzu/check_vulkan.cpp deleted file mode 100644 index e6d66ab34..000000000 --- a/src/yuzu/check_vulkan.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "video_core/vulkan_common/vulkan_wrapper.h" - -#include <filesystem> -#include <fstream> -#include "common/fs/fs.h" -#include "common/fs/path_util.h" -#include "common/logging/log.h" -#include "video_core/vulkan_common/vulkan_instance.h" -#include "video_core/vulkan_common/vulkan_library.h" -#include "yuzu/check_vulkan.h" -#include "yuzu/uisettings.h" - -constexpr char TEMP_FILE_NAME[] = "vulkan_check"; - -bool CheckVulkan() { - if (UISettings::values.has_broken_vulkan) { - return true; - } - - LOG_DEBUG(Frontend, "Checking presence of Vulkan"); - - const auto fs_config_loc = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir); - const auto temp_file_loc = fs_config_loc / TEMP_FILE_NAME; - - if (std::filesystem::exists(temp_file_loc)) { - LOG_WARNING(Frontend, "Detected recovery from previous failed Vulkan initialization"); - - UISettings::values.has_broken_vulkan = true; - std::filesystem::remove(temp_file_loc); - return false; - } - - std::ofstream temp_file_handle(temp_file_loc); - temp_file_handle.close(); - - try { - Vulkan::vk::InstanceDispatch dld; - const Common::DynamicLibrary library = Vulkan::OpenLibrary(); - const Vulkan::vk::Instance instance = - Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_0); - - } catch (const Vulkan::vk::Exception& exception) { - LOG_ERROR(Frontend, "Failed to initialize Vulkan: {}", exception.what()); - // Don't set has_broken_vulkan to true here: we care when loading Vulkan crashes the - // application, not when we can handle it. - } - - std::filesystem::remove(temp_file_loc); - return true; -} diff --git a/src/yuzu/check_vulkan.h b/src/yuzu/check_vulkan.h deleted file mode 100644 index e4ea93582..000000000 --- a/src/yuzu/check_vulkan.h +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -bool CheckVulkan(); diff --git a/src/yuzu/compatdb.cpp b/src/yuzu/compatdb.cpp index 2442bb3c3..f46fff340 100644 --- a/src/yuzu/compatdb.cpp +++ b/src/yuzu/compatdb.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QButtonGroup> #include <QMessageBox> diff --git a/src/yuzu/compatdb.h b/src/yuzu/compatdb.h index e2b2522bd..3252fc47a 100644 --- a/src/yuzu/compatdb.h +++ b/src/yuzu/compatdb.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 1f76e86b9..58f1239bf 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <QKeySequence> @@ -11,6 +10,7 @@ #include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/hid/controllers/npad.h" #include "input_common/main.h" +#include "network/network.h" #include "yuzu/configuration/config.h" namespace FS = Common::FS; @@ -688,12 +688,6 @@ void Config::ReadRendererValues() { ReadGlobalSetting(Settings::values.bg_green); ReadGlobalSetting(Settings::values.bg_blue); - if (!global && UISettings::values.has_broken_vulkan && - Settings::values.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan && - !Settings::values.renderer_backend.UsingGlobal()) { - Settings::values.renderer_backend.SetGlobal(true); - } - if (global) { ReadBasicSetting(Settings::values.renderer_debug); ReadBasicSetting(Settings::values.renderer_shader_feedback); @@ -800,6 +794,7 @@ void Config::ReadUIValues() { ReadPathValues(); ReadScreenshotValues(); ReadShortcutValues(); + ReadMultiplayerValues(); ReadBasicSetting(UISettings::values.single_window_mode); ReadBasicSetting(UISettings::values.fullscreen); @@ -813,7 +808,6 @@ void Config::ReadUIValues() { ReadBasicSetting(UISettings::values.pause_when_in_background); ReadBasicSetting(UISettings::values.mute_when_in_background); ReadBasicSetting(UISettings::values.hide_mouse); - ReadBasicSetting(UISettings::values.has_broken_vulkan); ReadBasicSetting(UISettings::values.disable_web_applet); qt_config->endGroup(); @@ -867,6 +861,42 @@ void Config::ReadWebServiceValues() { qt_config->endGroup(); } +void Config::ReadMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + ReadBasicSetting(UISettings::values.multiplayer_nickname); + ReadBasicSetting(UISettings::values.multiplayer_ip); + ReadBasicSetting(UISettings::values.multiplayer_port); + ReadBasicSetting(UISettings::values.multiplayer_room_nickname); + ReadBasicSetting(UISettings::values.multiplayer_room_name); + ReadBasicSetting(UISettings::values.multiplayer_room_port); + ReadBasicSetting(UISettings::values.multiplayer_host_type); + ReadBasicSetting(UISettings::values.multiplayer_port); + ReadBasicSetting(UISettings::values.multiplayer_max_player); + ReadBasicSetting(UISettings::values.multiplayer_game_id); + ReadBasicSetting(UISettings::values.multiplayer_room_description); + + // Read ban list back + int size = qt_config->beginReadArray(QStringLiteral("username_ban_list")); + UISettings::values.multiplayer_ban_list.first.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.multiplayer_ban_list.first[i] = + ReadSetting(QStringLiteral("username")).toString().toStdString(); + } + qt_config->endArray(); + size = qt_config->beginReadArray(QStringLiteral("ip_ban_list")); + UISettings::values.multiplayer_ban_list.second.resize(size); + for (int i = 0; i < size; ++i) { + qt_config->setArrayIndex(i); + UISettings::values.multiplayer_ban_list.second[i] = + ReadSetting(QStringLiteral("ip")).toString().toStdString(); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + void Config::ReadValues() { if (global) { ReadControlValues(); @@ -883,6 +913,7 @@ void Config::ReadValues() { ReadRendererValues(); ReadAudioValues(); ReadSystemValues(); + ReadMultiplayerValues(); } void Config::SavePlayerValue(std::size_t player_index) { @@ -1032,6 +1063,7 @@ void Config::SaveValues() { SaveRendererValues(); SaveAudioValues(); SaveSystemValues(); + SaveMultiplayerValues(); } void Config::SaveAudioValues() { @@ -1354,6 +1386,7 @@ void Config::SaveUIValues() { SavePathValues(); SaveScreenshotValues(); SaveShortcutValues(); + SaveMultiplayerValues(); WriteBasicSetting(UISettings::values.single_window_mode); WriteBasicSetting(UISettings::values.fullscreen); @@ -1367,7 +1400,6 @@ void Config::SaveUIValues() { WriteBasicSetting(UISettings::values.pause_when_in_background); WriteBasicSetting(UISettings::values.mute_when_in_background); WriteBasicSetting(UISettings::values.hide_mouse); - WriteBasicSetting(UISettings::values.has_broken_vulkan); WriteBasicSetting(UISettings::values.disable_web_applet); qt_config->endGroup(); @@ -1419,6 +1451,40 @@ void Config::SaveWebServiceValues() { qt_config->endGroup(); } +void Config::SaveMultiplayerValues() { + qt_config->beginGroup(QStringLiteral("Multiplayer")); + + WriteBasicSetting(UISettings::values.multiplayer_nickname); + WriteBasicSetting(UISettings::values.multiplayer_ip); + WriteBasicSetting(UISettings::values.multiplayer_port); + WriteBasicSetting(UISettings::values.multiplayer_room_nickname); + WriteBasicSetting(UISettings::values.multiplayer_room_name); + WriteBasicSetting(UISettings::values.multiplayer_room_port); + WriteBasicSetting(UISettings::values.multiplayer_host_type); + WriteBasicSetting(UISettings::values.multiplayer_port); + WriteBasicSetting(UISettings::values.multiplayer_max_player); + WriteBasicSetting(UISettings::values.multiplayer_game_id); + WriteBasicSetting(UISettings::values.multiplayer_room_description); + + // Write ban list + qt_config->beginWriteArray(QStringLiteral("username_ban_list")); + for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.first.size(); ++i) { + qt_config->setArrayIndex(static_cast<int>(i)); + WriteSetting(QStringLiteral("username"), + QString::fromStdString(UISettings::values.multiplayer_ban_list.first[i])); + } + qt_config->endArray(); + qt_config->beginWriteArray(QStringLiteral("ip_ban_list")); + for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.second.size(); ++i) { + qt_config->setArrayIndex(static_cast<int>(i)); + WriteSetting(QStringLiteral("ip"), + QString::fromStdString(UISettings::values.multiplayer_ban_list.second[i])); + } + qt_config->endArray(); + + qt_config->endGroup(); +} + QVariant Config::ReadSetting(const QString& name) const { return qt_config->value(name); } diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index a71eabe8e..486ceea94 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -89,6 +88,7 @@ private: void ReadUIGamelistValues(); void ReadUILayoutValues(); void ReadWebServiceValues(); + void ReadMultiplayerValues(); void SaveValues(); void SavePlayerValue(std::size_t player_index); @@ -118,6 +118,7 @@ private: void SaveUIGamelistValues(); void SaveUILayoutValues(); void SaveWebServiceValues(); + void SaveMultiplayerValues(); /** * Reads a setting from the qt_config. diff --git a/src/yuzu/configuration/configuration_shared.cpp b/src/yuzu/configuration/configuration_shared.cpp index dd4959417..97fb664bf 100644 --- a/src/yuzu/configuration/configuration_shared.cpp +++ b/src/yuzu/configuration/configuration_shared.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QCheckBox> #include <QObject> diff --git a/src/yuzu/configuration/configuration_shared.h b/src/yuzu/configuration/configuration_shared.h index 56800b6ff..e597dcdb5 100644 --- a/src/yuzu/configuration/configuration_shared.h +++ b/src/yuzu/configuration/configuration_shared.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 84808f678..e16d127a8 100644 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QDesktopServices> #include <QUrl> diff --git a/src/yuzu/configuration/configure_debug.h b/src/yuzu/configuration/configure_debug.h index 73f71c9e3..64d68ab8f 100644 --- a/src/yuzu/configuration/configure_debug.h +++ b/src/yuzu/configuration/configure_debug.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index e99657bd6..4301313cf 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> #include "common/logging/log.h" @@ -29,9 +28,10 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, InputCommon::InputSubsystem* input_subsystem, - Core::System& system_) - : QDialog(parent), ui{std::make_unique<Ui::ConfigureDialog>()}, registry{registry_}, - system{system_}, audio_tab{std::make_unique<ConfigureAudio>(system_, this)}, + Core::System& system_, bool enable_web_config) + : QDialog(parent), ui{std::make_unique<Ui::ConfigureDialog>()}, + registry(registry_), system{system_}, audio_tab{std::make_unique<ConfigureAudio>(system_, + this)}, cpu_tab{std::make_unique<ConfigureCpu>(system_, this)}, debug_tab_tab{std::make_unique<ConfigureDebugTab>(system_, this)}, filesystem_tab{std::make_unique<ConfigureFilesystem>(this)}, @@ -64,6 +64,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, ui->tabWidget->addTab(ui_tab.get(), tr("Game List")); ui->tabWidget->addTab(web_tab.get(), tr("Web")); + web_tab->SetWebServiceConfigEnabled(enable_web_config); hotkeys_tab->Populate(registry); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h index 12cf25daf..1f724834a 100644 --- a/src/yuzu/configuration/configure_dialog.h +++ b/src/yuzu/configuration/configure_dialog.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -41,7 +40,8 @@ class ConfigureDialog : public QDialog { public: explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, - InputCommon::InputSubsystem* input_subsystem, Core::System& system_); + InputCommon::InputSubsystem* input_subsystem, Core::System& system_, + bool enable_web_config = true); ~ConfigureDialog() override; void ApplyConfiguration(); diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index 2a446205b..7ade01ba6 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <functional> #include <utility> diff --git a/src/yuzu/configuration/configure_general.h b/src/yuzu/configuration/configure_general.h index b6f3bb5ed..a090c1a3f 100644 --- a/src/yuzu/configuration/configure_general.h +++ b/src/yuzu/configuration/configure_general.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 85f34dc35..87e5d0f48 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later // Include this early to include Vulkan headers how we want to #include "video_core/vulkan_common/vulkan_wrapper.h" @@ -58,24 +57,9 @@ ConfigureGraphics::ConfigureGraphics(const Core::System& system_, QWidget* paren UpdateBackgroundColorButton(new_bg_color); }); - connect(ui->button_check_vulkan, &QAbstractButton::clicked, this, [this] { - UISettings::values.has_broken_vulkan = false; - - if (RetrieveVulkanDevices()) { - ui->api->setEnabled(true); - ui->button_check_vulkan->hide(); - - for (const auto& device : vulkan_devices) { - ui->device->addItem(device); - } - } else { - UISettings::values.has_broken_vulkan = true; - } - }); - - ui->api->setEnabled(!UISettings::values.has_broken_vulkan.GetValue()); - ui->button_check_vulkan->setVisible(UISettings::values.has_broken_vulkan.GetValue()); - + ui->api->setEnabled(!UISettings::values.has_broken_vulkan); + ui->api_widget->setEnabled(!UISettings::values.has_broken_vulkan || + Settings::IsConfiguringGlobal()); ui->bg_label->setVisible(Settings::IsConfiguringGlobal()); ui->bg_combobox->setVisible(!Settings::IsConfiguringGlobal()); } @@ -315,7 +299,7 @@ void ConfigureGraphics::UpdateAPILayout() { vulkan_device = Settings::values.vulkan_device.GetValue(true); shader_backend = Settings::values.shader_backend.GetValue(true); ui->device_widget->setEnabled(false); - ui->backend_widget->setEnabled(UISettings::values.has_broken_vulkan.GetValue()); + ui->backend_widget->setEnabled(false); } else { vulkan_device = Settings::values.vulkan_device.GetValue(); shader_backend = Settings::values.shader_backend.GetValue(); @@ -337,9 +321,9 @@ void ConfigureGraphics::UpdateAPILayout() { } } -bool ConfigureGraphics::RetrieveVulkanDevices() try { +void ConfigureGraphics::RetrieveVulkanDevices() try { if (UISettings::values.has_broken_vulkan) { - return false; + return; } using namespace Vulkan; @@ -355,11 +339,8 @@ bool ConfigureGraphics::RetrieveVulkanDevices() try { const std::string name = vk::PhysicalDevice(device, dld).GetProperties().deviceName; vulkan_devices.push_back(QString::fromStdString(name)); } - - return true; } catch (const Vulkan::vk::Exception& exception) { LOG_ERROR(Frontend, "Failed to enumerate devices with error: {}", exception.what()); - return false; } Settings::RendererBackend ConfigureGraphics::GetCurrentGraphicsBackend() const { @@ -440,11 +421,4 @@ void ConfigureGraphics::SetupPerGameUI() { ui->api, static_cast<int>(Settings::values.renderer_backend.GetValue(true))); ConfigurationShared::InsertGlobalItem( ui->nvdec_emulation, static_cast<int>(Settings::values.nvdec_emulation.GetValue(true))); - - if (UISettings::values.has_broken_vulkan) { - ui->backend_widget->setEnabled(true); - ConfigurationShared::SetColoredComboBox( - ui->backend, ui->backend_widget, - static_cast<int>(Settings::values.shader_backend.GetValue(true))); - } } diff --git a/src/yuzu/configuration/configure_graphics.h b/src/yuzu/configuration/configure_graphics.h index 8438f0187..70034eb1b 100644 --- a/src/yuzu/configuration/configure_graphics.h +++ b/src/yuzu/configuration/configure_graphics.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -41,7 +40,7 @@ private: void UpdateDeviceSelection(int device); void UpdateShaderBackendSelection(int backend); - bool RetrieveVulkanDevices(); + void RetrieveVulkanDevices(); void SetupPerGameUI(); diff --git a/src/yuzu/configuration/configure_graphics.ui b/src/yuzu/configuration/configure_graphics.ui index 2f94c94bc..1e4f74704 100644 --- a/src/yuzu/configuration/configure_graphics.ui +++ b/src/yuzu/configuration/configure_graphics.ui @@ -6,7 +6,7 @@ <rect> <x>0</x> <y>0</y> - <width>471</width> + <width>541</width> <height>759</height> </rect> </property> @@ -574,13 +574,6 @@ </property> </spacer> </item> - <item> - <widget class="QPushButton" name="button_check_vulkan"> - <property name="text"> - <string>Check for Working Vulkan</string> - </property> - </widget> - </item> </layout> </widget> <resources/> diff --git a/src/yuzu/configuration/configure_hotkeys.cpp b/src/yuzu/configuration/configure_hotkeys.cpp index edf0893c4..daa77a8f8 100644 --- a/src/yuzu/configuration/configure_hotkeys.cpp +++ b/src/yuzu/configuration/configure_hotkeys.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QMenu> #include <QMessageBox> diff --git a/src/yuzu/configuration/configure_hotkeys.h b/src/yuzu/configuration/configure_hotkeys.h index f943ec538..b45ecb185 100644 --- a/src/yuzu/configuration/configure_hotkeys.h +++ b/src/yuzu/configuration/configure_hotkeys.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp index f1b061b13..16fba3deb 100644 --- a/src/yuzu/configuration/configure_input.cpp +++ b/src/yuzu/configuration/configure_input.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> #include <thread> diff --git a/src/yuzu/configuration/configure_input.h b/src/yuzu/configuration/configure_input.h index 4cafa3dab..c89189c36 100644 --- a/src/yuzu/configuration/configure_input.h +++ b/src/yuzu/configuration/configure_input.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index f3be9a374..00bee85b2 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <memory> diff --git a/src/yuzu/configuration/configure_input_player.h b/src/yuzu/configuration/configure_input_player.h index 47df6b3d3..79434fdd8 100644 --- a/src/yuzu/configuration/configure_input_player.h +++ b/src/yuzu/configuration/configure_input_player.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_motion_touch.cpp b/src/yuzu/configuration/configure_motion_touch.cpp index c313b0919..d1b870c72 100644 --- a/src/yuzu/configuration/configure_motion_touch.cpp +++ b/src/yuzu/configuration/configure_motion_touch.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <sstream> diff --git a/src/yuzu/configuration/configure_motion_touch.h b/src/yuzu/configuration/configure_motion_touch.h index 91d1ae671..7dcc9318e 100644 --- a/src/yuzu/configuration/configure_motion_touch.h +++ b/src/yuzu/configuration/configure_motion_touch.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_network.cpp b/src/yuzu/configuration/configure_network.cpp index 8ed08fa6a..ba1986eb1 100644 --- a/src/yuzu/configuration/configure_network.cpp +++ b/src/yuzu/configuration/configure_network.cpp @@ -4,7 +4,7 @@ #include <QtConcurrent/QtConcurrent> #include "common/settings.h" #include "core/core.h" -#include "core/network/network_interface.h" +#include "core/internal_network/network_interface.h" #include "ui_configure_network.h" #include "yuzu/configuration/configure_network.h" diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index 4906997ab..674a75a62 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <memory> diff --git a/src/yuzu/configuration/configure_per_game_addons.h b/src/yuzu/configuration/configure_per_game_addons.h index 14690fba8..53db405c1 100644 --- a/src/yuzu/configuration/configure_per_game_addons.h +++ b/src/yuzu/configuration/configure_per_game_addons.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_profile_manager.cpp b/src/yuzu/configuration/configure_profile_manager.cpp index 5442fe328..5c0217ba8 100644 --- a/src/yuzu/configuration/configure_profile_manager.cpp +++ b/src/yuzu/configuration/configure_profile_manager.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <algorithm> #include <QFileDialog> diff --git a/src/yuzu/configuration/configure_profile_manager.h b/src/yuzu/configuration/configure_profile_manager.h index 575cb89d5..fe9033779 100644 --- a/src/yuzu/configuration/configure_profile_manager.h +++ b/src/yuzu/configuration/configure_profile_manager.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp index ecebb0fb7..bc9d9d77a 100644 --- a/src/yuzu/configuration/configure_system.cpp +++ b/src/yuzu/configuration/configure_system.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <chrono> #include <optional> diff --git a/src/yuzu/configuration/configure_system.h b/src/yuzu/configuration/configure_system.h index 5a1633192..8f02880a7 100644 --- a/src/yuzu/configuration/configure_system.h +++ b/src/yuzu/configuration/configure_system.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_touch_from_button.cpp b/src/yuzu/configuration/configure_touch_from_button.cpp index 06cc452c3..18e2eba69 100644 --- a/src/yuzu/configuration/configure_touch_from_button.cpp +++ b/src/yuzu/configuration/configure_touch_from_button.cpp @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QInputDialog> #include <QKeyEvent> diff --git a/src/yuzu/configuration/configure_touch_from_button.h b/src/yuzu/configuration/configure_touch_from_button.h index b8c55db66..5a1416d00 100644 --- a/src/yuzu/configuration/configure_touch_from_button.h +++ b/src/yuzu/configuration/configure_touch_from_button.h @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_touch_widget.h b/src/yuzu/configuration/configure_touch_widget.h index 347b46583..49f533afe 100644 --- a/src/yuzu/configuration/configure_touch_widget.h +++ b/src/yuzu/configuration/configure_touch_widget.h @@ -1,6 +1,5 @@ -// Copyright 2020 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2020 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_touchscreen_advanced.cpp b/src/yuzu/configuration/configure_touchscreen_advanced.cpp index 29c86c7bc..5a03e48df 100644 --- a/src/yuzu/configuration/configure_touchscreen_advanced.cpp +++ b/src/yuzu/configuration/configure_touchscreen_advanced.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> #include "ui_configure_touchscreen_advanced.h" diff --git a/src/yuzu/configuration/configure_touchscreen_advanced.h b/src/yuzu/configuration/configure_touchscreen_advanced.h index 72061492c..034dc0d46 100644 --- a/src/yuzu/configuration/configure_touchscreen_advanced.h +++ b/src/yuzu/configuration/configure_touchscreen_advanced.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp index d3a60cdd1..2e98ede8e 100644 --- a/src/yuzu/configuration/configure_ui.cpp +++ b/src/yuzu/configuration/configure_ui.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <utility> diff --git a/src/yuzu/configuration/configure_ui.h b/src/yuzu/configuration/configure_ui.h index 48b6e6d82..95af8370e 100644 --- a/src/yuzu/configuration/configure_ui.h +++ b/src/yuzu/configuration/configure_ui.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp index d779251b4..d668c992b 100644 --- a/src/yuzu/configuration/configure_web.cpp +++ b/src/yuzu/configuration/configure_web.cpp @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QIcon> #include <QMessageBox> @@ -169,3 +168,8 @@ void ConfigureWeb::OnLoginVerified() { "correctly, and that your internet connection is working.")); } } + +void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) { + ui->label_disable_info->setVisible(!enabled); + ui->groupBoxWebConfig->setEnabled(enabled); +} diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h index 9054711ea..03feb55f8 100644 --- a/src/yuzu/configuration/configure_web.h +++ b/src/yuzu/configuration/configure_web.h @@ -1,6 +1,5 @@ -// Copyright 2017 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -20,6 +19,7 @@ public: ~ConfigureWeb() override; void ApplyConfiguration(); + void SetWebServiceConfigEnabled(bool enabled); private: void changeEvent(QEvent* event) override; diff --git a/src/yuzu/configuration/configure_web.ui b/src/yuzu/configuration/configure_web.ui index 35b4274b0..3ac3864be 100644 --- a/src/yuzu/configuration/configure_web.ui +++ b/src/yuzu/configuration/configure_web.ui @@ -113,6 +113,16 @@ </widget> </item> <item> + <widget class="QLabel" name="label_disable_info"> + <property name="text"> + <string>Web Service configuration can only be changed when a public room isn't being hosted.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> <widget class="QGroupBox" name="groupBox"> <property name="title"> <string>Telemetry</string> diff --git a/src/yuzu/debugger/controller.cpp b/src/yuzu/debugger/controller.cpp index 6b834c42e..e4bf16a04 100644 --- a/src/yuzu/debugger/controller.cpp +++ b/src/yuzu/debugger/controller.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QAction> #include <QLayout> diff --git a/src/yuzu/debugger/controller.h b/src/yuzu/debugger/controller.h index 52cea3326..9651dfaa9 100644 --- a/src/yuzu/debugger/controller.h +++ b/src/yuzu/debugger/controller.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/debugger/profiler.cpp b/src/yuzu/debugger/profiler.cpp index 33110685a..d3e2d3c12 100644 --- a/src/yuzu/debugger/profiler.cpp +++ b/src/yuzu/debugger/profiler.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QAction> #include <QLayout> diff --git a/src/yuzu/debugger/profiler.h b/src/yuzu/debugger/profiler.h index 8e69fdb06..4c8ccd3c2 100644 --- a/src/yuzu/debugger/profiler.h +++ b/src/yuzu/debugger/profiler.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp index 0ea31cd33..7f7c5fc42 100644 --- a/src/yuzu/debugger/wait_tree.cpp +++ b/src/yuzu/debugger/wait_tree.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <fmt/format.h> diff --git a/src/yuzu/debugger/wait_tree.h b/src/yuzu/debugger/wait_tree.h index f21b9f467..7e528b592 100644 --- a/src/yuzu/debugger/wait_tree.h +++ b/src/yuzu/debugger/wait_tree.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/discord.h b/src/yuzu/discord.h index a867cc4d6..e08784498 100644 --- a/src/yuzu/discord.h +++ b/src/yuzu/discord.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/discord_impl.cpp b/src/yuzu/discord_impl.cpp index 66f928af6..c351e9b83 100644 --- a/src/yuzu/discord_impl.cpp +++ b/src/yuzu/discord_impl.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <chrono> #include <string> diff --git a/src/yuzu/discord_impl.h b/src/yuzu/discord_impl.h index 03ad42681..84710b9c6 100644 --- a/src/yuzu/discord_impl.h +++ b/src/yuzu/discord_impl.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 05d309827..041e6ac11 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <regex> #include <QApplication> @@ -499,6 +498,8 @@ void GameList::DonePopulating(const QStringList& watch_list) { } item_model->sort(tree_view->header()->sortIndicatorSection(), tree_view->header()->sortIndicatorOrder()); + + emit PopulatingCompleted(); } void GameList::PopupContextMenu(const QPoint& menu_location) { @@ -752,6 +753,10 @@ void GameList::LoadCompatibilityList() { } } +QStandardItemModel* GameList::GetModel() const { + return item_model; +} + void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { tree_view->setEnabled(false); diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index bc36d015a..f783283c9 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -16,9 +15,14 @@ #include <QWidget> #include "common/common_types.h" +#include "core/core.h" #include "uisettings.h" #include "yuzu/compatibility_list.h" +namespace Core { +class System; +} + class ControllerNavigation; class GameListWorker; class GameListSearchField; @@ -84,6 +88,8 @@ public: void SaveInterfaceLayout(); void LoadInterfaceLayout(); + QStandardItemModel* GetModel() const; + /// Disables events from the emulated controller void UnloadController(); @@ -108,6 +114,7 @@ signals: void OpenDirectory(const QString& directory); void AddDirectory(); void ShowList(bool show); + void PopulatingCompleted(); private slots: void OnItemExpanded(const QModelIndex& item); diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index cd7d63536..e7667cf60 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/hotkeys.cpp b/src/yuzu/hotkeys.cpp index d59aa5d18..13723f6e5 100644 --- a/src/yuzu/hotkeys.cpp +++ b/src/yuzu/hotkeys.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <sstream> #include <QShortcut> diff --git a/src/yuzu/hotkeys.h b/src/yuzu/hotkeys.h index 57a7c7da5..dc5b7f628 100644 --- a/src/yuzu/hotkeys.h +++ b/src/yuzu/hotkeys.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 08ccc1555..ef91ef19c 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <cinttypes> #include <clocale> @@ -9,6 +8,10 @@ #ifdef __APPLE__ #include <unistd.h> // for chdir #endif +#ifdef __linux__ +#include <csignal> +#include <sys/socket.h> +#endif // VFS includes must be before glad as they will conflict with Windows file api, which uses defines. #include "applets/qt_controller.h" @@ -32,6 +35,7 @@ #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" +#include "yuzu/multiplayer/state.h" #include "yuzu/util/controller_navigation.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows @@ -115,7 +119,6 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "video_core/shader_notify.h" #include "yuzu/about_dialog.h" #include "yuzu/bootmanager.h" -#include "yuzu/check_vulkan.h" #include "yuzu/compatdb.h" #include "yuzu/compatibility_list.h" #include "yuzu/configuration/config.h" @@ -131,7 +134,9 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/install_dialog.h" #include "yuzu/loading_screen.h" #include "yuzu/main.h" +#include "yuzu/startup_checks.h" #include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" using namespace Common::Literals; @@ -252,12 +257,16 @@ static QString PrettyProductName() { return QSysInfo::prettyProductName(); } -GMainWindow::GMainWindow() +GMainWindow::GMainWindow(bool has_broken_vulkan) : ui{std::make_unique<Ui::MainWindow>()}, system{std::make_unique<Core::System>()}, input_subsystem{std::make_shared<InputCommon::InputSubsystem>()}, config{std::make_unique<Config>(*system)}, vfs{std::make_shared<FileSys::RealVfsFilesystem>()}, provider{std::make_unique<FileSys::ManualContentProvider>()} { +#ifdef __linux__ + SetupSigInterrupts(); +#endif + Common::Log::Initialize(); LoadTranslation(); @@ -271,6 +280,8 @@ GMainWindow::GMainWindow() SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); + system->GetRoomNetwork().Init(); + RegisterMetaTypes(); InitializeWidgets(); @@ -352,17 +363,15 @@ GMainWindow::GMainWindow() MigrateConfigFiles(); - if (!CheckVulkan()) { - config->Save(); + if (has_broken_vulkan) { + UISettings::values.has_broken_vulkan = true; + + QMessageBox::warning(this, tr("Broken Vulkan Installation Detected"), + tr("Vulkan initialization failed during boot.<br><br>Click <a " + "href='https://yuzu-emu.org/wiki/faq/" + "#yuzu-starts-with-the-error-broken-vulkan-installation-detected'>" + "here for instructions to fix the issue</a>.")); - QMessageBox::warning( - this, tr("Broken Vulkan Installation Detected"), - tr("Vulkan initialization failed on the previous boot.<br><br>Click <a " - "href='https://yuzu-emu.org/wiki/faq/" - "#yuzu-starts-with-the-error-broken-vulkan-installation-detected'>here for " - "instructions to fix the issue</a>.")); - } - if (UISettings::values.has_broken_vulkan) { Settings::values.renderer_backend = Settings::RendererBackend::OpenGL; renderer_status_button->setDisabled(true); @@ -461,6 +470,13 @@ GMainWindow::~GMainWindow() { if (render_window->parent() == nullptr) { delete render_window; } + + system->GetRoomNetwork().Shutdown(); + +#ifdef __linux__ + ::close(sig_interrupt_fds[0]); + ::close(sig_interrupt_fds[1]); +#endif } void GMainWindow::RegisterMetaTypes() { @@ -824,6 +840,10 @@ void GMainWindow::InitializeWidgets() { } }); + multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room, + ui->action_Show_Room, system->GetRoomNetwork()); + multiplayer_state->setVisible(false); + // Create status bar message_label = new QLabel(); // Configured separately for left alignment @@ -856,6 +876,10 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(label); } + // TODO (flTobi): Add the widget when multiplayer is fully implemented + // statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0); + // statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0); + tas_label = new QLabel(); tas_label->setObjectName(QStringLiteral("TASlabel")); tas_label->setFocusPolicy(Qt::NoFocus); @@ -1165,6 +1189,8 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); + connect(game_list, &GameList::PopulatingCompleted, + [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); @@ -1182,6 +1208,9 @@ void GMainWindow::ConnectWidgetEvents() { connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); + + connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, + &MultiplayerState::UpdateThemedIcons); } void GMainWindow::ConnectMenuEvents() { @@ -1225,6 +1254,18 @@ void GMainWindow::ConnectMenuEvents() { ui->action_Reset_Window_Size_900, ui->action_Reset_Window_Size_1080}); + // Multiplayer + connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnViewLobby); + connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCreateRoom); + connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnCloseRoom); + connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnDirectConnectToRoom); + connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, + &MultiplayerState::OnOpenNetworkRoom); + // Tools connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this, ReinitializeKeyBehavior::Warning)); @@ -1325,6 +1366,52 @@ static void ReleaseWakeLockLinux(QDBusObjectPath lock) { QString::fromLatin1("org.freedesktop.portal.Request")); unlocker.call(QString::fromLatin1("Close")); } + +std::array<int, 3> GMainWindow::sig_interrupt_fds{0, 0, 0}; + +void GMainWindow::SetupSigInterrupts() { + if (sig_interrupt_fds[2] == 1) { + return; + } + socketpair(AF_UNIX, SOCK_STREAM, 0, sig_interrupt_fds.data()); + sig_interrupt_fds[2] = 1; + + struct sigaction sa; + sa.sa_handler = &GMainWindow::HandleSigInterrupt; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESETHAND; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); + + sig_interrupt_notifier = new QSocketNotifier(sig_interrupt_fds[1], QSocketNotifier::Read, this); + connect(sig_interrupt_notifier, &QSocketNotifier::activated, this, + &GMainWindow::OnSigInterruptNotifierActivated); + connect(this, &GMainWindow::SigInterrupt, this, &GMainWindow::close); +} + +void GMainWindow::HandleSigInterrupt(int sig) { + if (sig == SIGINT) { + exit(1); + } + + // Calling into Qt directly from a signal handler is not safe, + // so wake up a QSocketNotifier with this hacky write call instead. + char a = 1; + int ret = write(sig_interrupt_fds[0], &a, sizeof(a)); + (void)ret; +} + +void GMainWindow::OnSigInterruptNotifierActivated() { + sig_interrupt_notifier->setEnabled(false); + + char a; + int ret = read(sig_interrupt_fds[1], &a, sizeof(a)); + (void)ret; + + sig_interrupt_notifier->setEnabled(true); + + emit SigInterrupt(); +} #endif // __linux__ void GMainWindow::PreventOSSleep() { @@ -2785,7 +2872,8 @@ void GMainWindow::OnConfigure() { const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); Settings::SetConfiguringGlobal(true); - ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system); + ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system, + !multiplayer_state->IsHostingPublicRoom()); connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, &GMainWindow::OnLanguageChanged); @@ -2842,6 +2930,11 @@ void GMainWindow::OnConfigure() { if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); } + + if (!multiplayer_state->IsHostingPublicRoom()) { + multiplayer_state->UpdateCredentials(); + } + emit UpdateThemedIcons(); const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); @@ -3662,6 +3755,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) { } render_window->close(); + multiplayer_state->Close(); QWidget::closeEvent(event); } @@ -3858,6 +3952,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) { UISettings::values.language = locale; LoadTranslation(); ui->retranslateUi(this); + multiplayer_state->retranslateUi(); UpdateWindowTitle(); } @@ -3879,6 +3974,11 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { #endif int main(int argc, char* argv[]) { + bool has_broken_vulkan = false; + if (StartupChecks(argv[0], &has_broken_vulkan)) { + return 0; + } + Common::DetachedTasks detached_tasks; MicroProfileOnThreadCreate("Frontend"); SCOPE_EXIT({ MicroProfileShutdown(); }); @@ -3918,7 +4018,7 @@ int main(int argc, char* argv[]) { // generating shaders setlocale(LC_ALL, "C"); - GMainWindow main_window{}; + GMainWindow main_window{has_broken_vulkan}; // After settings have been loaded by GMainWindow, apply the filter main_window.show(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 09e37f152..509bb91df 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -11,6 +10,7 @@ #include <QTimer> #include <QTranslator> +#include "common/announce_multiplayer_room.h" #include "common/common_types.h" #include "yuzu/compatibility_list.h" #include "yuzu/hotkeys.h" @@ -22,6 +22,7 @@ #endif class Config; +class ClickableLabel; class EmuThread; class GameList; class GImageInfo; @@ -31,6 +32,7 @@ class MicroProfileDialog; class ProfilerWidget; class ControllerDialog; class QLabel; +class MultiplayerState; class QPushButton; class QProgressDialog; class WaitTreeWidget; @@ -118,7 +120,7 @@ class GMainWindow : public QMainWindow { public: void filterBarSetChecked(bool state); void UpdateUITheme(); - explicit GMainWindow(); + explicit GMainWindow(bool has_broken_vulkan); ~GMainWindow() override; bool DropAction(QDropEvent* event); @@ -161,6 +163,8 @@ signals: void WebBrowserExtractOfflineRomFS(); void WebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, std::string last_url); + void SigInterrupt(); + public slots: void OnLoadComplete(); void OnExecuteProgram(std::size_t program_index); @@ -200,6 +204,8 @@ private: void ConnectMenuEvents(); void UpdateMenuState(); + MultiplayerState* multiplayer_state = nullptr; + void PreventOSSleep(); void AllowOSSleep(); @@ -247,6 +253,12 @@ private: void RequestGameResume(); void closeEvent(QCloseEvent* event) override; +#ifdef __linux__ + void SetupSigInterrupts(); + static void HandleSigInterrupt(int); + void OnSigInterruptNotifierActivated(); +#endif + private slots: void OnStartGame(); void OnRestartGame(); @@ -415,6 +427,9 @@ private: bool is_tas_recording_dialog_active{}; #ifdef __linux__ + QSocketNotifier* sig_interrupt_notifier; + static std::array<int, 3> sig_interrupt_fds; + QDBusObjectPath wake_lock{}; #endif diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 6ab95b9a5..cdf31b417 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -154,6 +154,7 @@ <addaction name="menu_Emulation"/> <addaction name="menu_View"/> <addaction name="menu_Tools"/> + <addaction name="menu_Multiplayer"/> <addaction name="menu_Help"/> </widget> <action name="action_Install_File_NAND"> @@ -245,6 +246,43 @@ <string>Show Status Bar</string> </property> </action> + <action name="action_View_Lobby"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Browse Public Game Lobby</string> + </property> + </action> + <action name="action_Start_Room"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Create Room</string> + </property> + </action> + <action name="action_Leave_Room"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Leave Room</string> + </property> + </action> + <action name="action_Connect_To_Room"> + <property name="text"> + <string>Direct Connect to Room</string> + </property> + </action> + <action name="action_Show_Room"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Show Current Room</string> + </property> + </action> <action name="action_Fullscreen"> <property name="checkable"> <bool>true</bool> diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp new file mode 100644 index 000000000..5837b36ab --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.cpp @@ -0,0 +1,491 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <array> +#include <future> +#include <QColor> +#include <QDesktopServices> +#include <QFutureWatcher> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMenu> +#include <QMessageBox> +#include <QMetaType> +#include <QTime> +#include <QUrl> +#include <QtConcurrent/QtConcurrentRun> +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_chat_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/message.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +class ChatMessage { +public: + explicit ChatMessage(const Network::ChatEntry& chat, Network::RoomNetwork& room_network, + QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + nickname = QString::fromStdString(chat.nickname); + username = QString::fromStdString(chat.username); + message = QString::fromStdString(chat.message); + + // Check for user pings + QString cur_nickname, cur_username; + if (auto room = room_network.GetRoomMember().lock()) { + cur_nickname = QString::fromStdString(room->GetNickname()); + cur_username = QString::fromStdString(room->GetUsername()); + } + + // Handle pings at the beginning and end of message + QString fixed_message = QStringLiteral(" %1 ").arg(message); + if (fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_nickname)) || + (!cur_username.isEmpty() && + fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_username)))) { + + contains_ping = true; + } else { + contains_ping = false; + } + } + + bool ContainsPing() const { + return contains_ping; + } + + /// Format the message using the players color + QString GetPlayerChatMessage(u16 player) const { + auto color = player_color[player % 16]; + QString name; + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + + QString style, text_color; + if (ContainsPing()) { + // Add a background color to these messages + style = QStringLiteral("background-color: %1").arg(QString::fromStdString(ping_color)); + // Add a font color + text_color = QStringLiteral("color='#000000'"); + } + + return QStringLiteral("[%1] <font color='%2'><%3></font> <font style='%4' " + "%5>%6</font>") + .arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color, + message.toHtmlEscaped()); + } + +private: + static constexpr std::array<const char*, 16> player_color = { + {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222", + "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}}; + static constexpr char ping_color[] = "#FFFF00"; + + QString timestamp; + QString nickname; + QString username; + QString message; + bool contains_ping; +}; + +class StatusMessage { +public: + explicit StatusMessage(const QString& msg, QTime ts = {}) { + /// Convert the time to their default locale defined format + QLocale locale; + timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat); + message = msg; + } + + QString GetSystemChatMessage() const { + return QStringLiteral("[%1] <font color='%2'>* %3</font>") + .arg(timestamp, QString::fromStdString(system_color), message); + } + +private: + static constexpr const char system_color[] = "#FF8C00"; + QString timestamp; + QString message; +}; + +class PlayerListItem : public QStandardItem { +public: + static const int NicknameRole = Qt::UserRole + 1; + static const int UsernameRole = Qt::UserRole + 2; + static const int AvatarUrlRole = Qt::UserRole + 3; + static const int GameNameRole = Qt::UserRole + 4; + + PlayerListItem() = default; + explicit PlayerListItem(const std::string& nickname, const std::string& username, + const std::string& avatar_url, const std::string& game_name) { + setEditable(false); + setData(QString::fromStdString(nickname), NicknameRole); + setData(QString::fromStdString(username), UsernameRole); + setData(QString::fromStdString(avatar_url), AvatarUrlRole); + if (game_name.empty()) { + setData(QObject::tr("Not playing a game"), GameNameRole); + } else { + setData(QString::fromStdString(game_name), GameNameRole); + } + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return QStandardItem::data(role); + } + QString name; + const QString nickname = data(NicknameRole).toString(); + const QString username = data(UsernameRole).toString(); + if (username.isEmpty() || username == nickname) { + name = nickname; + } else { + name = QStringLiteral("%1 (%2)").arg(nickname, username); + } + return QStringLiteral("%1\n %2").arg(name, data(GameNameRole).toString()); + } +}; + +ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) { + ui->setupUi(this); + + // set the item_model for player_view + + player_list = new QStandardItemModel(ui->player_view); + ui->player_view->setModel(player_list); + ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu); + // set a header to make it look better though there is only one column + player_list->insertColumns(0, 1); + player_list->setHeaderData(0, Qt::Horizontal, tr("Members")); + + ui->chat_history->document()->setMaximumBlockCount(max_chat_lines); + + // register the network structs to use in slots and signals + qRegisterMetaType<Network::ChatEntry>(); + qRegisterMetaType<Network::StatusMessageEntry>(); + qRegisterMetaType<Network::RoomInformation>(); + qRegisterMetaType<Network::RoomMember::State>(); + + // Connect all the widgets to the appropriate events + connect(ui->player_view, &QTreeView::customContextMenuRequested, this, + &ChatRoom::PopupContextMenu); + connect(ui->chat_message, &QLineEdit::returnPressed, this, &ChatRoom::OnSendChat); + connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged); + connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat); +} + +ChatRoom::~ChatRoom() = default; + +void ChatRoom::Initialize(Network::RoomNetwork* room_network_) { + room_network = room_network_; + // setup the callbacks for network updates + if (auto member = room_network->GetRoomMember().lock()) { + member->BindOnChatMessageRecieved( + [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); }); + member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive); + connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive); + } +} + +void ChatRoom::SetModPerms(bool is_mod) { + has_mod_perms = is_mod; +} + +void ChatRoom::RetranslateUi() { + ui->retranslateUi(this); +} + +void ChatRoom::Clear() { + ui->chat_history->clear(); + block_list.clear(); +} + +void ChatRoom::AppendStatusMessage(const QString& msg) { + ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage()); +} + +void ChatRoom::AppendChatMessage(const QString& msg) { + ui->chat_history->append(msg); +} + +void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) { + if (auto room = room_network->GetRoomMember().lock()) { + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&nickname](const Network::RoomMember::MemberInformation& member) { + return member.nickname == nickname; + }); + if (it == members.end()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + return; + } + room->SendModerationRequest(type, nickname); + } +} + +bool ChatRoom::ValidateMessage(const std::string& msg) { + return !msg.empty(); +} + +void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) { + // TODO(B3N30): change title + if (auto room_member = room_network->GetRoomMember().lock()) { + SetPlayerList(room_member->GetMemberInformation()); + } +} + +void ChatRoom::Disable() { + ui->send_message->setDisabled(true); + ui->chat_message->setDisabled(true); +} + +void ChatRoom::Enable() { + ui->send_message->setEnabled(true); + ui->chat_message->setEnabled(true); +} + +void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) { + if (!ValidateMessage(chat.message)) { + return; + } + if (auto room = room_network->GetRoomMember().lock()) { + // get the id of the player + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Chat message received from unknown player. Ignoring it."); + return; + } + if (block_list.count(chat.nickname)) { + LOG_INFO(Network, "Chat message received from blocked player {}. Ignoring it.", + chat.nickname); + return; + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat, *room_network); + if (m.ContainsPing()) { + emit UserPinged(); + } + AppendChatMessage(m.GetPlayerChatMessage(player)); + } +} + +void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) { + QString name; + if (status_message.username.empty() || status_message.username == status_message.nickname) { + name = QString::fromStdString(status_message.nickname); + } else { + name = QStringLiteral("%1 (%2)").arg(QString::fromStdString(status_message.nickname), + QString::fromStdString(status_message.username)); + } + QString message; + switch (status_message.type) { + case Network::IdMemberJoin: + message = tr("%1 has joined").arg(name); + break; + case Network::IdMemberLeave: + message = tr("%1 has left").arg(name); + break; + case Network::IdMemberKicked: + message = tr("%1 has been kicked").arg(name); + break; + case Network::IdMemberBanned: + message = tr("%1 has been banned").arg(name); + break; + case Network::IdAddressUnbanned: + message = tr("%1 has been unbanned").arg(name); + break; + } + if (!message.isEmpty()) + AppendStatusMessage(message); +} + +void ChatRoom::OnSendChat() { + if (auto room = room_network->GetRoomMember().lock()) { + if (room->GetState() != Network::RoomMember::State::Joined && + room->GetState() != Network::RoomMember::State::Moderator) { + + return; + } + auto message = ui->chat_message->text().toStdString(); + if (!ValidateMessage(message)) { + return; + } + auto nick = room->GetNickname(); + auto username = room->GetUsername(); + Network::ChatEntry chat{nick, username, message}; + + auto members = room->GetMemberInformation(); + auto it = std::find_if(members.begin(), members.end(), + [&chat](const Network::RoomMember::MemberInformation& member) { + return member.nickname == chat.nickname && + member.username == chat.username; + }); + if (it == members.end()) { + LOG_INFO(Network, "Cannot find self in the player list when sending a message."); + } + auto player = std::distance(members.begin(), it); + ChatMessage m(chat, *room_network); + room->SendChatMessage(message); + AppendChatMessage(m.GetPlayerChatMessage(player)); + ui->chat_message->clear(); + } +} + +void ChatRoom::UpdateIconDisplay() { + for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) { + QStandardItem* item = player_list->invisibleRootItem()->child(row); + const std::string avatar_url = + item->data(PlayerListItem::AvatarUrlRole).toString().toStdString(); + if (icon_cache.count(avatar_url)) { + item->setData(icon_cache.at(avatar_url), Qt::DecorationRole); + } else { + item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48), + Qt::DecorationRole); + } + } +} + +void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) { + // TODO(B3N30): Remember which row is selected + player_list->removeRows(0, player_list->rowCount()); + for (const auto& member : member_list) { + if (member.nickname.empty()) + continue; + QStandardItem* name_item = new PlayerListItem(member.nickname, member.username, + member.avatar_url, member.game_info.name); + +#ifdef ENABLE_WEB_SERVICE + if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) { + // Start a request to get the member's avatar + const QUrl url(QString::fromStdString(member.avatar_url)); + QFuture<std::string> future = QtConcurrent::run([url] { + WebService::Client client( + QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", ""); + auto result = client.GetImage(url.path().toStdString(), true); + if (result.returned_data.empty()) { + LOG_ERROR(WebService, "Failed to get avatar"); + } + return result.returned_data; + }); + auto* future_watcher = new QFutureWatcher<std::string>(this); + connect(future_watcher, &QFutureWatcher<std::string>::finished, this, + [this, future_watcher, avatar_url = member.avatar_url] { + const std::string result = future_watcher->result(); + if (result.empty()) + return; + QPixmap pixmap; + if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()), + static_cast<uint>(result.size()))) + return; + icon_cache[avatar_url] = + pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + // Update all the displayed icons with the new icon_cache + UpdateIconDisplay(); + }); + future_watcher->setFuture(future); + } +#endif + + player_list->invisibleRootItem()->appendRow(name_item); + } + UpdateIconDisplay(); + // TODO(B3N30): Restore row selection +} + +void ChatRoom::OnChatTextChanged() { + if (ui->chat_message->text().length() > static_cast<int>(Network::MaxMessageSize)) + ui->chat_message->setText( + ui->chat_message->text().left(static_cast<int>(Network::MaxMessageSize))); +} + +void ChatRoom::PopupContextMenu(const QPoint& menu_location) { + QModelIndex item = ui->player_view->indexAt(menu_location); + if (!item.isValid()) + return; + + std::string nickname = + player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString(); + + QMenu context_menu; + + QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString(); + if (!username.isEmpty()) { + QAction* view_profile_action = context_menu.addAction(tr("View Profile")); + connect(view_profile_action, &QAction::triggered, [username] { + QDesktopServices::openUrl( + QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username))); + }); + } + + std::string cur_nickname; + if (auto room = room_network->GetRoomMember().lock()) { + cur_nickname = room->GetNickname(); + } + + if (nickname != cur_nickname) { // You can't block yourself + QAction* block_action = context_menu.addAction(tr("Block Player")); + + block_action->setCheckable(true); + block_action->setChecked(block_list.count(nickname) > 0); + + connect(block_action, &QAction::triggered, [this, nickname] { + if (block_list.count(nickname)) { + block_list.erase(nickname); + } else { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Block Player"), + tr("When you block a player, you will no longer receive chat messages from " + "them.<br><br>Are you sure you would like to block %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + block_list.emplace(nickname); + } + }); + } + + if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself + context_menu.addSeparator(); + + QAction* kick_action = context_menu.addAction(tr("Kick")); + QAction* ban_action = context_menu.addAction(tr("Ban")); + + connect(kick_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = + QMessageBox::question(this, tr("Kick Player"), + tr("Are you sure you would like to <b>kick</b> %1?") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModKick, nickname); + }); + connect(ban_action, &QAction::triggered, [this, nickname] { + QMessageBox::StandardButton result = QMessageBox::question( + this, tr("Ban Player"), + tr("Are you sure you would like to <b>kick and ban</b> %1?\n\nThis would " + "ban both their forum username and their IP address.") + .arg(QString::fromStdString(nickname)), + QMessageBox::Yes | QMessageBox::No); + if (result == QMessageBox::Yes) + SendModerationRequest(Network::IdModBan, nickname); + }); + } + + context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location)); +} diff --git a/src/yuzu/multiplayer/chat_room.h b/src/yuzu/multiplayer/chat_room.h new file mode 100644 index 000000000..01c70fad0 --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include <unordered_set> +#include <QDialog> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include <QVariant> +#include "network/network.h" + +namespace Ui { +class ChatRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +class ChatRoom : public QWidget { + Q_OBJECT + +public: + explicit ChatRoom(QWidget* parent); + void Initialize(Network::RoomNetwork* room_network); + void RetranslateUi(); + void SetPlayerList(const Network::RoomMember::MemberList& member_list); + void Clear(); + void AppendStatusMessage(const QString& msg); + ~ChatRoom(); + + void SetModPerms(bool is_mod); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation& info); + void OnChatReceive(const Network::ChatEntry&); + void OnStatusMessageReceive(const Network::StatusMessageEntry&); + void OnSendChat(); + void OnChatTextChanged(); + void PopupContextMenu(const QPoint& menu_location); + void Disable(); + void Enable(); + +signals: + void ChatReceived(const Network::ChatEntry&); + void StatusMessageReceived(const Network::StatusMessageEntry&); + void UserPinged(); + +private: + static constexpr u32 max_chat_lines = 1000; + void AppendChatMessage(const QString&); + bool ValidateMessage(const std::string&); + void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname); + + bool has_mod_perms = false; + QStandardItemModel* player_list; + std::unique_ptr<Ui::ChatRoom> ui; + std::unordered_set<std::string> block_list; + std::unordered_map<std::string, QPixmap> icon_cache; + Network::RoomNetwork* room_network; +}; + +Q_DECLARE_METATYPE(Network::ChatEntry); +Q_DECLARE_METATYPE(Network::StatusMessageEntry); +Q_DECLARE_METATYPE(Network::RoomInformation); +Q_DECLARE_METATYPE(Network::RoomMember::State); +Q_DECLARE_METATYPE(Network::RoomMember::Error); diff --git a/src/yuzu/multiplayer/chat_room.ui b/src/yuzu/multiplayer/chat_room.ui new file mode 100644 index 000000000..f2b31b5da --- /dev/null +++ b/src/yuzu/multiplayer/chat_room.ui @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ChatRoom</class> + <widget class="QWidget" name="ChatRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>807</width> + <height>432</height> + </rect> + </property> + <property name="windowTitle"> + <string>Room Window</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QTreeView" name="player_view"/> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QTextEdit" name="chat_history"> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLineEdit" name="chat_message"> + <property name="placeholderText"> + <string>Send Chat Message</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="send_message"> + <property name="text"> + <string>Send Message</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp new file mode 100644 index 000000000..a9859ed70 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.cpp @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <future> +#include <QColor> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMetaType> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#include "common/logging/log.h" +#include "core/announce_multiplayer_session.h" +#include "ui_client_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/moderation_dialog.h" +#include "yuzu/multiplayer/state.h" + +ClientRoomWindow::ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::ClientRoom>()), room_network{room_network_} { + ui->setupUi(this); + ui->chat->Initialize(&room_network); + + // setup the callbacks for network updates + if (auto member = room_network.GetRoomMember().lock()) { + member->BindOnRoomInformationChanged( + [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); }); + member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit StateChanged(state); }); + + connect(this, &ClientRoomWindow::RoomInformationChanged, this, + &ClientRoomWindow::OnRoomUpdate); + connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange); + // Update the state + OnStateChange(member->GetState()); + } else { + // TODO (jroweboy) network was not initialized? + } + + connect(ui->disconnect, &QPushButton::clicked, this, &ClientRoomWindow::Disconnect); + ui->disconnect->setDefault(false); + ui->disconnect->setAutoDefault(false); + connect(ui->moderation, &QPushButton::clicked, [this] { + ModerationDialog dialog(room_network, this); + dialog.exec(); + }); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); + connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification); + UpdateView(); +} + +ClientRoomWindow::~ClientRoomWindow() = default; + +void ClientRoomWindow::SetModPerms(bool is_mod) { + ui->chat->SetModPerms(is_mod); + ui->moderation->setVisible(is_mod); + ui->moderation->setDefault(false); + ui->moderation->setAutoDefault(false); +} + +void ClientRoomWindow::RetranslateUi() { + ui->retranslateUi(this); + ui->chat->RetranslateUi(); +} + +void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) { + UpdateView(); +} + +void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) { + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + ui->chat->Clear(); + ui->chat->AppendStatusMessage(tr("Connected")); + SetModPerms(state == Network::RoomMember::State::Moderator); + } + UpdateView(); +} + +void ClientRoomWindow::Disconnect() { + auto parent = static_cast<MultiplayerState*>(parentWidget()); + if (parent->OnCloseRoom()) { + ui->chat->AppendStatusMessage(tr("Disconnected")); + close(); + } +} + +void ClientRoomWindow::UpdateView() { + if (auto member = room_network.GetRoomMember().lock()) { + if (member->IsConnected()) { + ui->chat->Enable(); + ui->disconnect->setEnabled(true); + auto memberlist = member->GetMemberInformation(); + ui->chat->SetPlayerList(memberlist); + const auto information = member->GetRoomInformation(); + setWindowTitle(QString(tr("%1 (%2/%3 members) - connected")) + .arg(QString::fromStdString(information.name)) + .arg(memberlist.size()) + .arg(information.member_slots)); + ui->description->setText(QString::fromStdString(information.description)); + return; + } + } + // TODO(B3N30): can't get RoomMember*, show error and close window + close(); +} + +void ClientRoomWindow::UpdateIconDisplay() { + ui->chat->UpdateIconDisplay(); +} diff --git a/src/yuzu/multiplayer/client_room.h b/src/yuzu/multiplayer/client_room.h new file mode 100644 index 000000000..f338e3c59 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "yuzu/multiplayer/chat_room.h" + +namespace Ui { +class ClientRoom; +} + +class ClientRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_); + ~ClientRoomWindow(); + + void RetranslateUi(); + void UpdateIconDisplay(); + +public slots: + void OnRoomUpdate(const Network::RoomInformation&); + void OnStateChange(const Network::RoomMember::State&); + +signals: + void RoomInformationChanged(const Network::RoomInformation&); + void StateChanged(const Network::RoomMember::State&); + void ShowNotification(); + +private: + void Disconnect(); + void UpdateView(); + void SetModPerms(bool is_mod); + + QStandardItemModel* player_list; + std::unique_ptr<Ui::ClientRoom> ui; + Network::RoomNetwork& room_network; +}; diff --git a/src/yuzu/multiplayer/client_room.ui b/src/yuzu/multiplayer/client_room.ui new file mode 100644 index 000000000..97e88b502 --- /dev/null +++ b/src/yuzu/multiplayer/client_room.ui @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ClientRoom</class> + <widget class="QWidget" name="ClientRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>807</width> + <height>432</height> + </rect> + </property> + <property name="windowTitle"> + <string>Room Window</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="description"> + <property name="text"> + <string>Room Description</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="moderation"> + <property name="text"> + <string>Moderation...</string> + </property> + <property name="visible"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="disconnect"> + <property name="text"> + <string>Leave Room</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="ChatRoom" name="chat" native="true"/> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>ChatRoom</class> + <extends>QWidget</extends> + <header>multiplayer/chat_room.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp new file mode 100644 index 000000000..9000c4531 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QComboBox> +#include <QFuture> +#include <QIntValidator> +#include <QRegExpValidator> +#include <QString> +#include <QtConcurrent/QtConcurrentRun> +#include "common/settings.h" +#include "network/network.h" +#include "ui_direct_connect.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" + +enum class ConnectionType : u8 { TraversalServer, IP }; + +DirectConnectWindow::DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::DirectConnect>()), room_network{room_network_} { + + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher<void>; + connect(watcher, &QFutureWatcher<void>::finished, this, &DirectConnectWindow::OnConnection); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue()); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->ip->setValidator(validation.GetIP()); + ui->ip->setText(UISettings::values.multiplayer_ip.GetValue()); + ui->port->setValidator(validation.GetPort()); + ui->port->setText(QString::number(UISettings::values.multiplayer_port.GetValue())); + + // TODO(jroweboy): Show or hide the connection options based on the current value of the combo + // box. Add this back in when the traversal server support is added. + connect(ui->connect, &QPushButton::clicked, this, &DirectConnectWindow::Connect); +} + +DirectConnectWindow::~DirectConnectWindow() = default; + +void DirectConnectWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +void DirectConnectWindow::Connect() { + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (const auto member = room_network.GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + switch (static_cast<ConnectionType>(ui->connection_type->currentIndex())) { + case ConnectionType::TraversalServer: + break; + case ConnectionType::IP: + if (!ui->ip->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::IP_ADDRESS_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + break; + } + + // Store settings + UISettings::values.multiplayer_nickname = ui->nickname->text(); + UISettings::values.multiplayer_ip = ui->ip->text(); + if (ui->port->isModified() && !ui->port->text().isEmpty()) { + UISettings::values.multiplayer_port = ui->port->text().toInt(); + } else { + UISettings::values.multiplayer_port = UISettings::values.multiplayer_port.GetDefault(); + } + + // attempt to connect in a different thread + QFuture<void> f = QtConcurrent::run([&] { + if (auto room_member = room_network.GetRoomMember().lock()) { + auto port = UISettings::values.multiplayer_port.GetValue(); + room_member->Join(ui->nickname->text().toStdString(), "", + ui->ip->text().toStdString().c_str(), port, 0, + Network::NoPreferredMac, ui->password->text().toStdString().c_str()); + } + }); + watcher->setFuture(f); + // and disable widgets and display a connecting while we wait + BeginConnecting(); +} + +void DirectConnectWindow::BeginConnecting() { + ui->connect->setEnabled(false); + ui->connect->setText(tr("Connecting")); +} + +void DirectConnectWindow::EndConnecting() { + ui->connect->setEnabled(true); + ui->connect->setText(tr("Connect")); +} + +void DirectConnectWindow::OnConnection() { + EndConnecting(); + + if (auto room_member = room_network.GetRoomMember().lock()) { + if (room_member->GetState() == Network::RoomMember::State::Joined || + room_member->GetState() == Network::RoomMember::State::Moderator) { + + close(); + } + } +} diff --git a/src/yuzu/multiplayer/direct_connect.h b/src/yuzu/multiplayer/direct_connect.h new file mode 100644 index 000000000..4e1043053 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include <QDialog> +#include <QFutureWatcher> +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class DirectConnect; +} + +class DirectConnectWindow : public QDialog { + Q_OBJECT + +public: + explicit DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent = nullptr); + ~DirectConnectWindow(); + + void RetranslateUi(); + +signals: + /** + * Signalled by this widget when it is closing itself and destroying any state such as + * connections that it might have. + */ + void Closed(); + +private slots: + void OnConnection(); + +private: + void Connect(); + void BeginConnecting(); + void EndConnecting(); + + QFutureWatcher<void>* watcher; + std::unique_ptr<Ui::DirectConnect> ui; + Validation validation; + Network::RoomNetwork& room_network; +}; diff --git a/src/yuzu/multiplayer/direct_connect.ui b/src/yuzu/multiplayer/direct_connect.ui new file mode 100644 index 000000000..681b6bf69 --- /dev/null +++ b/src/yuzu/multiplayer/direct_connect.ui @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DirectConnect</class> + <widget class="QWidget" name="DirectConnect"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>455</width> + <height>161</height> + </rect> + </property> + <property name="windowTitle"> + <string>Direct Connect</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <item> + <widget class="QComboBox" name="connection_type"> + <item> + <property name="text"> + <string>IP Address</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QWidget" name="ip_container" native="true"> + <layout class="QHBoxLayout" name="ip_layout"> + <property name="leftMargin"> + <number>5</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>IP</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="ip"> + <property name="toolTip"> + <string><html><head/><body><p>IPv4 address of the host</p></body></html></string> + </property> + <property name="maxLength"> + <number>16</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Port</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="port"> + <property name="toolTip"> + <string><html><head/><body><p>Port number the host is listening on</p></body></html></string> + </property> + <property name="maxLength"> + <number>5</number> + </property> + <property name="placeholderText"> + <string>24872</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nickname"> + <property name="maxLength"> + <number>20</number> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="password"/> + </item> + </layout> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="connect"> + <property name="text"> + <string>Connect</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp new file mode 100644 index 000000000..cb9464b2b --- /dev/null +++ b/src/yuzu/multiplayer/host_room.cpp @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <future> +#include <QColor> +#include <QImage> +#include <QList> +#include <QLocale> +#include <QMessageBox> +#include <QMetaType> +#include <QTime> +#include <QtConcurrent/QtConcurrentRun> +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/announce_multiplayer_session.h" +#include "ui_host_room.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/verify_user_jwt.h" +#endif + +HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session, + Network::RoomNetwork& room_network_) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::HostRoom>()), + announce_multiplayer_session(session), room_network{room_network_} { + ui->setupUi(this); + + // set up validation for all of the fields + ui->room_name->setValidator(validation.GetRoomName()); + ui->username->setValidator(validation.GetNickname()); + ui->port->setValidator(validation.GetPort()); + ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort)); + + // Create a proxy to the game list to display the list of preferred games + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new ComboBoxProxyModel; + proxy->setSourceModel(game_list); + proxy->sort(0, Qt::AscendingOrder); + ui->game_list->setModel(proxy); + + // Connect all the widgets to the appropriate events + connect(ui->host, &QPushButton::clicked, this, &HostRoomWindow::Host); + + // Restore the settings: + ui->username->setText(UISettings::values.multiplayer_room_nickname.GetValue()); + if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + ui->room_name->setText(UISettings::values.multiplayer_room_name.GetValue()); + ui->port->setText(QString::number(UISettings::values.multiplayer_room_port.GetValue())); + ui->max_player->setValue(UISettings::values.multiplayer_max_player.GetValue()); + int index = UISettings::values.multiplayer_host_type.GetValue(); + if (index < ui->host_type->count()) { + ui->host_type->setCurrentIndex(index); + } + index = ui->game_list->findData(UISettings::values.multiplayer_game_id.GetValue(), + GameListItemPath::ProgramIdRole); + if (index != -1) { + ui->game_list->setCurrentIndex(index); + } + ui->room_description->setText(UISettings::values.multiplayer_room_description.GetValue()); +} + +HostRoomWindow::~HostRoomWindow() = default; + +void HostRoomWindow::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } +} + +void HostRoomWindow::RetranslateUi() { + ui->retranslateUi(this); +} + +std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBackend( + bool use_validation) const { + std::unique_ptr<Network::VerifyUser::Backend> verify_backend; + if (use_validation) { +#ifdef ENABLE_WEB_SERVICE + verify_backend = + std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue()); +#else + verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); +#endif + } else { + verify_backend = std::make_unique<Network::VerifyUser::NullBackend>(); + } + return verify_backend; +} + +void HostRoomWindow::Host() { + if (!ui->username->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + if (!ui->room_name->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOMNAME_NOT_VALID); + return; + } + if (!ui->port->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID); + return; + } + if (ui->game_list->currentIndex() == -1) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED); + return; + } + if (auto member = room_network.GetRoomMember().lock()) { + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + auto parent = static_cast<MultiplayerState*>(parentWidget()); + if (!parent->OnCloseRoom()) { + close(); + return; + } + } + ui->host->setDisabled(true); + + const AnnounceMultiplayerRoom::GameInfo game{ + .name = ui->game_list->currentData(Qt::DisplayRole).toString().toStdString(), + .id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toULongLong(), + }; + const auto port = + ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort; + const auto password = ui->password->text().toStdString(); + const bool is_public = ui->host_type->currentIndex() == 0; + Network::Room::BanList ban_list{}; + if (ui->load_ban_list->isChecked()) { + ban_list = UISettings::values.multiplayer_ban_list; + } + if (auto room = room_network.GetRoom().lock()) { + const bool created = + room->Create(ui->room_name->text().toStdString(), + ui->room_description->toPlainText().toStdString(), "", port, password, + ui->max_player->value(), Settings::values.yuzu_username.GetValue(), + game, CreateVerifyBackend(is_public), ban_list); + if (!created) { + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::COULD_NOT_CREATE_ROOM); + LOG_ERROR(Network, "Could not create room!"); + ui->host->setEnabled(true); + return; + } + } + // Start the announce session if they chose Public + if (is_public) { + if (auto session = announce_multiplayer_session.lock()) { + // Register the room first to ensure verify_uid is present when we connect + WebService::WebResult result = session->Register(); + if (result.result_code != WebService::WebResult::Code::Success) { + QMessageBox::warning( + this, tr("Error"), + tr("Failed to announce the room to the public lobby. In order to host a " + "room publicly, you must have a valid yuzu account configured in " + "Emulation -> Configure -> Web. If you do not want to publish a room in " + "the public lobby, then select Unlisted instead.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); + ui->host->setEnabled(true); + if (auto room = room_network.GetRoom().lock()) { + room->Destroy(); + } + return; + } + session->Start(); + } else { + LOG_ERROR(Network, "Starting announce session failed"); + } + } + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (is_public) { + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); + if (auto room = room_network.GetRoom().lock()) { + token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; + } + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + // TODO: Check what to do with this + member->Join(ui->username->text().toStdString(), "", "127.0.0.1", port, 0, + Network::NoPreferredMac, password, token); + + // Store settings + UISettings::values.multiplayer_room_nickname = ui->username->text(); + UISettings::values.multiplayer_room_name = ui->room_name->text(); + UISettings::values.multiplayer_game_id = + ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong(); + UISettings::values.multiplayer_max_player = ui->max_player->value(); + + UISettings::values.multiplayer_host_type = ui->host_type->currentIndex(); + if (ui->port->isModified() && !ui->port->text().isEmpty()) { + UISettings::values.multiplayer_room_port = ui->port->text().toInt(); + } else { + UISettings::values.multiplayer_room_port = Network::DefaultRoomPort; + } + UISettings::values.multiplayer_room_description = ui->room_description->toPlainText(); + ui->host->setEnabled(true); + close(); + } +} + +QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const { + if (role != Qt::DisplayRole) { + auto val = QSortFilterProxyModel::data(idx, role); + // If its the icon, shrink it to 16x16 + if (role == Qt::DecorationRole) + val = val.value<QImage>().scaled(16, 16, Qt::KeepAspectRatio); + return val; + } + std::string filename; + Common::SplitPath( + QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(), + nullptr, &filename, nullptr); + QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString(); + return title.isEmpty() ? QString::fromStdString(filename) : title; +} + +bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + auto leftData = left.data(GameListItemPath::TitleRole).toString(); + auto rightData = right.data(GameListItemPath::TitleRole).toString(); + return leftData.compare(rightData) < 0; +} diff --git a/src/yuzu/multiplayer/host_room.h b/src/yuzu/multiplayer/host_room.h new file mode 100644 index 000000000..a968042d0 --- /dev/null +++ b/src/yuzu/multiplayer/host_room.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include <QDialog> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include <QVariant> +#include "network/network.h" +#include "yuzu/multiplayer/chat_room.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class HostRoom; +} + +namespace Core { +class AnnounceMultiplayerSession; +} + +class ConnectionError; +class ComboBoxProxyModel; + +class ChatMessage; + +namespace Network::VerifyUser { +class Backend; +}; + +class HostRoomWindow : public QDialog { + Q_OBJECT + +public: + explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session, + Network::RoomNetwork& room_network_); + ~HostRoomWindow(); + + /** + * Updates the dialog with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +private: + void Host(); + std::unique_ptr<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const; + + std::unique_ptr<Ui::HostRoom> ui; + std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + QStandardItemModel* game_list; + ComboBoxProxyModel* proxy; + Validation validation; + Network::RoomNetwork& room_network; +}; + +/** + * Proxy Model for the game list combo box so we can reuse the game list model while still + * displaying the fields slightly differently + */ +class ComboBoxProxyModel : public QSortFilterProxyModel { + Q_OBJECT + +public: + int columnCount(const QModelIndex& idx) const override { + return 1; + } + + QVariant data(const QModelIndex& idx, int role) const override; + + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; +}; diff --git a/src/yuzu/multiplayer/host_room.ui b/src/yuzu/multiplayer/host_room.ui new file mode 100644 index 000000000..d54cf49c6 --- /dev/null +++ b/src/yuzu/multiplayer/host_room.ui @@ -0,0 +1,207 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>HostRoom</class> + <widget class="QWidget" name="HostRoom"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>607</width> + <height>211</height> + </rect> + </property> + <property name="windowTitle"> + <string>Create Room</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QWidget" name="settings" native="true"> + <layout class="QHBoxLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <layout class="QFormLayout" name="formLayout_2"> + <property name="labelAlignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Room Name</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="room_name"> + <property name="maxLength"> + <number>50</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Preferred Game</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="game_list"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Max Players</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="max_player"> + <property name="minimum"> + <number>2</number> + </property> + <property name="maximum"> + <number>16</number> + </property> + <property name="value"> + <number>8</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QFormLayout" name="formLayout"> + <property name="labelAlignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <item row="0" column="1"> + <widget class="QLineEdit" name="username"/> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="password"> + <property name="echoMode"> + <enum>QLineEdit::PasswordEchoOnEdit</enum> + </property> + <property name="placeholderText"> + <string>(Leave blank for open game)</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="port"> + <property name="inputMethodHints"> + <set>Qt::ImhDigitsOnly</set> + </property> + <property name="maxLength"> + <number>5</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Password</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Port</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Room Description</string> + </property> + </widget> + </item> + <item> + <widget class="QTextEdit" name="room_description"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout"> + <item> + <widget class="QCheckBox" name="load_ban_list"> + <property name="text"> + <string>Load Previous Ban List</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QComboBox" name="host_type"> + <item> + <property name="text"> + <string>Public</string> + </property> + </item> + <item> + <property name="text"> + <string>Unlisted</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QPushButton" name="host"> + <property name="text"> + <string>Host Room</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp new file mode 100644 index 000000000..23c2f21ab --- /dev/null +++ b/src/yuzu/multiplayer/lobby.cpp @@ -0,0 +1,367 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QInputDialog> +#include <QList> +#include <QtConcurrent/QtConcurrentRun> +#include "common/logging/log.h" +#include "common/settings.h" +#include "network/network.h" +#include "ui_lobby.h" +#include "yuzu/game_list_p.h" +#include "yuzu/main.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/lobby_p.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/multiplayer/validation.h" +#include "yuzu/uisettings.h" +#ifdef ENABLE_WEB_SERVICE +#include "web_service/web_backend.h" +#endif + +Lobby::Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session, + Network::RoomNetwork& room_network_) + : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), + ui(std::make_unique<Ui::Lobby>()), + announce_multiplayer_session(session), room_network{room_network_} { + ui->setupUi(this); + + // setup the watcher for background connections + watcher = new QFutureWatcher<void>; + + model = new QStandardItemModel(ui->room_list); + + // Create a proxy to the game list to get the list of games owned + game_list = new QStandardItemModel; + UpdateGameList(list); + + proxy = new LobbyFilterProxyModel(this, game_list); + proxy->setSourceModel(model); + proxy->setDynamicSortFilter(true); + proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxy->setSortLocaleAware(true); + ui->room_list->setModel(proxy); + ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive); + ui->room_list->header()->stretchLastSection(); + ui->room_list->setAlternatingRowColors(true); + ui->room_list->setSelectionMode(QHeaderView::SingleSelection); + ui->room_list->setSelectionBehavior(QHeaderView::SelectRows); + ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + ui->room_list->setSortingEnabled(true); + ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers); + ui->room_list->setExpandsOnDoubleClick(false); + ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu); + + ui->nickname->setValidator(validation.GetNickname()); + ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue()); + if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) { + // Use yuzu Web Service user name as nickname by default + ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue())); + } + + // UI Buttons + connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby); + connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned); + connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull); + connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch); + connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom); + connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom); + + // Actions + connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this, + &Lobby::OnRefreshLobby); + + // manually start a refresh when the window is opening + // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as + // part of the constructor, but offload the refresh until after the window shown. perhaps emit a + // refreshroomlist signal from places that open the lobby + RefreshLobby(); +} + +Lobby::~Lobby() = default; + +void Lobby::UpdateGameList(QStandardItemModel* list) { + game_list->clear(); + for (int i = 0; i < list->rowCount(); i++) { + auto parent = list->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + game_list->appendRow(parent->child(j)->clone()); + } + } + if (proxy) + proxy->UpdateGameList(game_list); +} + +void Lobby::RetranslateUi() { + ui->retranslateUi(this); +} + +QString Lobby::PasswordPrompt() { + bool ok; + const QString text = + QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"), + QLineEdit::Password, QString(), &ok); + return ok ? text : QString(); +} + +void Lobby::OnExpandRoom(const QModelIndex& index) { + QModelIndex member_index = proxy->index(index.row(), Column::MEMBER); + auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList(); +} + +void Lobby::OnJoinRoom(const QModelIndex& source) { + if (const auto member = room_network.GetRoomMember().lock()) { + // Prevent the user from trying to join a room while they are already joining. + if (member->GetState() == Network::RoomMember::State::Joining) { + return; + } else if (member->IsConnected()) { + // And ask if they want to leave the room if they are already in one. + if (!NetworkMessage::WarnDisconnect()) { + return; + } + } + } + QModelIndex index = source; + // If the user double clicks on a child row (aka the player list) then use the parent instead + if (source.parent() != QModelIndex()) { + index = source.parent(); + } + if (!ui->nickname->hasAcceptableInput()) { + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID); + return; + } + + // Get a password to pass if the room is password protected + QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME); + bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool(); + const std::string password = has_password ? PasswordPrompt().toStdString() : ""; + if (has_password && password.empty()) { + return; + } + + QModelIndex connection_index = proxy->index(index.row(), Column::HOST); + const std::string nickname = ui->nickname->text().toStdString(); + const std::string ip = + proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString(); + int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); + const std::string verify_uid = + proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString(); + + // attempt to connect in a different thread + QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] { + std::string token; +#ifdef ENABLE_WEB_SERVICE + if (!Settings::values.yuzu_username.GetValue().empty() && + !Settings::values.yuzu_token.GetValue().empty()) { + WebService::Client client(Settings::values.web_api_url.GetValue(), + Settings::values.yuzu_username.GetValue(), + Settings::values.yuzu_token.GetValue()); + token = client.GetExternalJWT(verify_uid).returned_data; + if (token.empty()) { + LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); + } else { + LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); + } + } +#endif + if (auto room_member = room_network.GetRoomMember().lock()) { + room_member->Join(nickname, "", ip.c_str(), port, 0, Network::NoPreferredMac, password, + token); + } + }); + watcher->setFuture(f); + + // TODO(jroweboy): disable widgets and display a connecting while we wait + + // Save settings + UISettings::values.multiplayer_nickname = ui->nickname->text(); + UISettings::values.multiplayer_ip = + proxy->data(connection_index, LobbyItemHost::HostIPRole).toString(); + UISettings::values.multiplayer_port = + proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt(); +} + +void Lobby::ResetModel() { + model->clear(); + model->insertColumns(0, Column::TOTAL); + model->setHeaderData(Column::EXPAND, Qt::Horizontal, QString(), Qt::DisplayRole); + model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole); + model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole); + model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole); + model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole); +} + +void Lobby::RefreshLobby() { + if (auto session = announce_multiplayer_session.lock()) { + ResetModel(); + ui->refresh_list->setEnabled(false); + ui->refresh_list->setText(tr("Refreshing")); + room_list_watcher.setFuture( + QtConcurrent::run([session]() { return session->GetRoomList(); })); + } else { + // TODO(jroweboy): Display an error box about announce couldn't be started + } +} + +void Lobby::OnRefreshLobby() { + AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result(); + for (auto room : new_room_list) { + // find the icon for the game if this person owns that game. + QPixmap smdh_icon; + for (int r = 0; r < game_list->rowCount(); ++r) { + auto index = game_list->index(r, 0); + auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong(); + if (game_id != 0 && room.information.preferred_game.id == game_id) { + smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>(); + } + } + + QList<QVariant> members; + for (auto member : room.members) { + QVariant var; + var.setValue(LobbyMember{QString::fromStdString(member.username), + QString::fromStdString(member.nickname), member.game.id, + QString::fromStdString(member.game.name)}); + members.append(var); + } + + auto first_item = new LobbyItem(); + auto row = QList<QStandardItem*>({ + first_item, + new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)), + new LobbyItemGame(room.information.preferred_game.id, + QString::fromStdString(room.information.preferred_game.name), + smdh_icon), + new LobbyItemHost(QString::fromStdString(room.information.host_username), + QString::fromStdString(room.ip), room.information.port, + QString::fromStdString(room.verify_uid)), + new LobbyItemMemberList(members, room.information.member_slots), + }); + model->appendRow(row); + // To make the rows expandable, add the member data as a child of the first column of the + // rows with people in them and have qt set them to colspan after the model is finished + // resetting + if (!room.information.description.empty()) { + first_item->appendRow( + new LobbyItemDescription(QString::fromStdString(room.information.description))); + } + if (!room.members.empty()) { + first_item->appendRow(new LobbyItemExpandedMemberList(members)); + } + } + + // Reenable the refresh button and resize the columns + ui->refresh_list->setEnabled(true); + ui->refresh_list->setText(tr("Refresh List")); + ui->room_list->header()->stretchLastSection(); + for (int i = 0; i < Column::TOTAL - 1; ++i) { + ui->room_list->resizeColumnToContents(i); + } + + // Set the member list child items to span all columns + for (int i = 0; i < proxy->rowCount(); i++) { + auto parent = model->item(i, 0); + for (int j = 0; j < parent->rowCount(); j++) { + ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true); + } + } +} + +LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list) + : QSortFilterProxyModel(parent), game_list(list) {} + +void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) { + game_list = list; +} + +bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { + // Prioritize filters by fastest to compute + + // pass over any child rows (aka row that shows the players in the room) + if (sourceParent != QModelIndex()) { + return true; + } + + // filter by filled rooms + if (filter_full) { + QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent); + int player_count = + sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size(); + int max_players = + sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt(); + if (player_count >= max_players) { + return false; + } + } + + // filter by search parameters + if (!filter_search.isEmpty()) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent); + QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent); + bool preferred_game_match = sourceModel() + ->data(game_name, LobbyItemGame::GameNameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool room_name_match = sourceModel() + ->data(room_name, LobbyItemName::NameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + bool username_match = sourceModel() + ->data(host_name, LobbyItemHost::HostUsernameRole) + .toString() + .contains(filter_search, filterCaseSensitivity()); + if (!preferred_game_match && !room_name_match && !username_match) { + return false; + } + } + + // filter by game owned + if (filter_owned) { + QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent); + QList<QModelIndex> owned_games; + for (int r = 0; r < game_list->rowCount(); ++r) { + owned_games.append(QModelIndex(game_list->index(r, 0))); + } + auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong(); + if (current_id == 0) { + // TODO(jroweboy): homebrew often doesn't have a game id and this hides them + return false; + } + bool owned = false; + for (const auto& game : owned_games) { + auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong(); + if (current_id == game_id) { + owned = true; + } + } + if (!owned) { + return false; + } + } + + return true; +} + +void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) { + sourceModel()->sort(column, order); +} + +void LobbyFilterProxyModel::SetFilterOwned(bool filter) { + filter_owned = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterFull(bool filter) { + filter_full = filter; + invalidate(); +} + +void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) { + filter_search = filter; + invalidate(); +} diff --git a/src/yuzu/multiplayer/lobby.h b/src/yuzu/multiplayer/lobby.h new file mode 100644 index 000000000..82744ca94 --- /dev/null +++ b/src/yuzu/multiplayer/lobby.h @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include <QDialog> +#include <QFutureWatcher> +#include <QSortFilterProxyModel> +#include <QStandardItemModel> +#include "common/announce_multiplayer_room.h" +#include "core/announce_multiplayer_session.h" +#include "network/network.h" +#include "yuzu/multiplayer/validation.h" + +namespace Ui { +class Lobby; +} + +class LobbyModel; +class LobbyFilterProxyModel; + +/** + * Listing of all public games pulled from services. The lobby should be simple enough for users to + * find the game they want to play, and join it. + */ +class Lobby : public QDialog { + Q_OBJECT + +public: + explicit Lobby(QWidget* parent, QStandardItemModel* list, + std::shared_ptr<Core::AnnounceMultiplayerSession> session, + Network::RoomNetwork& room_network_); + ~Lobby() override; + + /** + * Updates the lobby with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* list); + void RetranslateUi(); + +public slots: + /** + * Begin the process to pull the latest room list from web services. After the listing is + * returned from web services, `LobbyRefreshed` will be signalled + */ + void RefreshLobby(); + +private slots: + /** + * Pulls the list of rooms from network and fills out the lobby model with the results + */ + void OnRefreshLobby(); + + /** + * Handler for single clicking on a room in the list. Expands the treeitem to show player + * information for the people in the room + * + * index - The row of the proxy model that the user wants to join. + */ + void OnExpandRoom(const QModelIndex&); + + /** + * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts + * to connect. Will also prompt for a password in case one is required. + * + * index - The row of the proxy model that the user wants to join. + */ + void OnJoinRoom(const QModelIndex&); + +signals: + void StateChanged(const Network::RoomMember::State&); + +private: + /** + * Removes all entries in the Lobby before refreshing. + */ + void ResetModel(); + + /** + * Prompts for a password. Returns an empty QString if the user either did not provide a + * password or if the user closed the window. + */ + QString PasswordPrompt(); + + std::unique_ptr<Ui::Lobby> ui; + + QStandardItemModel* model{}; + QStandardItemModel* game_list{}; + LobbyFilterProxyModel* proxy{}; + + QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher; + std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + QFutureWatcher<void>* watcher; + Validation validation; + Network::RoomNetwork& room_network; +}; + +/** + * Proxy Model for filtering the lobby + */ +class LobbyFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT; + +public: + explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list); + + /** + * Updates the filter with a new game list model. + * This model should be the processed one created by the Lobby. + */ + void UpdateGameList(QStandardItemModel* list); + + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + void sort(int column, Qt::SortOrder order) override; + +public slots: + void SetFilterOwned(bool); + void SetFilterFull(bool); + void SetFilterSearch(const QString&); + +private: + QStandardItemModel* game_list; + bool filter_owned = false; + bool filter_full = false; + QString filter_search; +}; diff --git a/src/yuzu/multiplayer/lobby.ui b/src/yuzu/multiplayer/lobby.ui new file mode 100644 index 000000000..4c9901c9a --- /dev/null +++ b/src/yuzu/multiplayer/lobby.ui @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Lobby</class> + <widget class="QWidget" name="Lobby"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>903</width> + <height>487</height> + </rect> + </property> + <property name="windowTitle"> + <string>Public Room Browser</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="spacing"> + <number>3</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>6</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nickname"> + <property name="placeholderText"> + <string>Nickname</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Filters</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="search"> + <property name="placeholderText"> + <string>Search</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="games_owned"> + <property name="text"> + <string>Games I Own</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="hide_full"> + <property name="text"> + <string>Hide Full Rooms</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refresh_list"> + <property name="text"> + <string>Refresh Lobby</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + <item> + <widget class="QTreeView" name="room_list"/> + </item> + <item> + <widget class="QWidget" name="widget" native="true"/> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/yuzu/multiplayer/lobby_p.h b/src/yuzu/multiplayer/lobby_p.h new file mode 100644 index 000000000..8071cede4 --- /dev/null +++ b/src/yuzu/multiplayer/lobby_p.h @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <utility> +#include <QPixmap> +#include <QStandardItem> +#include <QStandardItemModel> +#include "common/common_types.h" + +namespace Column { +enum List { + EXPAND, + ROOM_NAME, + GAME_NAME, + HOST, + MEMBER, + TOTAL, +}; +} + +class LobbyItem : public QStandardItem { +public: + LobbyItem() = default; + explicit LobbyItem(const QString& string) : QStandardItem(string) {} + virtual ~LobbyItem() override = default; +}; + +class LobbyItemName : public LobbyItem { +public: + static const int NameRole = Qt::UserRole + 1; + static const int PasswordRole = Qt::UserRole + 2; + + LobbyItemName() = default; + explicit LobbyItemName(bool has_password, QString name) : LobbyItem() { + setData(name, NameRole); + setData(has_password, PasswordRole); + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + bool has_password = data(PasswordRole).toBool(); + return has_password ? QIcon::fromTheme(QStringLiteral("lock")).pixmap(16) : QIcon(); + } + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(NameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0; + } +}; + +class LobbyItemDescription : public LobbyItem { +public: + static const int DescriptionRole = Qt::UserRole + 1; + + LobbyItemDescription() = default; + explicit LobbyItemDescription(QString description) { + setData(description, DescriptionRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto description = data(DescriptionRole).toString(); + description.prepend(QStringLiteral("Description: ")); + return description; + } + + bool operator<(const QStandardItem& other) const override { + return data(DescriptionRole) + .toString() + .localeAwareCompare(other.data(DescriptionRole).toString()) < 0; + } +}; + +class LobbyItemGame : public LobbyItem { +public: + static const int TitleIDRole = Qt::UserRole + 1; + static const int GameNameRole = Qt::UserRole + 2; + static const int GameIconRole = Qt::UserRole + 3; + + LobbyItemGame() = default; + explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) { + setData(static_cast<unsigned long long>(title_id), TitleIDRole); + setData(game_name, GameNameRole); + if (!smdh_icon.isNull()) { + setData(smdh_icon, GameIconRole); + } + } + + QVariant data(int role) const override { + if (role == Qt::DecorationRole) { + auto val = data(GameIconRole); + if (val.isValid()) { + val = val.value<QPixmap>().scaled(16, 16, Qt::KeepAspectRatio); + } + return val; + } else if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(GameNameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(GameNameRole) + .toString() + .localeAwareCompare(other.data(GameNameRole).toString()) < 0; + } +}; + +class LobbyItemHost : public LobbyItem { +public: + static const int HostUsernameRole = Qt::UserRole + 1; + static const int HostIPRole = Qt::UserRole + 2; + static const int HostPortRole = Qt::UserRole + 3; + static const int HostVerifyUIDRole = Qt::UserRole + 4; + + LobbyItemHost() = default; + explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_uid) { + setData(username, HostUsernameRole); + setData(ip, HostIPRole); + setData(port, HostPortRole); + setData(verify_uid, HostVerifyUIDRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + return data(HostUsernameRole).toString(); + } + + bool operator<(const QStandardItem& other) const override { + return data(HostUsernameRole) + .toString() + .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0; + } +}; + +class LobbyMember { +public: + LobbyMember() = default; + LobbyMember(const LobbyMember& other) = default; + explicit LobbyMember(QString username_, QString nickname_, u64 title_id_, QString game_name_) + : username(std::move(username_)), nickname(std::move(nickname_)), title_id(title_id_), + game_name(std::move(game_name_)) {} + ~LobbyMember() = default; + + QString GetName() const { + if (username.isEmpty() || username == nickname) { + return nickname; + } else { + return QStringLiteral("%1 (%2)").arg(nickname, username); + } + } + u64 GetTitleId() const { + return title_id; + } + QString GetGameName() const { + return game_name; + } + +private: + QString username; + QString nickname; + u64 title_id; + QString game_name; +}; + +Q_DECLARE_METATYPE(LobbyMember); + +class LobbyItemMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + static const int MaxPlayerRole = Qt::UserRole + 2; + + LobbyItemMemberList() = default; + explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) { + setData(members, MemberListRole); + setData(max_players, MaxPlayerRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + return QStringLiteral("%1 / %2").arg(QString::number(members.size()), + data(MaxPlayerRole).toString()); + } + + bool operator<(const QStandardItem& other) const override { + // sort by rooms that have the most players + int left_members = data(MemberListRole).toList().size(); + int right_members = other.data(MemberListRole).toList().size(); + return left_members < right_members; + } +}; + +/** + * Member information for when a lobby is expanded in the UI + */ +class LobbyItemExpandedMemberList : public LobbyItem { +public: + static const int MemberListRole = Qt::UserRole + 1; + + LobbyItemExpandedMemberList() = default; + explicit LobbyItemExpandedMemberList(QList<QVariant> members) { + setData(members, MemberListRole); + } + + QVariant data(int role) const override { + if (role != Qt::DisplayRole) { + return LobbyItem::data(role); + } + auto members = data(MemberListRole).toList(); + QString out; + bool first = true; + for (const auto& member : members) { + if (!first) + out.append(QStringLiteral("\n")); + const auto& m = member.value<LobbyMember>(); + if (m.GetGameName().isEmpty()) { + out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName()); + } else { + out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName()); + } + first = false; + } + return out; + } +}; diff --git a/src/yuzu/multiplayer/message.cpp b/src/yuzu/multiplayer/message.cpp new file mode 100644 index 000000000..76ec276ad --- /dev/null +++ b/src/yuzu/multiplayer/message.cpp @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QMessageBox> +#include <QString> + +#include "yuzu/multiplayer/message.h" + +namespace NetworkMessage { +const ConnectionError ErrorManager::USERNAME_NOT_VALID( + QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::ROOMNAME_NOT_VALID( + QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters.")); +const ConnectionError ErrorManager::USERNAME_NOT_VALID_SERVER( + QT_TR_NOOP("Username is already in use or not valid. Please choose another.")); +const ConnectionError ErrorManager::IP_ADDRESS_NOT_VALID( + QT_TR_NOOP("IP is not a valid IPv4 address.")); +const ConnectionError ErrorManager::PORT_NOT_VALID( + QT_TR_NOOP("Port must be a number between 0 to 65535.")); +const ConnectionError ErrorManager::GAME_NOT_SELECTED(QT_TR_NOOP( + "You must choose a Preferred Game to host a room. If you do not have any games in your game " + "list yet, add a game folder by clicking on the plus icon in the game list.")); +const ConnectionError ErrorManager::NO_INTERNET( + QT_TR_NOOP("Unable to find an internet connection. Check your internet settings.")); +const ConnectionError ErrorManager::UNABLE_TO_CONNECT( + QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If " + "you still cannot connect, contact the room host and verify that the host is " + "properly configured with the external port forwarded.")); +const ConnectionError ErrorManager::ROOM_IS_FULL( + QT_TR_NOOP("Unable to connect to the room because it is already full.")); +const ConnectionError ErrorManager::COULD_NOT_CREATE_ROOM( + QT_TR_NOOP("Creating a room failed. Please retry. Restarting yuzu might be necessary.")); +const ConnectionError ErrorManager::HOST_BANNED( + QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you " + "or try a different room.")); +const ConnectionError ErrorManager::WRONG_VERSION( + QT_TR_NOOP("Version mismatch! Please update to the latest version of yuzu. If the problem " + "persists, contact the room host and ask them to update the server.")); +const ConnectionError ErrorManager::WRONG_PASSWORD(QT_TR_NOOP("Incorrect password.")); +const ConnectionError ErrorManager::GENERIC_ERROR(QT_TR_NOOP( + "An unknown error occurred. If this error continues to occur, please open an issue")); +const ConnectionError ErrorManager::LOST_CONNECTION( + QT_TR_NOOP("Connection to room lost. Try to reconnect.")); +const ConnectionError ErrorManager::HOST_KICKED( + QT_TR_NOOP("You have been kicked by the room host.")); +const ConnectionError ErrorManager::MAC_COLLISION( + QT_TR_NOOP("MAC address is already in use. Please choose another.")); +const ConnectionError ErrorManager::CONSOLE_ID_COLLISION(QT_TR_NOOP( + "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation " + "> Configure > System to regenerate your Console ID.")); +const ConnectionError ErrorManager::PERMISSION_DENIED( + QT_TR_NOOP("You do not have enough permission to perform this action.")); +const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP( + "The user you are trying to kick/ban could not be found.\nThey may have left the room.")); + +static bool WarnMessage(const std::string& title, const std::string& text) { + return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()), + QObject::tr(text.c_str()), + QMessageBox::Ok | QMessageBox::Cancel); +} + +void ErrorManager::ShowError(const ConnectionError& e) { + QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str())); +} + +bool WarnCloseRoom() { + return WarnMessage( + QT_TR_NOOP("Leave Room"), + QT_TR_NOOP("You are about to close the room. Any network connections will be closed.")); +} + +bool WarnDisconnect() { + return WarnMessage( + QT_TR_NOOP("Disconnect"), + QT_TR_NOOP("You are about to leave the room. Any network connections will be closed.")); +} + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/message.h b/src/yuzu/multiplayer/message.h new file mode 100644 index 000000000..eb5c8d1be --- /dev/null +++ b/src/yuzu/multiplayer/message.h @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <utility> + +namespace NetworkMessage { + +class ConnectionError { + +public: + explicit ConnectionError(std::string str) : err(std::move(str)) {} + const std::string& GetString() const { + return err; + } + +private: + std::string err; +}; + +class ErrorManager : QObject { + Q_OBJECT +public: + /// When the nickname is considered invalid by the client + static const ConnectionError USERNAME_NOT_VALID; + static const ConnectionError ROOMNAME_NOT_VALID; + /// When the nickname is considered invalid by the room server + static const ConnectionError USERNAME_NOT_VALID_SERVER; + static const ConnectionError IP_ADDRESS_NOT_VALID; + static const ConnectionError PORT_NOT_VALID; + static const ConnectionError GAME_NOT_SELECTED; + static const ConnectionError NO_INTERNET; + static const ConnectionError UNABLE_TO_CONNECT; + static const ConnectionError ROOM_IS_FULL; + static const ConnectionError COULD_NOT_CREATE_ROOM; + static const ConnectionError HOST_BANNED; + static const ConnectionError WRONG_VERSION; + static const ConnectionError WRONG_PASSWORD; + static const ConnectionError GENERIC_ERROR; + static const ConnectionError LOST_CONNECTION; + static const ConnectionError HOST_KICKED; + static const ConnectionError MAC_COLLISION; + static const ConnectionError CONSOLE_ID_COLLISION; + static const ConnectionError PERMISSION_DENIED; + static const ConnectionError NO_SUCH_USER; + /** + * Shows a standard QMessageBox with a error message + */ + static void ShowError(const ConnectionError& e); +}; +/** + * Show a standard QMessageBox with a warning message about leaving the room + * return true if the user wants to close the network connection + */ +bool WarnCloseRoom(); + +/** + * Show a standard QMessageBox with a warning message about disconnecting from the room + * return true if the user wants to disconnect + */ +bool WarnDisconnect(); + +} // namespace NetworkMessage diff --git a/src/yuzu/multiplayer/moderation_dialog.cpp b/src/yuzu/multiplayer/moderation_dialog.cpp new file mode 100644 index 000000000..c9b8ed397 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.cpp @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QStandardItem> +#include <QStandardItemModel> +#include "network/network.h" +#include "network/room_member.h" +#include "ui_moderation_dialog.h" +#include "yuzu/multiplayer/moderation_dialog.h" + +namespace Column { +enum { + SUBJECT, + TYPE, + COUNT, +}; +} + +ModerationDialog::ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent) + : QDialog(parent), ui(std::make_unique<Ui::ModerationDialog>()), room_network{room_network_} { + ui->setupUi(this); + + qRegisterMetaType<Network::Room::BanList>(); + + if (auto member = room_network.GetRoomMember().lock()) { + callback_handle_status_message = member->BindOnStatusMessageReceived( + [this](const Network::StatusMessageEntry& status_message) { + emit StatusMessageReceived(status_message); + }); + connect(this, &ModerationDialog::StatusMessageReceived, this, + &ModerationDialog::OnStatusMessageReceived); + callback_handle_ban_list = member->BindOnBanListReceived( + [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); }); + connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList); + } + + // Initialize the UI + model = new QStandardItemModel(ui->ban_list_view); + model->insertColumns(0, Column::COUNT); + model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject")); + model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type")); + + ui->ban_list_view->setModel(model); + + // Load the ban list in background + LoadBanList(); + + connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); }); + connect(ui->unban, &QPushButton::clicked, this, [this] { + auto index = ui->ban_list_view->currentIndex(); + SendUnbanRequest(model->item(index.row(), 0)->text()); + }); + connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); }); +} + +ModerationDialog::~ModerationDialog() { + if (callback_handle_status_message) { + if (auto room = room_network.GetRoomMember().lock()) { + room->Unbind(callback_handle_status_message); + } + } + + if (callback_handle_ban_list) { + if (auto room = room_network.GetRoomMember().lock()) { + room->Unbind(callback_handle_ban_list); + } + } +} + +void ModerationDialog::LoadBanList() { + if (auto room = room_network.GetRoomMember().lock()) { + ui->refresh->setEnabled(false); + ui->refresh->setText(tr("Refreshing")); + ui->unban->setEnabled(false); + room->RequestBanList(); + } +} + +void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) { + model->removeRows(0, model->rowCount()); + for (const auto& username : ban_list.first) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username)); + QStandardItem* type_item = new QStandardItem(tr("Forum Username")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (const auto& ip : ban_list.second) { + QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip)); + QStandardItem* type_item = new QStandardItem(tr("IP Address")); + model->invisibleRootItem()->appendRow({subject_item, type_item}); + } + for (int i = 0; i < Column::COUNT - 1; ++i) { + ui->ban_list_view->resizeColumnToContents(i); + } + ui->refresh->setEnabled(true); + ui->refresh->setText(tr("Refresh")); + ui->unban->setEnabled(false); +} + +void ModerationDialog::SendUnbanRequest(const QString& subject) { + if (auto room = room_network.GetRoomMember().lock()) { + room->SendModerationRequest(Network::IdModUnban, subject.toStdString()); + } +} + +void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) { + if (status_message.type != Network::IdMemberBanned && + status_message.type != Network::IdAddressUnbanned) + return; + + // Update the ban list for ban/unban + LoadBanList(); +} diff --git a/src/yuzu/multiplayer/moderation_dialog.h b/src/yuzu/multiplayer/moderation_dialog.h new file mode 100644 index 000000000..e9e5daff7 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include <optional> +#include <QDialog> +#include "network/room.h" +#include "network/room_member.h" + +namespace Ui { +class ModerationDialog; +} + +class QStandardItemModel; + +class ModerationDialog : public QDialog { + Q_OBJECT + +public: + explicit ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent = nullptr); + ~ModerationDialog(); + +signals: + void StatusMessageReceived(const Network::StatusMessageEntry&); + void BanListReceived(const Network::Room::BanList&); + +private: + void LoadBanList(); + void PopulateBanList(const Network::Room::BanList& ban_list); + void SendUnbanRequest(const QString& subject); + void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message); + + std::unique_ptr<Ui::ModerationDialog> ui; + QStandardItemModel* model; + Network::RoomMember::CallbackHandle<Network::StatusMessageEntry> callback_handle_status_message; + Network::RoomMember::CallbackHandle<Network::Room::BanList> callback_handle_ban_list; + + Network::RoomNetwork& room_network; +}; + +Q_DECLARE_METATYPE(Network::Room::BanList); diff --git a/src/yuzu/multiplayer/moderation_dialog.ui b/src/yuzu/multiplayer/moderation_dialog.ui new file mode 100644 index 000000000..808d99414 --- /dev/null +++ b/src/yuzu/multiplayer/moderation_dialog.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ModerationDialog</class> + <widget class="QDialog" name="ModerationDialog"> + <property name="windowTitle"> + <string>Moderation</string> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>300</height> + </rect> + </property> + <layout class="QVBoxLayout"> + <item> + <widget class="QGroupBox" name="ban_list_group_box"> + <property name="title"> + <string>Ban List</string> + </property> + <layout class="QVBoxLayout"> + <item> + <layout class="QHBoxLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="refresh"> + <property name="text"> + <string>Refreshing</string> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="unban"> + <property name="text"> + <string>Unban</string> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeView" name="ban_list_view"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ModerationDialog</receiver> + <slot>accept()</slot> + </connection> + </connections> + <resources/> +</ui> diff --git a/src/yuzu/multiplayer/state.cpp b/src/yuzu/multiplayer/state.cpp new file mode 100644 index 000000000..4149b5232 --- /dev/null +++ b/src/yuzu/multiplayer/state.cpp @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <QAction> +#include <QApplication> +#include <QIcon> +#include <QMessageBox> +#include <QStandardItemModel> +#include "common/announce_multiplayer_room.h" +#include "common/logging/log.h" +#include "yuzu/game_list.h" +#include "yuzu/multiplayer/client_room.h" +#include "yuzu/multiplayer/direct_connect.h" +#include "yuzu/multiplayer/host_room.h" +#include "yuzu/multiplayer/lobby.h" +#include "yuzu/multiplayer/message.h" +#include "yuzu/multiplayer/state.h" +#include "yuzu/uisettings.h" +#include "yuzu/util/clickable_label.h" + +MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_, + QAction* leave_room_, QAction* show_room_, + Network::RoomNetwork& room_network_) + : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_), + show_room(show_room_), room_network{room_network_} { + if (auto member = room_network.GetRoomMember().lock()) { + // register the network structs to use in slots and signals + state_callback_handle = member->BindOnStateChanged( + [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); }); + connect(this, &MultiplayerState::NetworkStateChanged, this, + &MultiplayerState::OnNetworkStateChanged); + error_callback_handle = member->BindOnError( + [this](const Network::RoomMember::Error& error) { emit NetworkError(error); }); + connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError); + } + + qRegisterMetaType<Network::RoomMember::State>(); + qRegisterMetaType<Network::RoomMember::Error>(); + qRegisterMetaType<WebService::WebResult>(); + announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(room_network); + announce_multiplayer_session->BindErrorCallback( + [this](const WebService::WebResult& result) { emit AnnounceFailed(result); }); + connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed); + + status_text = new ClickableLabel(this); + status_icon = new ClickableLabel(this); + status_text->setToolTip(tr("Current connection status")); + status_text->setText(tr("Not Connected. Click here to find a room!")); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + + connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom); + + connect(static_cast<QApplication*>(QApplication::instance()), &QApplication::focusChanged, this, + [this](QWidget* /*old*/, QWidget* now) { + if (client_room && client_room->isAncestorOf(now)) { + HideNotification(); + } + }); +} + +MultiplayerState::~MultiplayerState() { + if (state_callback_handle) { + if (auto member = room_network.GetRoomMember().lock()) { + member->Unbind(state_callback_handle); + } + } + + if (error_callback_handle) { + if (auto member = room_network.GetRoomMember().lock()) { + member->Unbind(error_callback_handle); + } + } +} + +void MultiplayerState::Close() { + if (host_room) { + host_room->close(); + } + if (direct_connect) { + direct_connect->close(); + } + if (client_room) { + client_room->close(); + } + if (lobby) { + lobby->close(); + } +} + +void MultiplayerState::retranslateUi() { + status_text->setToolTip(tr("Current connection status")); + + if (current_state == Network::RoomMember::State::Uninitialized) { + status_text->setText(tr("Not Connected. Click here to find a room!")); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_text->setText(tr("Connected")); + } else { + status_text->setText(tr("Not Connected")); + } + + if (lobby) { + lobby->RetranslateUi(); + } + if (host_room) { + host_room->RetranslateUi(); + } + if (client_room) { + client_room->RetranslateUi(); + } + if (direct_connect) { + direct_connect->RetranslateUi(); + } +} + +void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) { + LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state)); + if (state == Network::RoomMember::State::Joined || + state == Network::RoomMember::State::Moderator) { + + OnOpenNetworkRoom(); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); + leave_room->setEnabled(true); + show_room->setEnabled(true); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + status_text->setText(tr("Not Connected")); + leave_room->setEnabled(false); + show_room->setEnabled(false); + } + + current_state = state; +} + +void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) { + LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error)); + switch (error) { + case Network::RoomMember::Error::LostConnection: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::LOST_CONNECTION); + break; + case Network::RoomMember::Error::HostKicked: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_KICKED); + break; + case Network::RoomMember::Error::CouldNotConnect: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::NameCollision: + NetworkMessage::ErrorManager::ShowError( + NetworkMessage::ErrorManager::USERNAME_NOT_VALID_SERVER); + break; + case Network::RoomMember::Error::MacCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::MAC_COLLISION); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::CONSOLE_ID_COLLISION); + break; + case Network::RoomMember::Error::RoomIsFull: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOM_IS_FULL); + break; + case Network::RoomMember::Error::WrongPassword: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_PASSWORD); + break; + case Network::RoomMember::Error::WrongVersion: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_VERSION); + break; + case Network::RoomMember::Error::HostBanned: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_BANNED); + break; + case Network::RoomMember::Error::UnknownError: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT); + break; + case Network::RoomMember::Error::PermissionDenied: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PERMISSION_DENIED); + break; + case Network::RoomMember::Error::NoSuchUser: + NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER); + break; + } +} + +void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) { + announce_multiplayer_session->Stop(); + QMessageBox::warning(this, tr("Error"), + tr("Failed to update the room information. Please check your Internet " + "connection and try hosting the room again.\nDebug Message: ") + + QString::fromStdString(result.result_string), + QMessageBox::Ok); +} + +void MultiplayerState::UpdateThemedIcons() { + if (show_notification) { + status_icon->setPixmap( + QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + } else if (current_state == Network::RoomMember::State::Joined || + current_state == Network::RoomMember::State::Moderator) { + + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + } else { + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16)); + } + if (client_room) + client_room->UpdateIconDisplay(); +} + +static void BringWidgetToFront(QWidget* widget) { + widget->show(); + widget->activateWindow(); + widget->raise(); +} + +void MultiplayerState::OnViewLobby() { + if (lobby == nullptr) { + lobby = new Lobby(this, game_list_model, announce_multiplayer_session, room_network); + } + BringWidgetToFront(lobby); +} + +void MultiplayerState::OnCreateRoom() { + if (host_room == nullptr) { + host_room = + new HostRoomWindow(this, game_list_model, announce_multiplayer_session, room_network); + } + BringWidgetToFront(host_room); +} + +bool MultiplayerState::OnCloseRoom() { + if (!NetworkMessage::WarnCloseRoom()) + return false; + if (auto room = room_network.GetRoom().lock()) { + // if you are in a room, leave it + if (auto member = room_network.GetRoomMember().lock()) { + member->Leave(); + LOG_DEBUG(Frontend, "Left the room (as a client)"); + } + + // if you are hosting a room, also stop hosting + if (room->GetState() != Network::Room::State::Open) { + return true; + } + // Save ban list + UISettings::values.multiplayer_ban_list = std::move(room->GetBanList()); + + room->Destroy(); + announce_multiplayer_session->Stop(); + LOG_DEBUG(Frontend, "Closed the room (as a server)"); + } + return true; +} + +void MultiplayerState::ShowNotification() { + if (client_room && client_room->isAncestorOf(QApplication::focusWidget())) + return; // Do not show notification if the chat window currently has focus + show_notification = true; + QApplication::alert(nullptr); + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16)); + status_text->setText(tr("New Messages Received")); +} + +void MultiplayerState::HideNotification() { + show_notification = false; + status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16)); + status_text->setText(tr("Connected")); +} + +void MultiplayerState::OnOpenNetworkRoom() { + if (auto member = room_network.GetRoomMember().lock()) { + if (member->IsConnected()) { + if (client_room == nullptr) { + client_room = new ClientRoomWindow(this, room_network); + connect(client_room, &ClientRoomWindow::ShowNotification, this, + &MultiplayerState::ShowNotification); + } + BringWidgetToFront(client_room); + return; + } + } + // If the user is not a member of a room, show the lobby instead. + // This is currently only used on the clickable label in the status bar + OnViewLobby(); +} + +void MultiplayerState::OnDirectConnectToRoom() { + if (direct_connect == nullptr) { + direct_connect = new DirectConnectWindow(room_network, this); + } + BringWidgetToFront(direct_connect); +} + +bool MultiplayerState::IsHostingPublicRoom() const { + return announce_multiplayer_session->IsRunning(); +} + +void MultiplayerState::UpdateCredentials() { + announce_multiplayer_session->UpdateCredentials(); +} + +void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) { + game_list_model = game_list; + if (lobby) { + lobby->UpdateGameList(game_list); + } + if (host_room) { + host_room->UpdateGameList(game_list); + } +} diff --git a/src/yuzu/multiplayer/state.h b/src/yuzu/multiplayer/state.h new file mode 100644 index 000000000..9c60712d5 --- /dev/null +++ b/src/yuzu/multiplayer/state.h @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QWidget> +#include "core/announce_multiplayer_session.h" +#include "network/network.h" + +class QStandardItemModel; +class Lobby; +class HostRoomWindow; +class ClientRoomWindow; +class DirectConnectWindow; +class ClickableLabel; + +class MultiplayerState : public QWidget { + Q_OBJECT; + +public: + explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room, + QAction* show_room, Network::RoomNetwork& room_network_); + ~MultiplayerState(); + + /** + * Close all open multiplayer related dialogs + */ + void Close(); + + ClickableLabel* GetStatusText() const { + return status_text; + } + + ClickableLabel* GetStatusIcon() const { + return status_icon; + } + + void retranslateUi(); + + /** + * Whether a public room is being hosted or not. + * When this is true, Web Services configuration should be disabled. + */ + bool IsHostingPublicRoom() const; + + void UpdateCredentials(); + + /** + * Updates the multiplayer dialogs with a new game list model. + * This model should be the original model of the game list. + */ + void UpdateGameList(QStandardItemModel* game_list); + +public slots: + void OnNetworkStateChanged(const Network::RoomMember::State& state); + void OnNetworkError(const Network::RoomMember::Error& error); + void OnViewLobby(); + void OnCreateRoom(); + bool OnCloseRoom(); + void OnOpenNetworkRoom(); + void OnDirectConnectToRoom(); + void OnAnnounceFailed(const WebService::WebResult&); + void UpdateThemedIcons(); + void ShowNotification(); + void HideNotification(); + +signals: + void NetworkStateChanged(const Network::RoomMember::State&); + void NetworkError(const Network::RoomMember::Error&); + void AnnounceFailed(const WebService::WebResult&); + +private: + Lobby* lobby = nullptr; + HostRoomWindow* host_room = nullptr; + ClientRoomWindow* client_room = nullptr; + DirectConnectWindow* direct_connect = nullptr; + ClickableLabel* status_icon = nullptr; + ClickableLabel* status_text = nullptr; + QStandardItemModel* game_list_model = nullptr; + QAction* leave_room; + QAction* show_room; + std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; + Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized; + bool has_mod_perms = false; + Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle; + Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle; + + bool show_notification = false; + Network::RoomNetwork& room_network; +}; + +Q_DECLARE_METATYPE(WebService::WebResult); diff --git a/src/yuzu/multiplayer/validation.h b/src/yuzu/multiplayer/validation.h new file mode 100644 index 000000000..7d48e589d --- /dev/null +++ b/src/yuzu/multiplayer/validation.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QRegExp> +#include <QString> +#include <QValidator> + +class Validation { +public: + Validation() + : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {} + + ~Validation() = default; + + const QValidator* GetRoomName() const { + return &room_name; + } + const QValidator* GetNickname() const { + return &nickname; + } + const QValidator* GetIP() const { + return &ip; + } + const QValidator* GetPort() const { + return &port; + } + +private: + /// room name can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp room_name_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator room_name; + + /// nickname can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20 + QRegExp nickname_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$")); + QRegExpValidator nickname; + + /// ipv4 address only + // TODO remove this when we support hostnames in direct connect + QRegExp ip_regex = QRegExp(QStringLiteral( + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|" + "2[0-4][0-9]|25[0-5])")); + QRegExpValidator ip; + + /// port must be between 0 and 65535 + QIntValidator port; +}; diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp new file mode 100644 index 000000000..8421280bf --- /dev/null +++ b/src/yuzu/startup_checks.cpp @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "video_core/vulkan_common/vulkan_wrapper.h" + +#ifdef _WIN32 +#include <cstring> // for memset, strncpy +#include <processthreadsapi.h> +#include <windows.h> +#elif defined(YUZU_UNIX) +#include <errno.h> +#include <sys/wait.h> +#include <unistd.h> +#endif + +#include <cstdio> +#include "video_core/vulkan_common/vulkan_instance.h" +#include "video_core/vulkan_common/vulkan_library.h" +#include "yuzu/startup_checks.h" + +void CheckVulkan() { + // Just start the Vulkan loader, this will crash if something is wrong + try { + Vulkan::vk::InstanceDispatch dld; + const Common::DynamicLibrary library = Vulkan::OpenLibrary(); + const Vulkan::vk::Instance instance = + Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_0); + + } catch (const Vulkan::vk::Exception& exception) { + std::fprintf(stderr, "Failed to initialize Vulkan: %s\n", exception.what()); + } +} + +bool StartupChecks(const char* arg0, bool* has_broken_vulkan) { +#ifdef _WIN32 + // Check environment variable to see if we are the child + char variable_contents[8]; + const DWORD startup_check_var = + GetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, variable_contents, 8); + if (startup_check_var > 0 && std::strncmp(variable_contents, "ON", 8) == 0) { + CheckVulkan(); + return true; + } + + // Set the startup variable for child processes + const bool env_var_set = SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, "ON"); + if (!env_var_set) { + std::fprintf(stderr, "SetEnvironmentVariableA failed to set %s with error %d\n", + STARTUP_CHECK_ENV_VAR, GetLastError()); + return false; + } + + PROCESS_INFORMATION process_info; + std::memset(&process_info, '\0', sizeof(process_info)); + + if (!SpawnChild(arg0, &process_info)) { + return false; + } + + // Wait until the processs exits and get exit code from it + WaitForSingleObject(process_info.hProcess, INFINITE); + DWORD exit_code = STILL_ACTIVE; + const int err = GetExitCodeProcess(process_info.hProcess, &exit_code); + if (err == 0) { + std::fprintf(stderr, "GetExitCodeProcess failed with error %d\n", GetLastError()); + } + + // Vulkan is broken if the child crashed (return value is not zero) + *has_broken_vulkan = (exit_code != 0); + + if (CloseHandle(process_info.hProcess) == 0) { + std::fprintf(stderr, "CloseHandle failed with error %d\n", GetLastError()); + } + if (CloseHandle(process_info.hThread) == 0) { + std::fprintf(stderr, "CloseHandle failed with error %d\n", GetLastError()); + } + + if (!SetEnvironmentVariableA(STARTUP_CHECK_ENV_VAR, nullptr)) { + std::fprintf(stderr, "SetEnvironmentVariableA failed to clear %s with error %d\n", + STARTUP_CHECK_ENV_VAR, GetLastError()); + } + +#elif defined(YUZU_UNIX) + const pid_t pid = fork(); + if (pid == 0) { + CheckVulkan(); + return true; + } else if (pid == -1) { + const int err = errno; + std::fprintf(stderr, "fork failed with error %d\n", err); + return false; + } + + // Get exit code from child process + int status; + const int r_val = wait(&status); + if (r_val == -1) { + const int err = errno; + std::fprintf(stderr, "wait failed with error %d\n", err); + return false; + } + // Vulkan is broken if the child crashed (return value is not zero) + *has_broken_vulkan = (status != 0); +#endif + return false; +} + +#ifdef _WIN32 +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi) { + STARTUPINFOA startup_info; + + std::memset(&startup_info, '\0', sizeof(startup_info)); + startup_info.cb = sizeof(startup_info); + + char p_name[255]; + std::strncpy(p_name, arg0, 255); + + const bool process_created = CreateProcessA(nullptr, // lpApplicationName + p_name, // lpCommandLine + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + false, // bInheritHandles + 0, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &startup_info, // lpStartupInfo + pi // lpProcessInformation + ); + if (!process_created) { + std::fprintf(stderr, "CreateProcessA failed with error %d\n", GetLastError()); + return false; + } + + return true; +} +#endif diff --git a/src/yuzu/startup_checks.h b/src/yuzu/startup_checks.h new file mode 100644 index 000000000..096dd54a8 --- /dev/null +++ b/src/yuzu/startup_checks.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#ifdef _WIN32 +#include <windows.h> +#endif + +constexpr char STARTUP_CHECK_ENV_VAR[] = "YUZU_DO_STARTUP_CHECKS"; + +void CheckVulkan(); +bool StartupChecks(const char* arg0, bool* has_broken_vulkan); + +#ifdef _WIN32 +bool SpawnChild(const char* arg0, PROCESS_INFORMATION* pi); +#endif diff --git a/src/yuzu/uisettings.cpp b/src/yuzu/uisettings.cpp index f683b80f7..2c1b547fb 100644 --- a/src/yuzu/uisettings.cpp +++ b/src/yuzu/uisettings.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include "yuzu/uisettings.h" diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 044d88ca6..25d1bf1e6 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -78,7 +77,7 @@ struct Values { Settings::Setting<bool> mute_when_in_background{false, "muteWhenInBackground"}; Settings::Setting<bool> hide_mouse{true, "hideInactiveMouse"}; // Set when Vulkan is known to crash the application - Settings::Setting<bool> has_broken_vulkan{false, "has_broken_vulkan"}; + bool has_broken_vulkan = false; Settings::Setting<bool> select_user_on_boot{false, "select_user_on_boot"}; @@ -102,6 +101,19 @@ struct Values { Settings::Setting<uint32_t> callout_flags{0, "calloutFlags"}; + // multiplayer settings + Settings::Setting<QString> multiplayer_nickname{QStringLiteral("yuzu"), "nickname"}; + Settings::Setting<QString> multiplayer_ip{{}, "ip"}; + Settings::SwitchableSetting<uint, true> multiplayer_port{24872, 0, 65535, "port"}; + Settings::Setting<QString> multiplayer_room_nickname{{}, "room_nickname"}; + Settings::Setting<QString> multiplayer_room_name{{}, "room_name"}; + Settings::SwitchableSetting<uint, true> multiplayer_max_player{8, 0, 8, "max_player"}; + Settings::SwitchableSetting<uint, true> multiplayer_room_port{24872, 0, 65535, "room_port"}; + Settings::SwitchableSetting<uint, true> multiplayer_host_type{0, 0, 1, "host_type"}; + Settings::Setting<qulonglong> multiplayer_game_id{{}, "game_id"}; + Settings::Setting<QString> multiplayer_room_description{{}, "room_description"}; + std::pair<std::vector<std::string>, std::vector<std::string>> multiplayer_ban_list; + // logging Settings::Setting<bool> show_console{false, "showConsole"}; diff --git a/src/yuzu/util/clickable_label.cpp b/src/yuzu/util/clickable_label.cpp new file mode 100644 index 000000000..89d14190a --- /dev/null +++ b/src/yuzu/util/clickable_label.cpp @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "yuzu/util/clickable_label.h" + +ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f) + : QLabel(parent) {} + +void ClickableLabel::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { + emit clicked(); +} diff --git a/src/yuzu/util/clickable_label.h b/src/yuzu/util/clickable_label.h new file mode 100644 index 000000000..4fe744150 --- /dev/null +++ b/src/yuzu/util/clickable_label.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <QLabel> +#include <QWidget> + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() = default; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent* event); +}; diff --git a/src/yuzu/util/sequence_dialog/sequence_dialog.cpp b/src/yuzu/util/sequence_dialog/sequence_dialog.cpp index bb5f74ec4..4b10fa517 100644 --- a/src/yuzu/util/sequence_dialog/sequence_dialog.cpp +++ b/src/yuzu/util/sequence_dialog/sequence_dialog.cpp @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <QDialogButtonBox> #include <QKeySequenceEdit> diff --git a/src/yuzu/util/sequence_dialog/sequence_dialog.h b/src/yuzu/util/sequence_dialog/sequence_dialog.h index 969c77740..85e146d40 100644 --- a/src/yuzu/util/sequence_dialog/sequence_dialog.h +++ b/src/yuzu/util/sequence_dialog/sequence_dialog.h @@ -1,6 +1,5 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index ef31bc2d2..5c3e4589e 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <array> #include <cmath> diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index e6790f260..39dd2d895 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -1,6 +1,5 @@ -// Copyright 2015 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2015 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu/yuzu.qrc b/src/yuzu/yuzu.qrc index 5733cac98..855df05fd 100644 --- a/src/yuzu/yuzu.qrc +++ b/src/yuzu/yuzu.qrc @@ -1,3 +1,8 @@ +<!-- +SPDX-FileCopyrightText: 2021 yuzu Emulator Project +SPDX-License-Identifier: GPL-2.0-or-later +--> + <RCC> <qresource prefix="/img"> <file alias="yuzu.ico">../../dist/yuzu.ico</file> diff --git a/src/yuzu/yuzu.rc b/src/yuzu/yuzu.rc index 4a3645a71..1fc74d065 100644 --- a/src/yuzu/yuzu.rc +++ b/src/yuzu/yuzu.rc @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #include "winresrc.h" ///////////////////////////////////////////////////////////////////////////// // diff --git a/src/yuzu_cmd/CMakeLists.txt b/src/yuzu_cmd/CMakeLists.txt index c8901f2df..7d8ca3d8a 100644 --- a/src/yuzu_cmd/CMakeLists.txt +++ b/src/yuzu_cmd/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2018 yuzu Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) # Credits to Samantas5855 and others for this function. diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index ad7f9d239..bd0fb75f8 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <memory> #include <optional> diff --git a/src/yuzu_cmd/config.h b/src/yuzu_cmd/config.h index 32c03075f..021438b17 100644 --- a/src/yuzu_cmd/config.h +++ b/src/yuzu_cmd/config.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index d9a2a460c..1168cf136 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h @@ -1,6 +1,5 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp index 8e38724db..4ac72c2f6 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.cpp @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <SDL.h> diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl2.h b/src/yuzu_cmd/emu_window/emu_window_sdl2.h index 58b885465..90bb0b415 100644 --- a/src/yuzu_cmd/emu_window/emu_window_sdl2.h +++ b/src/yuzu_cmd/emu_window/emu_window_sdl2.h @@ -1,6 +1,5 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #pragma once diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index cb301e78b..003890c07 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -1,10 +1,10 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. +// SPDX-FileCopyrightText: 2014 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later #include <chrono> #include <iostream> #include <memory> +#include <regex> #include <string> #include <thread> @@ -29,6 +29,7 @@ #include "core/loader/loader.h" #include "core/telemetry_session.h" #include "input_common/main.h" +#include "network/network.h" #include "video_core/renderer_base.h" #include "yuzu_cmd/config.h" #include "yuzu_cmd/emu_window/emu_window_sdl2.h" @@ -60,6 +61,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; static void PrintHelp(const char* argv0) { std::cout << "Usage: " << argv0 << " [options] <filename>\n" + "-m, --multiplayer=nick:password@address:port" + " Nickname, password, address and port for multiplayer\n" "-f, --fullscreen Start in fullscreen mode\n" "-h, --help Display this help and exit\n" "-v, --version Output version information and exit\n" @@ -71,6 +74,107 @@ static void PrintVersion() { std::cout << "yuzu " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; } +static void OnStateChanged(const Network::RoomMember::State& state) { + switch (state) { + case Network::RoomMember::State::Idle: + LOG_DEBUG(Network, "Network is idle"); + break; + case Network::RoomMember::State::Joining: + LOG_DEBUG(Network, "Connection sequence to room started"); + break; + case Network::RoomMember::State::Joined: + LOG_DEBUG(Network, "Successfully joined to the room"); + break; + case Network::RoomMember::State::Moderator: + LOG_DEBUG(Network, "Successfully joined the room as a moderator"); + break; + default: + break; + } +} + +static void OnNetworkError(const Network::RoomMember::Error& error) { + switch (error) { + case Network::RoomMember::Error::LostConnection: + LOG_DEBUG(Network, "Lost connection to the room"); + break; + case Network::RoomMember::Error::CouldNotConnect: + LOG_ERROR(Network, "Error: Could not connect"); + exit(1); + break; + case Network::RoomMember::Error::NameCollision: + LOG_ERROR( + Network, + "You tried to use the same nickname as another user that is connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::MacCollision: + LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " + "connected to the Room"); + exit(1); + break; + case Network::RoomMember::Error::ConsoleIdCollision: + LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); + exit(1); + break; + case Network::RoomMember::Error::WrongPassword: + LOG_ERROR(Network, "Room replied with: Wrong password"); + exit(1); + break; + case Network::RoomMember::Error::WrongVersion: + LOG_ERROR(Network, + "You are using a different version than the room you are trying to connect to"); + exit(1); + break; + case Network::RoomMember::Error::RoomIsFull: + LOG_ERROR(Network, "The room is full"); + exit(1); + break; + case Network::RoomMember::Error::HostKicked: + LOG_ERROR(Network, "You have been kicked by the host"); + break; + case Network::RoomMember::Error::HostBanned: + LOG_ERROR(Network, "You have been banned by the host"); + break; + case Network::RoomMember::Error::UnknownError: + LOG_ERROR(Network, "UnknownError"); + break; + case Network::RoomMember::Error::PermissionDenied: + LOG_ERROR(Network, "PermissionDenied"); + break; + case Network::RoomMember::Error::NoSuchUser: + LOG_ERROR(Network, "NoSuchUser"); + break; + } +} + +static void OnMessageReceived(const Network::ChatEntry& msg) { + std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; +} + +static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { + std::string message; + switch (msg.type) { + case Network::IdMemberJoin: + message = fmt::format("{} has joined", msg.nickname); + break; + case Network::IdMemberLeave: + message = fmt::format("{} has left", msg.nickname); + break; + case Network::IdMemberKicked: + message = fmt::format("{} has been kicked", msg.nickname); + break; + case Network::IdMemberBanned: + message = fmt::format("{} has been banned", msg.nickname); + break; + case Network::IdAddressUnbanned: + message = fmt::format("{} has been unbanned", msg.nickname); + break; + } + if (!message.empty()) + std::cout << std::endl << "* " << message << std::endl << std::endl; +} + /// Application entry point int main(int argc, char** argv) { Common::Log::Initialize(); @@ -92,10 +196,16 @@ int main(int argc, char** argv) { std::optional<std::string> config_path; std::string program_args; + bool use_multiplayer = false; bool fullscreen = false; + std::string nickname{}; + std::string password{}; + std::string address{}; + u16 port = Network::DefaultRoomPort; static struct option long_options[] = { // clang-format off + {"multiplayer", required_argument, 0, 'm'}, {"fullscreen", no_argument, 0, 'f'}, {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'v'}, @@ -109,6 +219,38 @@ int main(int argc, char** argv) { int arg = getopt_long(argc, argv, "g:fhvp::c:", long_options, &option_index); if (arg != -1) { switch (static_cast<char>(arg)) { + case 'm': { + use_multiplayer = true; + const std::string str_arg(optarg); + // regex to check if the format is nickname:password@ip:port + // with optional :password + const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); + if (!std::regex_match(str_arg, re)) { + std::cout << "Wrong format for option --multiplayer\n"; + PrintHelp(argv[0]); + return 0; + } + + std::smatch match; + std::regex_search(str_arg, match, re); + ASSERT(match.size() == 5); + nickname = match[1]; + password = match[2]; + address = match[3]; + if (!match[4].str().empty()) + port = std::stoi(match[4]); + std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); + if (!std::regex_match(nickname, nickname_re)) { + std::cout + << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; + return 0; + } + if (address.empty()) { + std::cout << "Address to room must not be empty.\n"; + return 0; + } + break; + } case 'f': fullscreen = true; LOG_INFO(Frontend, "Starting in fullscreen mode..."); @@ -215,6 +357,21 @@ int main(int argc, char** argv) { system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); + if (use_multiplayer) { + if (auto member = system.GetRoomNetwork().GetRoomMember().lock()) { + member->BindOnChatMessageRecieved(OnMessageReceived); + member->BindOnStatusMessageReceived(OnStatusMessageReceived); + member->BindOnStateChanged(OnStateChanged); + member->BindOnError(OnNetworkError); + LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, + nickname); + member->Join(nickname, "", address.c_str(), port, 0, Network::NoPreferredMac, password); + } else { + LOG_ERROR(Network, "Could not access RoomMember"); + return 0; + } + } + // Core is loaded, start the GPU (makes the GPU contexts current to this thread) system.GPU().Start(); system.GetCpuManager().OnGpuReady(); diff --git a/src/yuzu_cmd/yuzu.rc b/src/yuzu_cmd/yuzu.rc index 0cde75e2f..e230cf680 100644 --- a/src/yuzu_cmd/yuzu.rc +++ b/src/yuzu_cmd/yuzu.rc @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + #include "winresrc.h" ///////////////////////////////////////////////////////////////////////////// // |