summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar liamwhite2022-07-25 18:31:45 -0400
committerGravatar GitHub2022-07-25 18:31:45 -0400
commit1e67d2b59f6dfd561768db3fb9a8e0c6a16ec9f2 (patch)
tree999411f1ca76390654d1034c6d0bd2c47c3f101c
parentMerge pull request #8564 from lat9nq/dinner-fork (diff)
parentnetwork: Address review comments (diff)
downloadyuzu-1e67d2b59f6dfd561768db3fb9a8e0c6a16ec9f2.tar.gz
yuzu-1e67d2b59f6dfd561768db3fb9a8e0c6a16ec9f2.tar.xz
yuzu-1e67d2b59f6dfd561768db3fb9a8e0c6a16ec9f2.zip
Merge pull request #8541 from FearlessTobi/multiplayer-part1
yuzu, network: Add room service and UI configuration
-rw-r--r--.gitmodules6
-rw-r--r--dist/license.md9
-rw-r--r--dist/qt_themes/colorful/icons/16x16/connected.pngbin0 -> 362 bytes
-rw-r--r--dist/qt_themes/colorful/icons/16x16/connected_notification.pngbin0 -> 607 bytes
-rw-r--r--dist/qt_themes/colorful/icons/16x16/disconnected.pngbin0 -> 784 bytes
-rw-r--r--dist/qt_themes/colorful/style.qrc3
-rw-r--r--dist/qt_themes/colorful_dark/style.qrc4
-rw-r--r--dist/qt_themes/default/default.qrc4
-rw-r--r--dist/qt_themes/default/icons/16x16/connected.pngbin0 -> 269 bytes
-rw-r--r--dist/qt_themes/default/icons/16x16/connected_notification.pngbin0 -> 517 bytes
-rw-r--r--dist/qt_themes/default/icons/16x16/disconnected.pngbin0 -> 306 bytes
-rw-r--r--dist/qt_themes/default/icons/48x48/no_avatar.pngbin0 -> 588 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/16x16/connected.pngbin0 -> 397 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.pngbin0 -> 526 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/16x16/disconnected.pngbin0 -> 444 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.pngbin0 -> 708 bytes
-rw-r--r--dist/qt_themes/qdarkstyle/style.qrc4
-rw-r--r--externals/CMakeLists.txt9
m---------externals/cpp-jwt0
m---------externals/enet0
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/common/CMakeLists.txt1
-rw-r--r--src/common/announce_multiplayer_room.h143
-rw-r--r--src/core/CMakeLists.txt14
-rw-r--r--src/core/announce_multiplayer_session.cpp164
-rw-r--r--src/core/announce_multiplayer_session.h98
-rw-r--r--src/core/core.cpp31
-rw-r--r--src/core/core.h10
-rw-r--r--src/core/hle/service/nifm/nifm.cpp4
-rw-r--r--src/core/hle/service/sockets/bsd.cpp4
-rw-r--r--src/core/hle/service/sockets/bsd.h2
-rw-r--r--src/core/hle/service/sockets/sockets_translate.cpp2
-rw-r--r--src/core/hle/service/sockets/sockets_translate.h2
-rw-r--r--src/core/internal_network/network.cpp (renamed from src/core/network/network.cpp)6
-rw-r--r--src/core/internal_network/network.h (renamed from src/core/network/network.h)0
-rw-r--r--src/core/internal_network/network_interface.cpp (renamed from src/core/network/network_interface.cpp)2
-rw-r--r--src/core/internal_network/network_interface.h (renamed from src/core/network/network_interface.h)0
-rw-r--r--src/core/internal_network/sockets.h (renamed from src/core/network/sockets.h)3
-rw-r--r--src/input_common/helpers/udp_protocol.h2
-rw-r--r--src/network/CMakeLists.txt16
-rw-r--r--src/network/network.cpp50
-rw-r--r--src/network/network.h33
-rw-r--r--src/network/packet.cpp262
-rw-r--r--src/network/packet.h165
-rw-r--r--src/network/room.cpp1110
-rw-r--r--src/network/room.h151
-rw-r--r--src/network/room_member.cpp696
-rw-r--r--src/network/room_member.h318
-rw-r--r--src/network/verify_user.cpp17
-rw-r--r--src/network/verify_user.h45
-rw-r--r--src/tests/CMakeLists.txt2
-rw-r--r--src/tests/core/internal_network/network.cpp (renamed from src/tests/core/network/network.cpp)4
-rw-r--r--src/web_service/CMakeLists.txt6
-rw-r--r--src/web_service/announce_room_json.cpp145
-rw-r--r--src/web_service/announce_room_json.h41
-rw-r--r--src/web_service/verify_user_jwt.cpp67
-rw-r--r--src/web_service/verify_user_jwt.h26
-rw-r--r--src/yuzu/CMakeLists.txt32
-rw-r--r--src/yuzu/configuration/config.cpp75
-rw-r--r--src/yuzu/configuration/config.h2
-rw-r--r--src/yuzu/configuration/configure_dialog.cpp8
-rw-r--r--src/yuzu/configuration/configure_dialog.h3
-rw-r--r--src/yuzu/configuration/configure_network.cpp2
-rw-r--r--src/yuzu/configuration/configure_web.cpp5
-rw-r--r--src/yuzu/configuration/configure_web.h1
-rw-r--r--src/yuzu/configuration/configure_web.ui10
-rw-r--r--src/yuzu/game_list.cpp6
-rw-r--r--src/yuzu/game_list.h8
-rw-r--r--src/yuzu/main.cpp40
-rw-r--r--src/yuzu/main.h5
-rw-r--r--src/yuzu/main.ui38
-rw-r--r--src/yuzu/multiplayer/chat_room.cpp491
-rw-r--r--src/yuzu/multiplayer/chat_room.h75
-rw-r--r--src/yuzu/multiplayer/chat_room.ui59
-rw-r--r--src/yuzu/multiplayer/client_room.cpp115
-rw-r--r--src/yuzu/multiplayer/client_room.h39
-rw-r--r--src/yuzu/multiplayer/client_room.ui80
-rw-r--r--src/yuzu/multiplayer/direct_connect.cpp130
-rw-r--r--src/yuzu/multiplayer/direct_connect.h43
-rw-r--r--src/yuzu/multiplayer/direct_connect.ui168
-rw-r--r--src/yuzu/multiplayer/host_room.cpp246
-rw-r--r--src/yuzu/multiplayer/host_room.h75
-rw-r--r--src/yuzu/multiplayer/host_room.ui207
-rw-r--r--src/yuzu/multiplayer/lobby.cpp367
-rw-r--r--src/yuzu/multiplayer/lobby.h128
-rw-r--r--src/yuzu/multiplayer/lobby.ui123
-rw-r--r--src/yuzu/multiplayer/lobby_p.h238
-rw-r--r--src/yuzu/multiplayer/message.cpp78
-rw-r--r--src/yuzu/multiplayer/message.h64
-rw-r--r--src/yuzu/multiplayer/moderation_dialog.cpp112
-rw-r--r--src/yuzu/multiplayer/moderation_dialog.h43
-rw-r--r--src/yuzu/multiplayer/moderation_dialog.ui84
-rw-r--r--src/yuzu/multiplayer/state.cpp308
-rw-r--r--src/yuzu/multiplayer/state.h92
-rw-r--r--src/yuzu/multiplayer/validation.h48
-rw-r--r--src/yuzu/uisettings.h13
-rw-r--r--src/yuzu/util/clickable_label.cpp11
-rw-r--r--src/yuzu/util/clickable_label.h21
-rw-r--r--src/yuzu_cmd/yuzu.cpp158
99 files changed, 7501 insertions, 32 deletions
diff --git a/.gitmodules b/.gitmodules
index 3c0d15951..26c6735cd 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
1[submodule "enet"]
2 path = externals/enet
3 url = https://github.com/lsalzman/enet.git
1[submodule "inih"] 4[submodule "inih"]
2 path = externals/inih/inih 5 path = externals/inih/inih
3 url = https://github.com/benhoyt/inih.git 6 url = https://github.com/benhoyt/inih.git
@@ -43,3 +46,6 @@
43[submodule "vcpkg"] 46[submodule "vcpkg"]
44 path = externals/vcpkg 47 path = externals/vcpkg
45 url = https://github.com/Microsoft/vcpkg.git 48 url = https://github.com/Microsoft/vcpkg.git
49[submodule "cpp-jwt"]
50 path = externals/cpp-jwt
51 url = https://github.com/arun11299/cpp-jwt.git
diff --git a/dist/license.md b/dist/license.md
index 7bdebfec1..c5ebe9c00 100644
--- a/dist/license.md
+++ b/dist/license.md
@@ -3,6 +3,9 @@ The icons in this folder and its subfolders have the following licenses:
3Icon Name | License | Origin/Author 3Icon Name | License | Origin/Author
4--- | --- | --- 4--- | --- | ---
5qt_themes/default/icons/16x16/checked.png | CC BY-ND 3.0 | https://icons8.com 5qt_themes/default/icons/16x16/checked.png | CC BY-ND 3.0 | https://icons8.com
6qt_themes/default/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
7qt_themes/default/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
8qt_themes/default/icons/16x16/disconnected.png | CC BY-ND 3.0 | https://icons8.com
6qt_themes/default/icons/16x16/failed.png | CC BY-ND 3.0 | https://icons8.com 9qt_themes/default/icons/16x16/failed.png | CC BY-ND 3.0 | https://icons8.com
7qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com 10qt_themes/default/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
8qt_themes/default/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io 11qt_themes/default/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io
@@ -10,18 +13,24 @@ qt_themes/default/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.
10qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com 13qt_themes/default/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
11qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com 14qt_themes/default/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
12qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com 15qt_themes/default/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
16qt_themes/default/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com
13qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team 17qt_themes/default/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
14qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com 18qt_themes/default/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
15qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com 19qt_themes/default/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
20qt_themes/qdarkstyle/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
21qt_themes/qdarkstyle/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
16qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com 22qt_themes/qdarkstyle/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
17qt_themes/qdarkstyle/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io 23qt_themes/qdarkstyle/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io
18qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com 24qt_themes/qdarkstyle/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
19qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com 25qt_themes/qdarkstyle/icons/48x48/bad_folder.png | CC BY-ND 3.0 | https://icons8.com
20qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com 26qt_themes/qdarkstyle/icons/48x48/chip.png | CC BY-ND 3.0 | https://icons8.com
21qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com 27qt_themes/qdarkstyle/icons/48x48/folder.png | CC BY-ND 3.0 | https://icons8.com
28qt_themes/qdarkstyle/icons/48x48/no_avatar.png | CC BY-ND 3.0 | https://icons8.com
22qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team 29qt_themes/qdarkstyle/icons/48x48/plus.png | CC0 1.0 | Designed by BreadFish64 from the Citra team
23qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com 30qt_themes/qdarkstyle/icons/48x48/sd_card.png | CC BY-ND 3.0 | https://icons8.com
24qt_themes/qdarkstyle/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com 31qt_themes/qdarkstyle/icons/48x48/star.png | CC BY-ND 3.0 | https://icons8.com
32qt_themes/colorful/icons/16x16/connected.png | CC BY-ND 3.0 | https://icons8.com
33qt_themes/colorful/icons/16x16/connected_notification.png | CC BY-ND 3.0 | https://icons8.com
25qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com 34qt_themes/colorful/icons/16x16/lock.png | CC BY-ND 3.0 | https://icons8.com
26qt_themes/colorful/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io 35qt_themes/colorful/icons/16x16/view-refresh.png | Apache 2.0 | https://material.io
27qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com 36qt_themes/colorful/icons/256x256/plus_folder.png | CC BY-ND 3.0 | https://icons8.com
diff --git a/dist/qt_themes/colorful/icons/16x16/connected.png b/dist/qt_themes/colorful/icons/16x16/connected.png
new file mode 100644
index 000000000..d6052f1a0
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/16x16/connected.png
Binary files differ
diff --git a/dist/qt_themes/colorful/icons/16x16/connected_notification.png b/dist/qt_themes/colorful/icons/16x16/connected_notification.png
new file mode 100644
index 000000000..0dfe032d5
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/16x16/connected_notification.png
Binary files differ
diff --git a/dist/qt_themes/colorful/icons/16x16/disconnected.png b/dist/qt_themes/colorful/icons/16x16/disconnected.png
new file mode 100644
index 000000000..bacee3aeb
--- /dev/null
+++ b/dist/qt_themes/colorful/icons/16x16/disconnected.png
Binary files differ
diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc
index 18b10869e..bd7898eb1 100644
--- a/dist/qt_themes/colorful/style.qrc
+++ b/dist/qt_themes/colorful/style.qrc
@@ -1,6 +1,9 @@
1<RCC> 1<RCC>
2 <qresource prefix="icons/colorful"> 2 <qresource prefix="icons/colorful">
3 <file alias="index.theme">icons/index.theme</file> 3 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/connected.png">icons/16x16/connected.png</file>
5 <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file>
6 <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file> 7 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> 8 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
6 <file alias="48x48/chip.png">icons/48x48/chip.png</file> 9 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
diff --git a/dist/qt_themes/colorful_dark/style.qrc b/dist/qt_themes/colorful_dark/style.qrc
index 0abcb4e83..602c71b32 100644
--- a/dist/qt_themes/colorful_dark/style.qrc
+++ b/dist/qt_themes/colorful_dark/style.qrc
@@ -1,11 +1,15 @@
1<RCC> 1<RCC>
2 <qresource prefix="icons/colorful_dark"> 2 <qresource prefix="icons/colorful_dark">
3 <file alias="16x16/connected.png">../colorful/icons/16x16/connected.png</file>
4 <file alias="16x16/connected_notification.png">../colorful/icons/16x16/connected_notification.png</file>
5 <file alias="16x16/disconnected.png">../colorful/icons/16x16/disconnected.png</file>
3 <file alias="index.theme">icons/index.theme</file> 6 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file> 7 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> 8 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file>
6 <file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file> 9 <file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
7 <file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file> 10 <file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
8 <file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file> 11 <file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
12 <file alias="48x48/no_avatar.png">../qdarkstyle/icons/48x48/no_avatar.png</file>
9 <file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file> 13 <file alias="48x48/plus.png">../colorful/icons/48x48/plus.png</file>
10 <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file> 14 <file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>
11 <file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file> 15 <file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file>
diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc
index b195747a3..41842e5d8 100644
--- a/dist/qt_themes/default/default.qrc
+++ b/dist/qt_themes/default/default.qrc
@@ -4,10 +4,14 @@
4 <file alias="16x16/checked.png">icons/16x16/checked.png</file> 4 <file alias="16x16/checked.png">icons/16x16/checked.png</file>
5 <file alias="16x16/failed.png">icons/16x16/failed.png</file> 5 <file alias="16x16/failed.png">icons/16x16/failed.png</file>
6 <file alias="16x16/lock.png">icons/16x16/lock.png</file> 6 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
7 <file alias="16x16/connected.png">icons/16x16/connected.png</file>
8 <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file>
9 <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file>
7 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> 10 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file>
8 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> 11 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
9 <file alias="48x48/chip.png">icons/48x48/chip.png</file> 12 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
10 <file alias="48x48/folder.png">icons/48x48/folder.png</file> 13 <file alias="48x48/folder.png">icons/48x48/folder.png</file>
14 <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
11 <file alias="48x48/plus.png">icons/48x48/plus.png</file> 15 <file alias="48x48/plus.png">icons/48x48/plus.png</file>
12 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> 16 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
13 <file alias="48x48/star.png">icons/48x48/star.png</file> 17 <file alias="48x48/star.png">icons/48x48/star.png</file>
diff --git a/dist/qt_themes/default/icons/16x16/connected.png b/dist/qt_themes/default/icons/16x16/connected.png
new file mode 100644
index 000000000..afa797394
--- /dev/null
+++ b/dist/qt_themes/default/icons/16x16/connected.png
Binary files differ
diff --git a/dist/qt_themes/default/icons/16x16/connected_notification.png b/dist/qt_themes/default/icons/16x16/connected_notification.png
new file mode 100644
index 000000000..e64901378
--- /dev/null
+++ b/dist/qt_themes/default/icons/16x16/connected_notification.png
Binary files differ
diff --git a/dist/qt_themes/default/icons/16x16/disconnected.png b/dist/qt_themes/default/icons/16x16/disconnected.png
new file mode 100644
index 000000000..835b1f0d6
--- /dev/null
+++ b/dist/qt_themes/default/icons/16x16/disconnected.png
Binary files differ
diff --git a/dist/qt_themes/default/icons/48x48/no_avatar.png b/dist/qt_themes/default/icons/48x48/no_avatar.png
new file mode 100644
index 000000000..d4bf82026
--- /dev/null
+++ b/dist/qt_themes/default/icons/48x48/no_avatar.png
Binary files differ
diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/connected.png b/dist/qt_themes/qdarkstyle/icons/16x16/connected.png
new file mode 100644
index 000000000..90feb372a
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/16x16/connected.png
Binary files differ
diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png b/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png
new file mode 100644
index 000000000..7cd8b9d29
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/16x16/connected_notification.png
Binary files differ
diff --git a/dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png b/dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png
new file mode 100644
index 000000000..fc5f23894
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/16x16/disconnected.png
Binary files differ
diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png b/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png
new file mode 100644
index 000000000..43e0dd267
--- /dev/null
+++ b/dist/qt_themes/qdarkstyle/icons/48x48/no_avatar.png
Binary files differ
diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc
index 34e872d25..f770e09fd 100644
--- a/dist/qt_themes/qdarkstyle/style.qrc
+++ b/dist/qt_themes/qdarkstyle/style.qrc
@@ -1,11 +1,15 @@
1<RCC> 1<RCC>
2 <qresource prefix="icons/qdarkstyle"> 2 <qresource prefix="icons/qdarkstyle">
3 <file alias="index.theme">icons/index.theme</file> 3 <file alias="index.theme">icons/index.theme</file>
4 <file alias="16x16/connected.png">icons/16x16/connected.png</file>
5 <file alias="16x16/disconnected.png">icons/16x16/disconnected.png</file>
6 <file alias="16x16/connected_notification.png">icons/16x16/connected_notification.png</file>
4 <file alias="16x16/lock.png">icons/16x16/lock.png</file> 7 <file alias="16x16/lock.png">icons/16x16/lock.png</file>
5 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file> 8 <file alias="16x16/view-refresh.png">icons/16x16/view-refresh.png</file>
6 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file> 9 <file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
7 <file alias="48x48/chip.png">icons/48x48/chip.png</file> 10 <file alias="48x48/chip.png">icons/48x48/chip.png</file>
8 <file alias="48x48/folder.png">icons/48x48/folder.png</file> 11 <file alias="48x48/folder.png">icons/48x48/folder.png</file>
12 <file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
9 <file alias="48x48/plus.png">icons/48x48/plus.png</file> 13 <file alias="48x48/plus.png">icons/48x48/plus.png</file>
10 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file> 14 <file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
11 <file alias="48x48/star.png">icons/48x48/star.png</file> 15 <file alias="48x48/star.png">icons/48x48/star.png</file>
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index bd01f4c4d..5b74a1773 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -73,6 +73,10 @@ if (YUZU_USE_EXTERNAL_SDL2)
73 add_library(SDL2 ALIAS SDL2-static) 73 add_library(SDL2 ALIAS SDL2-static)
74endif() 74endif()
75 75
76# ENet
77add_subdirectory(enet)
78target_include_directories(enet INTERFACE ./enet/include)
79
76# Cubeb 80# Cubeb
77if(ENABLE_CUBEB) 81if(ENABLE_CUBEB)
78 set(BUILD_TESTS OFF CACHE BOOL "") 82 set(BUILD_TESTS OFF CACHE BOOL "")
@@ -112,6 +116,11 @@ if (ENABLE_WEB_SERVICE)
112 if (WIN32) 116 if (WIN32)
113 target_link_libraries(httplib INTERFACE crypt32 cryptui ws2_32) 117 target_link_libraries(httplib INTERFACE crypt32 cryptui ws2_32)
114 endif() 118 endif()
119
120 # cpp-jwt
121 add_library(cpp-jwt INTERFACE)
122 target_include_directories(cpp-jwt INTERFACE ./cpp-jwt/include)
123 target_compile_definitions(cpp-jwt INTERFACE CPP_JWT_USE_VENDORED_NLOHMANN_JSON)
115endif() 124endif()
116 125
117# Opus 126# Opus
diff --git a/externals/cpp-jwt b/externals/cpp-jwt
new file mode 160000
Subproject e12ef06218596b52d9b5d6e1639484866a8e706
diff --git a/externals/enet b/externals/enet
new file mode 160000
Subproject 39a72ab1990014eb399cee9d538fd529df99c6a
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 39ae573b2..9367f67c1 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -156,6 +156,7 @@ add_subdirectory(common)
156add_subdirectory(core) 156add_subdirectory(core)
157add_subdirectory(audio_core) 157add_subdirectory(audio_core)
158add_subdirectory(video_core) 158add_subdirectory(video_core)
159add_subdirectory(network)
159add_subdirectory(input_common) 160add_subdirectory(input_common)
160add_subdirectory(shader_recompiler) 161add_subdirectory(shader_recompiler)
161 162
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index d574e4b79..05fdfea82 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -41,6 +41,7 @@ add_custom_command(OUTPUT scm_rev.cpp
41add_library(common STATIC 41add_library(common STATIC
42 algorithm.h 42 algorithm.h
43 alignment.h 43 alignment.h
44 announce_multiplayer_room.h
44 assert.cpp 45 assert.cpp
45 assert.h 46 assert.h
46 atomic_helpers.h 47 atomic_helpers.h
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <array>
7#include <functional>
8#include <string>
9#include <vector>
10#include "common/common_types.h"
11#include "web_service/web_result.h"
12
13namespace AnnounceMultiplayerRoom {
14
15using MacAddress = std::array<u8, 6>;
16
17struct GameInfo {
18 std::string name{""};
19 u64 id{0};
20};
21
22struct Member {
23 std::string username;
24 std::string nickname;
25 std::string display_name;
26 std::string avatar_url;
27 MacAddress mac_address;
28 GameInfo game;
29};
30
31struct RoomInformation {
32 std::string name; ///< Name of the server
33 std::string description; ///< Server description
34 u32 member_slots; ///< Maximum number of members in this room
35 u16 port; ///< The port of this room
36 GameInfo preferred_game; ///< Game to advertise that you want to play
37 std::string host_username; ///< Forum username of the host
38 bool enable_yuzu_mods; ///< Allow yuzu Moderators to moderate on this room
39};
40
41struct Room {
42 RoomInformation information;
43
44 std::string id;
45 std::string verify_uid; ///< UID used for verification
46 std::string ip;
47 u32 net_version;
48 bool has_password;
49
50 std::vector<Member> members;
51};
52using RoomList = std::vector<Room>;
53
54/**
55 * A AnnounceMultiplayerRoom interface class. A backend to submit/get to/from a web service should
56 * implement this interface.
57 */
58class Backend {
59public:
60 virtual ~Backend() = default;
61
62 /**
63 * Sets the Information that gets used for the announce
64 * @param uid The Id of the room
65 * @param name The name of the room
66 * @param description The room description
67 * @param port The port of the room
68 * @param net_version The version of the libNetwork that gets used
69 * @param has_password True if the room is passowrd protected
70 * @param preferred_game The preferred game of the room
71 * @param preferred_game_id The title id of the preferred game
72 */
73 virtual void SetRoomInformation(const std::string& name, const std::string& description,
74 const u16 port, const u32 max_player, const u32 net_version,
75 const bool has_password, const GameInfo& preferred_game) = 0;
76 /**
77 * Adds a player information to the data that gets announced
78 * @param nickname The nickname of the player
79 * @param mac_address The MAC Address of the player
80 * @param game_id The title id of the game the player plays
81 * @param game_name The name of the game the player plays
82 */
83 virtual void AddPlayer(const Member& member) = 0;
84
85 /**
86 * Updates the data in the announce service. Re-register the room when required.
87 * @result The result of the update attempt
88 */
89 virtual WebService::WebResult Update() = 0;
90
91 /**
92 * Registers the data in the announce service
93 * @result The result of the register attempt. When the result code is Success, A global Guid of
94 * the room which may be used for verification will be in the result's returned_data.
95 */
96 virtual WebService::WebResult Register() = 0;
97
98 /**
99 * Empties the stored players
100 */
101 virtual void ClearPlayers() = 0;
102
103 /**
104 * Get the room information from the announce service
105 * @result A list of all rooms the announce service has
106 */
107 virtual RoomList GetRoomList() = 0;
108
109 /**
110 * Sends a delete message to the announce service
111 */
112 virtual void Delete() = 0;
113};
114
115/**
116 * Empty implementation of AnnounceMultiplayerRoom interface that drops all data. Used when a
117 * functional backend implementation is not available.
118 */
119class NullBackend : public Backend {
120public:
121 ~NullBackend() = default;
122 void SetRoomInformation(const std::string& /*name*/, const std::string& /*description*/,
123 const u16 /*port*/, const u32 /*max_player*/, const u32 /*net_version*/,
124 const bool /*has_password*/,
125 const GameInfo& /*preferred_game*/) override {}
126 void AddPlayer(const Member& /*member*/) override {}
127 WebService::WebResult Update() override {
128 return WebService::WebResult{WebService::WebResult::Code::NoWebservice,
129 "WebService is missing", ""};
130 }
131 WebService::WebResult Register() override {
132 return WebService::WebResult{WebService::WebResult::Code::NoWebservice,
133 "WebService is missing", ""};
134 }
135 void ClearPlayers() override {}
136 RoomList GetRoomList() override {
137 return RoomList{};
138 }
139
140 void Delete() override {}
141};
142
143} // namespace AnnounceMultiplayerRoom
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 32cc2f392..c1cc62a45 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,4 +1,6 @@
1add_library(core STATIC 1add_library(core STATIC
2 announce_multiplayer_session.cpp
3 announce_multiplayer_session.h
2 arm/arm_interface.h 4 arm/arm_interface.h
3 arm/arm_interface.cpp 5 arm/arm_interface.cpp
4 arm/cpu_interrupt_handler.cpp 6 arm/cpu_interrupt_handler.cpp
@@ -714,6 +716,11 @@ add_library(core STATIC
714 hle/service/vi/vi_u.h 716 hle/service/vi/vi_u.h
715 hle/service/wlan/wlan.cpp 717 hle/service/wlan/wlan.cpp
716 hle/service/wlan/wlan.h 718 hle/service/wlan/wlan.h
719 internal_network/network.cpp
720 internal_network/network.h
721 internal_network/network_interface.cpp
722 internal_network/network_interface.h
723 internal_network/sockets.h
717 loader/deconstructed_rom_directory.cpp 724 loader/deconstructed_rom_directory.cpp
718 loader/deconstructed_rom_directory.h 725 loader/deconstructed_rom_directory.h
719 loader/elf.cpp 726 loader/elf.cpp
@@ -741,11 +748,6 @@ add_library(core STATIC
741 memory/dmnt_cheat_vm.h 748 memory/dmnt_cheat_vm.h
742 memory.cpp 749 memory.cpp
743 memory.h 750 memory.h
744 network/network.cpp
745 network/network.h
746 network/network_interface.cpp
747 network/network_interface.h
748 network/sockets.h
749 perf_stats.cpp 751 perf_stats.cpp
750 perf_stats.h 752 perf_stats.h
751 reporter.cpp 753 reporter.cpp
@@ -780,7 +782,7 @@ endif()
780 782
781create_target_directory_groups(core) 783create_target_directory_groups(core)
782 784
783target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) 785target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core)
784target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus) 786target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::Opus)
785if (MINGW) 787if (MINGW)
786 target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY}) 788 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <chrono>
5#include <future>
6#include <vector>
7#include "announce_multiplayer_session.h"
8#include "common/announce_multiplayer_room.h"
9#include "common/assert.h"
10#include "common/settings.h"
11#include "network/network.h"
12
13#ifdef ENABLE_WEB_SERVICE
14#include "web_service/announce_room_json.h"
15#endif
16
17namespace Core {
18
19// Time between room is announced to web_service
20static constexpr std::chrono::seconds announce_time_interval(15);
21
22AnnounceMultiplayerSession::AnnounceMultiplayerSession(Network::RoomNetwork& room_network_)
23 : room_network{room_network_} {
24#ifdef ENABLE_WEB_SERVICE
25 backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(),
26 Settings::values.yuzu_username.GetValue(),
27 Settings::values.yuzu_token.GetValue());
28#else
29 backend = std::make_unique<AnnounceMultiplayerRoom::NullBackend>();
30#endif
31}
32
33WebService::WebResult AnnounceMultiplayerSession::Register() {
34 std::shared_ptr<Network::Room> room = room_network.GetRoom().lock();
35 if (!room) {
36 return WebService::WebResult{WebService::WebResult::Code::LibError,
37 "Network is not initialized", ""};
38 }
39 if (room->GetState() != Network::Room::State::Open) {
40 return WebService::WebResult{WebService::WebResult::Code::LibError, "Room is not open", ""};
41 }
42 UpdateBackendData(room);
43 WebService::WebResult result = backend->Register();
44 if (result.result_code != WebService::WebResult::Code::Success) {
45 return result;
46 }
47 LOG_INFO(WebService, "Room has been registered");
48 room->SetVerifyUID(result.returned_data);
49 registered = true;
50 return WebService::WebResult{WebService::WebResult::Code::Success, "", ""};
51}
52
53void AnnounceMultiplayerSession::Start() {
54 if (announce_multiplayer_thread) {
55 Stop();
56 }
57 shutdown_event.Reset();
58 announce_multiplayer_thread =
59 std::make_unique<std::thread>(&AnnounceMultiplayerSession::AnnounceMultiplayerLoop, this);
60}
61
62void AnnounceMultiplayerSession::Stop() {
63 if (announce_multiplayer_thread) {
64 shutdown_event.Set();
65 announce_multiplayer_thread->join();
66 announce_multiplayer_thread.reset();
67 backend->Delete();
68 registered = false;
69 }
70}
71
72AnnounceMultiplayerSession::CallbackHandle AnnounceMultiplayerSession::BindErrorCallback(
73 std::function<void(const WebService::WebResult&)> function) {
74 std::lock_guard lock(callback_mutex);
75 auto handle = std::make_shared<std::function<void(const WebService::WebResult&)>>(function);
76 error_callbacks.insert(handle);
77 return handle;
78}
79
80void AnnounceMultiplayerSession::UnbindErrorCallback(CallbackHandle handle) {
81 std::lock_guard lock(callback_mutex);
82 error_callbacks.erase(handle);
83}
84
85AnnounceMultiplayerSession::~AnnounceMultiplayerSession() {
86 Stop();
87}
88
89void AnnounceMultiplayerSession::UpdateBackendData(std::shared_ptr<Network::Room> room) {
90 Network::RoomInformation room_information = room->GetRoomInformation();
91 std::vector<AnnounceMultiplayerRoom::Member> memberlist = room->GetRoomMemberList();
92 backend->SetRoomInformation(room_information.name, room_information.description,
93 room_information.port, room_information.member_slots,
94 Network::network_version, room->HasPassword(),
95 room_information.preferred_game);
96 backend->ClearPlayers();
97 for (const auto& member : memberlist) {
98 backend->AddPlayer(member);
99 }
100}
101
102void AnnounceMultiplayerSession::AnnounceMultiplayerLoop() {
103 // Invokes all current bound error callbacks.
104 const auto ErrorCallback = [this](WebService::WebResult result) {
105 std::lock_guard<std::mutex> lock(callback_mutex);
106 for (auto callback : error_callbacks) {
107 (*callback)(result);
108 }
109 };
110
111 if (!registered) {
112 WebService::WebResult result = Register();
113 if (result.result_code != WebService::WebResult::Code::Success) {
114 ErrorCallback(result);
115 return;
116 }
117 }
118
119 auto update_time = std::chrono::steady_clock::now();
120 std::future<WebService::WebResult> future;
121 while (!shutdown_event.WaitUntil(update_time)) {
122 update_time += announce_time_interval;
123 std::shared_ptr<Network::Room> room = room_network.GetRoom().lock();
124 if (!room) {
125 break;
126 }
127 if (room->GetState() != Network::Room::State::Open) {
128 break;
129 }
130 UpdateBackendData(room);
131 WebService::WebResult result = backend->Update();
132 if (result.result_code != WebService::WebResult::Code::Success) {
133 ErrorCallback(result);
134 }
135 if (result.result_string == "404") {
136 registered = false;
137 // Needs to register the room again
138 WebService::WebResult register_result = Register();
139 if (register_result.result_code != WebService::WebResult::Code::Success) {
140 ErrorCallback(register_result);
141 }
142 }
143 }
144}
145
146AnnounceMultiplayerRoom::RoomList AnnounceMultiplayerSession::GetRoomList() {
147 return backend->GetRoomList();
148}
149
150bool AnnounceMultiplayerSession::IsRunning() const {
151 return announce_multiplayer_thread != nullptr;
152}
153
154void AnnounceMultiplayerSession::UpdateCredentials() {
155 ASSERT_MSG(!IsRunning(), "Credentials can only be updated when session is not running");
156
157#ifdef ENABLE_WEB_SERVICE
158 backend = std::make_unique<WebService::RoomJson>(Settings::values.web_api_url.GetValue(),
159 Settings::values.yuzu_username.GetValue(),
160 Settings::values.yuzu_token.GetValue());
161#endif
162}
163
164} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <atomic>
7#include <functional>
8#include <memory>
9#include <mutex>
10#include <set>
11#include <thread>
12#include "common/announce_multiplayer_room.h"
13#include "common/common_types.h"
14#include "common/thread.h"
15
16namespace Network {
17class Room;
18class RoomNetwork;
19} // namespace Network
20
21namespace Core {
22
23/**
24 * Instruments AnnounceMultiplayerRoom::Backend.
25 * Creates a thread that regularly updates the room information and submits them
26 * An async get of room information is also possible
27 */
28class AnnounceMultiplayerSession {
29public:
30 using CallbackHandle = std::shared_ptr<std::function<void(const WebService::WebResult&)>>;
31 AnnounceMultiplayerSession(Network::RoomNetwork& room_network_);
32 ~AnnounceMultiplayerSession();
33
34 /**
35 * Allows to bind a function that will get called if the announce encounters an error
36 * @param function The function that gets called
37 * @return A handle that can be used the unbind the function
38 */
39 CallbackHandle BindErrorCallback(std::function<void(const WebService::WebResult&)> function);
40
41 /**
42 * Unbind a function from the error callbacks
43 * @param handle The handle for the function that should get unbind
44 */
45 void UnbindErrorCallback(CallbackHandle handle);
46
47 /**
48 * Registers a room to web services
49 * @return The result of the registration attempt.
50 */
51 WebService::WebResult Register();
52
53 /**
54 * Starts the announce of a room to web services
55 */
56 void Start();
57
58 /**
59 * Stops the announce to web services
60 */
61 void Stop();
62
63 /**
64 * Returns a list of all room information the backend got
65 * @param func A function that gets executed when the async get finished, e.g. a signal
66 * @return a list of rooms received from the web service
67 */
68 AnnounceMultiplayerRoom::RoomList GetRoomList();
69
70 /**
71 * Whether the announce session is still running
72 */
73 bool IsRunning() const;
74
75 /**
76 * Recreates the backend, updating the credentials.
77 * This can only be used when the announce session is not running.
78 */
79 void UpdateCredentials();
80
81private:
82 void UpdateBackendData(std::shared_ptr<Network::Room> room);
83 void AnnounceMultiplayerLoop();
84
85 Common::Event shutdown_event;
86 std::mutex callback_mutex;
87 std::set<CallbackHandle> error_callbacks;
88 std::unique_ptr<std::thread> announce_multiplayer_thread;
89
90 /// Backend interface that logs fields
91 std::unique_ptr<AnnounceMultiplayerRoom::Backend> backend;
92
93 std::atomic_bool registered = false; ///< Whether the room has been registered
94
95 Network::RoomNetwork& room_network;
96};
97
98} // namespace Core
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 0ede0d85c..95791a07f 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -43,14 +43,15 @@
43#include "core/hle/service/service.h" 43#include "core/hle/service/service.h"
44#include "core/hle/service/sm/sm.h" 44#include "core/hle/service/sm/sm.h"
45#include "core/hle/service/time/time_manager.h" 45#include "core/hle/service/time/time_manager.h"
46#include "core/internal_network/network.h"
46#include "core/loader/loader.h" 47#include "core/loader/loader.h"
47#include "core/memory.h" 48#include "core/memory.h"
48#include "core/memory/cheat_engine.h" 49#include "core/memory/cheat_engine.h"
49#include "core/network/network.h"
50#include "core/perf_stats.h" 50#include "core/perf_stats.h"
51#include "core/reporter.h" 51#include "core/reporter.h"
52#include "core/telemetry_session.h" 52#include "core/telemetry_session.h"
53#include "core/tools/freezer.h" 53#include "core/tools/freezer.h"
54#include "network/network.h"
54#include "video_core/renderer_base.h" 55#include "video_core/renderer_base.h"
55#include "video_core/video_core.h" 56#include "video_core/video_core.h"
56 57
@@ -130,7 +131,7 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
130 131
131struct System::Impl { 132struct System::Impl {
132 explicit Impl(System& system) 133 explicit Impl(System& system)
133 : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, 134 : kernel{system}, fs_controller{system}, memory{system}, hid_core{}, room_network{},
134 cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system} {} 135 cpu_manager{system}, reporter{system}, applet_manager{system}, time_manager{system} {}
135 136
136 SystemResultStatus Run() { 137 SystemResultStatus Run() {
@@ -315,6 +316,17 @@ struct System::Impl {
315 GetAndResetPerfStats(); 316 GetAndResetPerfStats();
316 perf_stats->BeginSystemFrame(); 317 perf_stats->BeginSystemFrame();
317 318
319 std::string name = "Unknown Game";
320 if (app_loader->ReadTitle(name) != Loader::ResultStatus::Success) {
321 LOG_ERROR(Core, "Failed to read title for ROM (Error {})", load_result);
322 }
323 if (auto room_member = room_network.GetRoomMember().lock()) {
324 Network::GameInfo game_info;
325 game_info.name = name;
326 game_info.id = program_id;
327 room_member->SendGameInfo(game_info);
328 }
329
318 status = SystemResultStatus::Success; 330 status = SystemResultStatus::Success;
319 return status; 331 return status;
320 } 332 }
@@ -362,6 +374,11 @@ struct System::Impl {
362 memory.Reset(); 374 memory.Reset();
363 applet_manager.ClearAll(); 375 applet_manager.ClearAll();
364 376
377 if (auto room_member = room_network.GetRoomMember().lock()) {
378 Network::GameInfo game_info{};
379 room_member->SendGameInfo(game_info);
380 }
381
365 LOG_DEBUG(Core, "Shutdown OK"); 382 LOG_DEBUG(Core, "Shutdown OK");
366 } 383 }
367 384
@@ -434,6 +451,8 @@ struct System::Impl {
434 std::unique_ptr<AudioCore::AudioCore> audio_core; 451 std::unique_ptr<AudioCore::AudioCore> audio_core;
435 Core::Memory::Memory memory; 452 Core::Memory::Memory memory;
436 Core::HID::HIDCore hid_core; 453 Core::HID::HIDCore hid_core;
454 Network::RoomNetwork room_network;
455
437 CpuManager cpu_manager; 456 CpuManager cpu_manager;
438 std::atomic_bool is_powered_on{}; 457 std::atomic_bool is_powered_on{};
439 bool exit_lock = false; 458 bool exit_lock = false;
@@ -879,6 +898,14 @@ const Core::Debugger& System::GetDebugger() const {
879 return *impl->debugger; 898 return *impl->debugger;
880} 899}
881 900
901Network::RoomNetwork& System::GetRoomNetwork() {
902 return impl->room_network;
903}
904
905const Network::RoomNetwork& System::GetRoomNetwork() const {
906 return impl->room_network;
907}
908
882void System::RegisterExecuteProgramCallback(ExecuteProgramCallback&& callback) { 909void System::RegisterExecuteProgramCallback(ExecuteProgramCallback&& callback) {
883 impl->execute_program_callback = std::move(callback); 910 impl->execute_program_callback = std::move(callback);
884} 911}
diff --git a/src/core/core.h b/src/core/core.h
index a49d1214b..13122dd61 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -97,6 +97,10 @@ namespace Core::HID {
97class HIDCore; 97class HIDCore;
98} 98}
99 99
100namespace Network {
101class RoomNetwork;
102}
103
100namespace Core { 104namespace Core {
101 105
102class ARM_Interface; 106class ARM_Interface;
@@ -379,6 +383,12 @@ public:
379 [[nodiscard]] Core::Debugger& GetDebugger(); 383 [[nodiscard]] Core::Debugger& GetDebugger();
380 [[nodiscard]] const Core::Debugger& GetDebugger() const; 384 [[nodiscard]] const Core::Debugger& GetDebugger() const;
381 385
386 /// Gets a mutable reference to the Room Network.
387 [[nodiscard]] Network::RoomNetwork& GetRoomNetwork();
388
389 /// Gets an immutable reference to the Room Network.
390 [[nodiscard]] const Network::RoomNetwork& GetRoomNetwork() const;
391
382 void SetExitLock(bool locked); 392 void SetExitLock(bool locked);
383 [[nodiscard]] bool GetExitLock() const; 393 [[nodiscard]] bool GetExitLock() const;
384 394
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 {
18 18
19} // Anonymous namespace 19} // Anonymous namespace
20 20
21#include "core/network/network.h" 21#include "core/internal_network/network.h"
22#include "core/network/network_interface.h" 22#include "core/internal_network/network_interface.h"
23 23
24namespace Service::NIFM { 24namespace Service::NIFM {
25 25
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 @@
13#include "core/hle/kernel/k_thread.h" 13#include "core/hle/kernel/k_thread.h"
14#include "core/hle/service/sockets/bsd.h" 14#include "core/hle/service/sockets/bsd.h"
15#include "core/hle/service/sockets/sockets_translate.h" 15#include "core/hle/service/sockets/sockets_translate.h"
16#include "core/network/network.h" 16#include "core/internal_network/network.h"
17#include "core/network/sockets.h" 17#include "core/internal_network/sockets.h"
18 18
19namespace Service::Sockets { 19namespace Service::Sockets {
20 20
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;
16 16
17namespace Network { 17namespace Network {
18class Socket; 18class Socket;
19} 19} // namespace Network
20 20
21namespace Service::Sockets { 21namespace Service::Sockets {
22 22
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 @@
7#include "common/common_types.h" 7#include "common/common_types.h"
8#include "core/hle/service/sockets/sockets.h" 8#include "core/hle/service/sockets/sockets.h"
9#include "core/hle/service/sockets/sockets_translate.h" 9#include "core/hle/service/sockets/sockets_translate.h"
10#include "core/network/network.h" 10#include "core/internal_network/network.h"
11 11
12namespace Service::Sockets { 12namespace Service::Sockets {
13 13
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 @@
7 7
8#include "common/common_types.h" 8#include "common/common_types.h"
9#include "core/hle/service/sockets/sockets.h" 9#include "core/hle/service/sockets/sockets.h"
10#include "core/network/network.h" 10#include "core/internal_network/network.h"
11 11
12namespace Service::Sockets { 12namespace Service::Sockets {
13 13
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 @@
29#include "common/common_types.h" 29#include "common/common_types.h"
30#include "common/logging/log.h" 30#include "common/logging/log.h"
31#include "common/settings.h" 31#include "common/settings.h"
32#include "core/network/network.h" 32#include "core/internal_network/network.h"
33#include "core/network/network_interface.h" 33#include "core/internal_network/network_interface.h"
34#include "core/network/sockets.h" 34#include "core/internal_network/sockets.h"
35 35
36namespace Network { 36namespace Network {
37 37
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 @@
11#include "common/logging/log.h" 11#include "common/logging/log.h"
12#include "common/settings.h" 12#include "common/settings.h"
13#include "common/string_util.h" 13#include "common/string_util.h"
14#include "core/network/network_interface.h" 14#include "core/internal_network/network_interface.h"
15 15
16#ifdef _WIN32 16#ifdef _WIN32
17#include <iphlpapi.h> 17#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 @@
3 3
4#pragma once 4#pragma once
5 5
6#include <map>
6#include <memory> 7#include <memory>
7#include <utility> 8#include <utility>
8 9
@@ -12,7 +13,7 @@
12#endif 13#endif
13 14
14#include "common/common_types.h" 15#include "common/common_types.h"
15#include "core/network/network.h" 16#include "core/internal_network/network.h"
16 17
17// TODO: C++20 Replace std::vector usages with std::span 18// TODO: C++20 Replace std::vector usages with std::span
18 19
diff --git a/src/input_common/helpers/udp_protocol.h b/src/input_common/helpers/udp_protocol.h
index 597f51cd3..889693e73 100644
--- a/src/input_common/helpers/udp_protocol.h
+++ b/src/input_common/helpers/udp_protocol.h
@@ -85,7 +85,7 @@ enum RegisterFlags : u8 {
85struct Version {}; 85struct Version {};
86/** 86/**
87 * Requests the server to send information about what controllers are plugged into the ports 87 * Requests the server to send information about what controllers are plugged into the ports
88 * In citra's case, we only have one controller, so for simplicity's sake, we can just send a 88 * In yuzu's case, we only have one controller, so for simplicity's sake, we can just send a
89 * request explicitly for the first controller port and leave it at that. In the future it would be 89 * request explicitly for the first controller port and leave it at that. In the future it would be
90 * nice to make this configurable 90 * nice to make this configurable
91 */ 91 */
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt
new file mode 100644
index 000000000..382a69e2f
--- /dev/null
+++ b/src/network/CMakeLists.txt
@@ -0,0 +1,16 @@
1add_library(network STATIC
2 network.cpp
3 network.h
4 packet.cpp
5 packet.h
6 room.cpp
7 room.h
8 room_member.cpp
9 room_member.h
10 verify_user.cpp
11 verify_user.h
12)
13
14create_target_directory_groups(network)
15
16target_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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "common/assert.h"
5#include "common/logging/log.h"
6#include "enet/enet.h"
7#include "network/network.h"
8
9namespace Network {
10
11RoomNetwork::RoomNetwork() {
12 m_room = std::make_shared<Room>();
13 m_room_member = std::make_shared<RoomMember>();
14}
15
16bool RoomNetwork::Init() {
17 if (enet_initialize() != 0) {
18 LOG_ERROR(Network, "Error initalizing ENet");
19 return false;
20 }
21 m_room = std::make_shared<Room>();
22 m_room_member = std::make_shared<RoomMember>();
23 LOG_DEBUG(Network, "initialized OK");
24 return true;
25}
26
27std::weak_ptr<Room> RoomNetwork::GetRoom() {
28 return m_room;
29}
30
31std::weak_ptr<RoomMember> RoomNetwork::GetRoomMember() {
32 return m_room_member;
33}
34
35void RoomNetwork::Shutdown() {
36 if (m_room_member) {
37 if (m_room_member->IsConnected())
38 m_room_member->Leave();
39 m_room_member.reset();
40 }
41 if (m_room) {
42 if (m_room->GetState() == Room::State::Open)
43 m_room->Destroy();
44 m_room.reset();
45 }
46 enet_deinitialize();
47 LOG_DEBUG(Network, "shutdown OK");
48}
49
50} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include "network/room.h"
8#include "network/room_member.h"
9
10namespace Network {
11
12class RoomNetwork {
13public:
14 RoomNetwork();
15
16 /// Initializes and registers the network device, the room, and the room member.
17 bool Init();
18
19 /// Returns a pointer to the room handle
20 std::weak_ptr<Room> GetRoom();
21
22 /// Returns a pointer to the room member handle
23 std::weak_ptr<RoomMember> GetRoomMember();
24
25 /// Unregisters the network device, the room, and the room member and shut them down.
26 void Shutdown();
27
28private:
29 std::shared_ptr<RoomMember> m_room_member; ///< RoomMember (Client) for network games
30 std::shared_ptr<Room> m_room; ///< Room (Server) for network games
31};
32
33} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#ifdef _WIN32
5#include <winsock2.h>
6#else
7#include <arpa/inet.h>
8#endif
9#include <cstring>
10#include <string>
11#include "network/packet.h"
12
13namespace Network {
14
15#ifndef htonll
16static u64 htonll(u64 x) {
17 return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32));
18}
19#endif
20
21#ifndef ntohll
22static u64 ntohll(u64 x) {
23 return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32));
24}
25#endif
26
27void Packet::Append(const void* in_data, std::size_t size_in_bytes) {
28 if (in_data && (size_in_bytes > 0)) {
29 std::size_t start = data.size();
30 data.resize(start + size_in_bytes);
31 std::memcpy(&data[start], in_data, size_in_bytes);
32 }
33}
34
35void Packet::Read(void* out_data, std::size_t size_in_bytes) {
36 if (out_data && CheckSize(size_in_bytes)) {
37 std::memcpy(out_data, &data[read_pos], size_in_bytes);
38 read_pos += size_in_bytes;
39 }
40}
41
42void Packet::Clear() {
43 data.clear();
44 read_pos = 0;
45 is_valid = true;
46}
47
48const void* Packet::GetData() const {
49 return !data.empty() ? &data[0] : nullptr;
50}
51
52void Packet::IgnoreBytes(u32 length) {
53 read_pos += length;
54}
55
56std::size_t Packet::GetDataSize() const {
57 return data.size();
58}
59
60bool Packet::EndOfPacket() const {
61 return read_pos >= data.size();
62}
63
64Packet::operator bool() const {
65 return is_valid;
66}
67
68Packet& Packet::Read(bool& out_data) {
69 u8 value{};
70 if (Read(value)) {
71 out_data = (value != 0);
72 }
73 return *this;
74}
75
76Packet& Packet::Read(s8& out_data) {
77 Read(&out_data, sizeof(out_data));
78 return *this;
79}
80
81Packet& Packet::Read(u8& out_data) {
82 Read(&out_data, sizeof(out_data));
83 return *this;
84}
85
86Packet& Packet::Read(s16& out_data) {
87 s16 value{};
88 Read(&value, sizeof(value));
89 out_data = ntohs(value);
90 return *this;
91}
92
93Packet& Packet::Read(u16& out_data) {
94 u16 value{};
95 Read(&value, sizeof(value));
96 out_data = ntohs(value);
97 return *this;
98}
99
100Packet& Packet::Read(s32& out_data) {
101 s32 value{};
102 Read(&value, sizeof(value));
103 out_data = ntohl(value);
104 return *this;
105}
106
107Packet& Packet::Read(u32& out_data) {
108 u32 value{};
109 Read(&value, sizeof(value));
110 out_data = ntohl(value);
111 return *this;
112}
113
114Packet& Packet::Read(s64& out_data) {
115 s64 value{};
116 Read(&value, sizeof(value));
117 out_data = ntohll(value);
118 return *this;
119}
120
121Packet& Packet::Read(u64& out_data) {
122 u64 value{};
123 Read(&value, sizeof(value));
124 out_data = ntohll(value);
125 return *this;
126}
127
128Packet& Packet::Read(float& out_data) {
129 Read(&out_data, sizeof(out_data));
130 return *this;
131}
132
133Packet& Packet::Read(double& out_data) {
134 Read(&out_data, sizeof(out_data));
135 return *this;
136}
137
138Packet& Packet::Read(char* out_data) {
139 // First extract string length
140 u32 length = 0;
141 Read(length);
142
143 if ((length > 0) && CheckSize(length)) {
144 // Then extract characters
145 std::memcpy(out_data, &data[read_pos], length);
146 out_data[length] = '\0';
147
148 // Update reading position
149 read_pos += length;
150 }
151
152 return *this;
153}
154
155Packet& Packet::Read(std::string& out_data) {
156 // First extract string length
157 u32 length = 0;
158 Read(length);
159
160 out_data.clear();
161 if ((length > 0) && CheckSize(length)) {
162 // Then extract characters
163 out_data.assign(&data[read_pos], length);
164
165 // Update reading position
166 read_pos += length;
167 }
168
169 return *this;
170}
171
172Packet& Packet::Write(bool in_data) {
173 Write(static_cast<u8>(in_data));
174 return *this;
175}
176
177Packet& Packet::Write(s8 in_data) {
178 Append(&in_data, sizeof(in_data));
179 return *this;
180}
181
182Packet& Packet::Write(u8 in_data) {
183 Append(&in_data, sizeof(in_data));
184 return *this;
185}
186
187Packet& Packet::Write(s16 in_data) {
188 s16 toWrite = htons(in_data);
189 Append(&toWrite, sizeof(toWrite));
190 return *this;
191}
192
193Packet& Packet::Write(u16 in_data) {
194 u16 toWrite = htons(in_data);
195 Append(&toWrite, sizeof(toWrite));
196 return *this;
197}
198
199Packet& Packet::Write(s32 in_data) {
200 s32 toWrite = htonl(in_data);
201 Append(&toWrite, sizeof(toWrite));
202 return *this;
203}
204
205Packet& Packet::Write(u32 in_data) {
206 u32 toWrite = htonl(in_data);
207 Append(&toWrite, sizeof(toWrite));
208 return *this;
209}
210
211Packet& Packet::Write(s64 in_data) {
212 s64 toWrite = htonll(in_data);
213 Append(&toWrite, sizeof(toWrite));
214 return *this;
215}
216
217Packet& Packet::Write(u64 in_data) {
218 u64 toWrite = htonll(in_data);
219 Append(&toWrite, sizeof(toWrite));
220 return *this;
221}
222
223Packet& Packet::Write(float in_data) {
224 Append(&in_data, sizeof(in_data));
225 return *this;
226}
227
228Packet& Packet::Write(double in_data) {
229 Append(&in_data, sizeof(in_data));
230 return *this;
231}
232
233Packet& Packet::Write(const char* in_data) {
234 // First insert string length
235 u32 length = static_cast<u32>(std::strlen(in_data));
236 Write(length);
237
238 // Then insert characters
239 Append(in_data, length * sizeof(char));
240
241 return *this;
242}
243
244Packet& Packet::Write(const std::string& in_data) {
245 // First insert string length
246 u32 length = static_cast<u32>(in_data.size());
247 Write(length);
248
249 // Then insert characters
250 if (length > 0)
251 Append(in_data.c_str(), length * sizeof(std::string::value_type));
252
253 return *this;
254}
255
256bool Packet::CheckSize(std::size_t size) {
257 is_valid = is_valid && (read_pos + size <= data.size());
258
259 return is_valid;
260}
261
262} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <array>
7#include <vector>
8#include "common/common_types.h"
9
10namespace Network {
11
12/// A class that serializes data for network transfer. It also handles endianess
13class Packet {
14public:
15 Packet() = default;
16 ~Packet() = default;
17
18 /**
19 * Append data to the end of the packet
20 * @param data Pointer to the sequence of bytes to append
21 * @param size_in_bytes Number of bytes to append
22 */
23 void Append(const void* data, std::size_t size_in_bytes);
24
25 /**
26 * Reads data from the current read position of the packet
27 * @param out_data Pointer where the data should get written to
28 * @param size_in_bytes Number of bytes to read
29 */
30 void Read(void* out_data, std::size_t size_in_bytes);
31
32 /**
33 * Clear the packet
34 * After calling Clear, the packet is empty.
35 */
36 void Clear();
37
38 /**
39 * Ignores bytes while reading
40 * @param length THe number of bytes to ignore
41 */
42 void IgnoreBytes(u32 length);
43
44 /**
45 * Get a pointer to the data contained in the packet
46 * @return Pointer to the data
47 */
48 const void* GetData() const;
49
50 /**
51 * This function returns the number of bytes pointed to by
52 * what getData returns.
53 * @return Data size, in bytes
54 */
55 std::size_t GetDataSize() const;
56
57 /**
58 * This function is useful to know if there is some data
59 * left to be read, without actually reading it.
60 * @return True if all data was read, false otherwise
61 */
62 bool EndOfPacket() const;
63
64 explicit operator bool() const;
65
66 /// Overloads of read function to read data from the packet
67 Packet& Read(bool& out_data);
68 Packet& Read(s8& out_data);
69 Packet& Read(u8& out_data);
70 Packet& Read(s16& out_data);
71 Packet& Read(u16& out_data);
72 Packet& Read(s32& out_data);
73 Packet& Read(u32& out_data);
74 Packet& Read(s64& out_data);
75 Packet& Read(u64& out_data);
76 Packet& Read(float& out_data);
77 Packet& Read(double& out_data);
78 Packet& Read(char* out_data);
79 Packet& Read(std::string& out_data);
80 template <typename T>
81 Packet& Read(std::vector<T>& out_data);
82 template <typename T, std::size_t S>
83 Packet& Read(std::array<T, S>& out_data);
84
85 /// Overloads of write function to write data into the packet
86 Packet& Write(bool in_data);
87 Packet& Write(s8 in_data);
88 Packet& Write(u8 in_data);
89 Packet& Write(s16 in_data);
90 Packet& Write(u16 in_data);
91 Packet& Write(s32 in_data);
92 Packet& Write(u32 in_data);
93 Packet& Write(s64 in_data);
94 Packet& Write(u64 in_data);
95 Packet& Write(float in_data);
96 Packet& Write(double in_data);
97 Packet& Write(const char* in_data);
98 Packet& Write(const std::string& in_data);
99 template <typename T>
100 Packet& Write(const std::vector<T>& in_data);
101 template <typename T, std::size_t S>
102 Packet& Write(const std::array<T, S>& data);
103
104private:
105 /**
106 * Check if the packet can extract a given number of bytes
107 * This function updates accordingly the state of the packet.
108 * @param size Size to check
109 * @return True if size bytes can be read from the packet
110 */
111 bool CheckSize(std::size_t size);
112
113 // Member data
114 std::vector<char> data; ///< Data stored in the packet
115 std::size_t read_pos = 0; ///< Current reading position in the packet
116 bool is_valid = true; ///< Reading state of the packet
117};
118
119template <typename T>
120Packet& Packet::Read(std::vector<T>& out_data) {
121 // First extract the size
122 u32 size = 0;
123 Read(size);
124 out_data.resize(size);
125
126 // Then extract the data
127 for (std::size_t i = 0; i < out_data.size(); ++i) {
128 T character;
129 Read(character);
130 out_data[i] = character;
131 }
132 return *this;
133}
134
135template <typename T, std::size_t S>
136Packet& Packet::Read(std::array<T, S>& out_data) {
137 for (std::size_t i = 0; i < out_data.size(); ++i) {
138 T character;
139 Read(character);
140 out_data[i] = character;
141 }
142 return *this;
143}
144
145template <typename T>
146Packet& Packet::Write(const std::vector<T>& in_data) {
147 // First insert the size
148 Write(static_cast<u32>(in_data.size()));
149
150 // Then insert the data
151 for (std::size_t i = 0; i < in_data.size(); ++i) {
152 Write(in_data[i]);
153 }
154 return *this;
155}
156
157template <typename T, std::size_t S>
158Packet& Packet::Write(const std::array<T, S>& in_data) {
159 for (std::size_t i = 0; i < in_data.size(); ++i) {
160 Write(in_data[i]);
161 }
162 return *this;
163}
164
165} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <algorithm>
5#include <atomic>
6#include <iomanip>
7#include <mutex>
8#include <random>
9#include <regex>
10#include <shared_mutex>
11#include <sstream>
12#include <thread>
13#include "common/logging/log.h"
14#include "enet/enet.h"
15#include "network/packet.h"
16#include "network/room.h"
17#include "network/verify_user.h"
18
19namespace Network {
20
21class Room::RoomImpl {
22public:
23 // This MAC address is used to generate a 'Nintendo' like Mac address.
24 const MacAddress NintendoOUI;
25 std::mt19937 random_gen; ///< Random number generator. Used for GenerateMacAddress
26
27 ENetHost* server = nullptr; ///< Network interface.
28
29 std::atomic<State> state{State::Closed}; ///< Current state of the room.
30 RoomInformation room_information; ///< Information about this room.
31
32 std::string verify_uid; ///< A GUID which may be used for verfication.
33 mutable std::mutex verify_uid_mutex; ///< Mutex for verify_uid
34
35 std::string password; ///< The password required to connect to this room.
36
37 struct Member {
38 std::string nickname; ///< The nickname of the member.
39 std::string console_id_hash; ///< A hash of the console ID of the member.
40 GameInfo game_info; ///< The current game of the member
41 MacAddress mac_address; ///< The assigned mac address of the member.
42 /// Data of the user, often including authenticated forum username.
43 VerifyUser::UserData user_data;
44 ENetPeer* peer; ///< The remote peer.
45 };
46 using MemberList = std::vector<Member>;
47 MemberList members; ///< Information about the members of this room
48 mutable std::shared_mutex member_mutex; ///< Mutex for locking the members list
49
50 UsernameBanList username_ban_list; ///< List of banned usernames
51 IPBanList ip_ban_list; ///< List of banned IP addresses
52 mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists
53
54 RoomImpl()
55 : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {}
56
57 /// Thread that receives and dispatches network packets
58 std::unique_ptr<std::thread> room_thread;
59
60 /// Verification backend of the room
61 std::unique_ptr<VerifyUser::Backend> verify_backend;
62
63 /// Thread function that will receive and dispatch messages until the room is destroyed.
64 void ServerLoop();
65 void StartLoop();
66
67 /**
68 * Parses and answers a room join request from a client.
69 * Validates the uniqueness of the username and assigns the MAC address
70 * that the client will use for the remainder of the connection.
71 */
72 void HandleJoinRequest(const ENetEvent* event);
73
74 /**
75 * Parses and answers a kick request from a client.
76 * Validates the permissions and that the given user exists and then kicks the member.
77 */
78 void HandleModKickPacket(const ENetEvent* event);
79
80 /**
81 * Parses and answers a ban request from a client.
82 * Validates the permissions and bans the user (by forum username or IP).
83 */
84 void HandleModBanPacket(const ENetEvent* event);
85
86 /**
87 * Parses and answers a unban request from a client.
88 * Validates the permissions and unbans the address.
89 */
90 void HandleModUnbanPacket(const ENetEvent* event);
91
92 /**
93 * Parses and answers a get ban list request from a client.
94 * Validates the permissions and returns the ban list.
95 */
96 void HandleModGetBanListPacket(const ENetEvent* event);
97
98 /**
99 * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room.
100 */
101 bool IsValidNickname(const std::string& nickname) const;
102
103 /**
104 * Returns whether the MAC address is valid, ie. isn't already taken by someone else in the
105 * room.
106 */
107 bool IsValidMacAddress(const MacAddress& address) const;
108
109 /**
110 * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in
111 * the room.
112 */
113 bool IsValidConsoleId(const std::string& console_id_hash) const;
114
115 /**
116 * Returns whether a user has mod permissions.
117 */
118 bool HasModPermission(const ENetPeer* client) const;
119
120 /**
121 * Sends a ID_ROOM_IS_FULL message telling the client that the room is full.
122 */
123 void SendRoomIsFull(ENetPeer* client);
124
125 /**
126 * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid.
127 */
128 void SendNameCollision(ENetPeer* client);
129
130 /**
131 * Sends a ID_ROOM_MAC_COLLISION message telling the client that the MAC is invalid.
132 */
133 void SendMacCollision(ENetPeer* client);
134
135 /**
136 * Sends a IdConsoleIdCollison message telling the client that another member with the same
137 * console ID exists.
138 */
139 void SendConsoleIdCollision(ENetPeer* client);
140
141 /**
142 * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid.
143 */
144 void SendVersionMismatch(ENetPeer* client);
145
146 /**
147 * Sends a ID_ROOM_WRONG_PASSWORD message telling the client that the password is wrong.
148 */
149 void SendWrongPassword(ENetPeer* client);
150
151 /**
152 * Notifies the member that its connection attempt was successful,
153 * and it is now part of the room.
154 */
155 void SendJoinSuccess(ENetPeer* client, MacAddress mac_address);
156
157 /**
158 * Notifies the member that its connection attempt was successful,
159 * and it is now part of the room, and it has been granted mod permissions.
160 */
161 void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address);
162
163 /**
164 * Sends a IdHostKicked message telling the client that they have been kicked.
165 */
166 void SendUserKicked(ENetPeer* client);
167
168 /**
169 * Sends a IdHostBanned message telling the client that they have been banned.
170 */
171 void SendUserBanned(ENetPeer* client);
172
173 /**
174 * Sends a IdModPermissionDenied message telling the client that they do not have mod
175 * permission.
176 */
177 void SendModPermissionDenied(ENetPeer* client);
178
179 /**
180 * Sends a IdModNoSuchUser message telling the client that the given user could not be found.
181 */
182 void SendModNoSuchUser(ENetPeer* client);
183
184 /**
185 * Sends the ban list in response to a client's request for getting ban list.
186 */
187 void SendModBanListResponse(ENetPeer* client);
188
189 /**
190 * Notifies the members that the room is closed,
191 */
192 void SendCloseMessage();
193
194 /**
195 * Sends a system message to all the connected clients.
196 */
197 void SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
198 const std::string& username, const std::string& ip);
199
200 /**
201 * Sends the information about the room, along with the list of members
202 * to every connected client in the room.
203 * The packet has the structure:
204 * <MessageID>ID_ROOM_INFORMATION
205 * <String> room_name
206 * <String> room_description
207 * <u32> member_slots: The max number of clients allowed in this room
208 * <String> uid
209 * <u16> port
210 * <u32> num_members: the number of currently joined clients
211 * This is followed by the following three values for each member:
212 * <String> nickname of that member
213 * <MacAddress> mac_address of that member
214 * <String> game_name of that member
215 */
216 void BroadcastRoomInformation();
217
218 /**
219 * Generates a free MAC address to assign to a new client.
220 * The first 3 bytes are the NintendoOUI 0x00, 0x1F, 0x32
221 */
222 MacAddress GenerateMacAddress();
223
224 /**
225 * Broadcasts this packet to all members except the sender.
226 * @param event The ENet event containing the data
227 */
228 void HandleWifiPacket(const ENetEvent* event);
229
230 /**
231 * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
232 * @param event The ENet event that was received.
233 */
234 void HandleChatPacket(const ENetEvent* event);
235
236 /**
237 * Extracts the game name from a received ENet packet and broadcasts it.
238 * @param event The ENet event that was received.
239 */
240 void HandleGameNamePacket(const ENetEvent* event);
241
242 /**
243 * Removes the client from the members list if it was in it and announces the change
244 * to all other clients.
245 */
246 void HandleClientDisconnection(ENetPeer* client);
247};
248
249// RoomImpl
250void Room::RoomImpl::ServerLoop() {
251 while (state != State::Closed) {
252 ENetEvent event;
253 if (enet_host_service(server, &event, 16) > 0) {
254 switch (event.type) {
255 case ENET_EVENT_TYPE_RECEIVE:
256 switch (event.packet->data[0]) {
257 case IdJoinRequest:
258 HandleJoinRequest(&event);
259 break;
260 case IdSetGameInfo:
261 HandleGameNamePacket(&event);
262 break;
263 case IdWifiPacket:
264 HandleWifiPacket(&event);
265 break;
266 case IdChatMessage:
267 HandleChatPacket(&event);
268 break;
269 // Moderation
270 case IdModKick:
271 HandleModKickPacket(&event);
272 break;
273 case IdModBan:
274 HandleModBanPacket(&event);
275 break;
276 case IdModUnban:
277 HandleModUnbanPacket(&event);
278 break;
279 case IdModGetBanList:
280 HandleModGetBanListPacket(&event);
281 break;
282 }
283 enet_packet_destroy(event.packet);
284 break;
285 case ENET_EVENT_TYPE_DISCONNECT:
286 HandleClientDisconnection(event.peer);
287 break;
288 case ENET_EVENT_TYPE_NONE:
289 case ENET_EVENT_TYPE_CONNECT:
290 break;
291 }
292 }
293 }
294 // Close the connection to all members:
295 SendCloseMessage();
296}
297
298void Room::RoomImpl::StartLoop() {
299 room_thread = std::make_unique<std::thread>(&Room::RoomImpl::ServerLoop, this);
300}
301
302void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
303 {
304 std::lock_guard lock(member_mutex);
305 if (members.size() >= room_information.member_slots) {
306 SendRoomIsFull(event->peer);
307 return;
308 }
309 }
310 Packet packet;
311 packet.Append(event->packet->data, event->packet->dataLength);
312 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
313 std::string nickname;
314 packet.Read(nickname);
315
316 std::string console_id_hash;
317 packet.Read(console_id_hash);
318
319 MacAddress preferred_mac;
320 packet.Read(preferred_mac);
321
322 u32 client_version;
323 packet.Read(client_version);
324
325 std::string pass;
326 packet.Read(pass);
327
328 std::string token;
329 packet.Read(token);
330
331 if (pass != password) {
332 SendWrongPassword(event->peer);
333 return;
334 }
335
336 if (!IsValidNickname(nickname)) {
337 SendNameCollision(event->peer);
338 return;
339 }
340
341 if (preferred_mac != NoPreferredMac) {
342 // Verify if the preferred mac is available
343 if (!IsValidMacAddress(preferred_mac)) {
344 SendMacCollision(event->peer);
345 return;
346 }
347 } else {
348 // Assign a MAC address of this client automatically
349 preferred_mac = GenerateMacAddress();
350 }
351
352 if (!IsValidConsoleId(console_id_hash)) {
353 SendConsoleIdCollision(event->peer);
354 return;
355 }
356
357 if (client_version != network_version) {
358 SendVersionMismatch(event->peer);
359 return;
360 }
361
362 // At this point the client is ready to be added to the room.
363 Member member{};
364 member.mac_address = preferred_mac;
365 member.console_id_hash = console_id_hash;
366 member.nickname = nickname;
367 member.peer = event->peer;
368
369 std::string uid;
370 {
371 std::lock_guard lock(verify_uid_mutex);
372 uid = verify_uid;
373 }
374 member.user_data = verify_backend->LoadUserData(uid, token);
375
376 std::string ip;
377 {
378 std::lock_guard lock(ban_list_mutex);
379
380 // Check username ban
381 if (!member.user_data.username.empty() &&
382 std::find(username_ban_list.begin(), username_ban_list.end(),
383 member.user_data.username) != username_ban_list.end()) {
384
385 SendUserBanned(event->peer);
386 return;
387 }
388
389 // Check IP ban
390 std::array<char, 256> ip_raw{};
391 enet_address_get_host_ip(&event->peer->address, ip_raw.data(), sizeof(ip_raw) - 1);
392 ip = ip_raw.data();
393
394 if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) {
395 SendUserBanned(event->peer);
396 return;
397 }
398 }
399
400 // Notify everyone that the user has joined.
401 SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username, ip);
402
403 {
404 std::lock_guard lock(member_mutex);
405 members.push_back(std::move(member));
406 }
407
408 // Notify everyone that the room information has changed.
409 BroadcastRoomInformation();
410 if (HasModPermission(event->peer)) {
411 SendJoinSuccessAsMod(event->peer, preferred_mac);
412 } else {
413 SendJoinSuccess(event->peer, preferred_mac);
414 }
415}
416
417void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) {
418 if (!HasModPermission(event->peer)) {
419 SendModPermissionDenied(event->peer);
420 return;
421 }
422
423 Packet packet;
424 packet.Append(event->packet->data, event->packet->dataLength);
425 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
426
427 std::string nickname;
428 packet.Read(nickname);
429
430 std::string username, ip;
431 {
432 std::lock_guard lock(member_mutex);
433 const auto target_member =
434 std::find_if(members.begin(), members.end(),
435 [&nickname](const auto& member) { return member.nickname == nickname; });
436 if (target_member == members.end()) {
437 SendModNoSuchUser(event->peer);
438 return;
439 }
440
441 // Notify the kicked member
442 SendUserKicked(target_member->peer);
443
444 username = target_member->user_data.username;
445
446 std::array<char, 256> ip_raw{};
447 enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1);
448 ip = ip_raw.data();
449
450 enet_peer_disconnect(target_member->peer, 0);
451 members.erase(target_member);
452 }
453
454 // Announce the change to all clients.
455 SendStatusMessage(IdMemberKicked, nickname, username, ip);
456 BroadcastRoomInformation();
457}
458
459void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) {
460 if (!HasModPermission(event->peer)) {
461 SendModPermissionDenied(event->peer);
462 return;
463 }
464
465 Packet packet;
466 packet.Append(event->packet->data, event->packet->dataLength);
467 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
468
469 std::string nickname;
470 packet.Read(nickname);
471
472 std::string username, ip;
473 {
474 std::lock_guard lock(member_mutex);
475 const auto target_member =
476 std::find_if(members.begin(), members.end(),
477 [&nickname](const auto& member) { return member.nickname == nickname; });
478 if (target_member == members.end()) {
479 SendModNoSuchUser(event->peer);
480 return;
481 }
482
483 // Notify the banned member
484 SendUserBanned(target_member->peer);
485
486 nickname = target_member->nickname;
487 username = target_member->user_data.username;
488
489 std::array<char, 256> ip_raw{};
490 enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1);
491 ip = ip_raw.data();
492
493 enet_peer_disconnect(target_member->peer, 0);
494 members.erase(target_member);
495 }
496
497 {
498 std::lock_guard lock(ban_list_mutex);
499
500 if (!username.empty()) {
501 // Ban the forum username
502 if (std::find(username_ban_list.begin(), username_ban_list.end(), username) ==
503 username_ban_list.end()) {
504
505 username_ban_list.emplace_back(username);
506 }
507 }
508
509 // Ban the member's IP as well
510 if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) {
511 ip_ban_list.emplace_back(ip);
512 }
513 }
514
515 // Announce the change to all clients.
516 SendStatusMessage(IdMemberBanned, nickname, username, ip);
517 BroadcastRoomInformation();
518}
519
520void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) {
521 if (!HasModPermission(event->peer)) {
522 SendModPermissionDenied(event->peer);
523 return;
524 }
525
526 Packet packet;
527 packet.Append(event->packet->data, event->packet->dataLength);
528 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
529
530 std::string address;
531 packet.Read(address);
532
533 bool unbanned = false;
534 {
535 std::lock_guard lock(ban_list_mutex);
536
537 auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address);
538 if (it != username_ban_list.end()) {
539 unbanned = true;
540 username_ban_list.erase(it);
541 }
542
543 it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address);
544 if (it != ip_ban_list.end()) {
545 unbanned = true;
546 ip_ban_list.erase(it);
547 }
548 }
549
550 if (unbanned) {
551 SendStatusMessage(IdAddressUnbanned, address, "", "");
552 } else {
553 SendModNoSuchUser(event->peer);
554 }
555}
556
557void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) {
558 if (!HasModPermission(event->peer)) {
559 SendModPermissionDenied(event->peer);
560 return;
561 }
562
563 SendModBanListResponse(event->peer);
564}
565
566bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const {
567 // A nickname is valid if it matches the regex and is not already taken by anybody else in the
568 // room.
569 const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$");
570 if (!std::regex_match(nickname, nickname_regex))
571 return false;
572
573 std::lock_guard lock(member_mutex);
574 return std::all_of(members.begin(), members.end(),
575 [&nickname](const auto& member) { return member.nickname != nickname; });
576}
577
578bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const {
579 // A MAC address is valid if it is not already taken by anybody else in the room.
580 std::lock_guard lock(member_mutex);
581 return std::all_of(members.begin(), members.end(),
582 [&address](const auto& member) { return member.mac_address != address; });
583}
584
585bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const {
586 // A Console ID is valid if it is not already taken by anybody else in the room.
587 std::lock_guard lock(member_mutex);
588 return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) {
589 return member.console_id_hash != console_id_hash;
590 });
591}
592
593bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const {
594 std::lock_guard lock(member_mutex);
595 const auto sending_member =
596 std::find_if(members.begin(), members.end(),
597 [client](const auto& member) { return member.peer == client; });
598 if (sending_member == members.end()) {
599 return false;
600 }
601 if (room_information.enable_yuzu_mods &&
602 sending_member->user_data.moderator) { // Community moderator
603
604 return true;
605 }
606 if (!room_information.host_username.empty() &&
607 sending_member->user_data.username == room_information.host_username) { // Room host
608
609 return true;
610 }
611 return false;
612}
613
614void Room::RoomImpl::SendNameCollision(ENetPeer* client) {
615 Packet packet;
616 packet.Write(static_cast<u8>(IdNameCollision));
617
618 ENetPacket* enet_packet =
619 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
620 enet_peer_send(client, 0, enet_packet);
621 enet_host_flush(server);
622}
623
624void Room::RoomImpl::SendMacCollision(ENetPeer* client) {
625 Packet packet;
626 packet.Write(static_cast<u8>(IdMacCollision));
627
628 ENetPacket* enet_packet =
629 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
630 enet_peer_send(client, 0, enet_packet);
631 enet_host_flush(server);
632}
633
634void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) {
635 Packet packet;
636 packet.Write(static_cast<u8>(IdConsoleIdCollision));
637
638 ENetPacket* enet_packet =
639 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
640 enet_peer_send(client, 0, enet_packet);
641 enet_host_flush(server);
642}
643
644void Room::RoomImpl::SendWrongPassword(ENetPeer* client) {
645 Packet packet;
646 packet.Write(static_cast<u8>(IdWrongPassword));
647
648 ENetPacket* enet_packet =
649 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
650 enet_peer_send(client, 0, enet_packet);
651 enet_host_flush(server);
652}
653
654void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) {
655 Packet packet;
656 packet.Write(static_cast<u8>(IdRoomIsFull));
657
658 ENetPacket* enet_packet =
659 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
660 enet_peer_send(client, 0, enet_packet);
661 enet_host_flush(server);
662}
663
664void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) {
665 Packet packet;
666 packet.Write(static_cast<u8>(IdVersionMismatch));
667 packet.Write(network_version);
668
669 ENetPacket* enet_packet =
670 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
671 enet_peer_send(client, 0, enet_packet);
672 enet_host_flush(server);
673}
674
675void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) {
676 Packet packet;
677 packet.Write(static_cast<u8>(IdJoinSuccess));
678 packet.Write(mac_address);
679 ENetPacket* enet_packet =
680 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
681 enet_peer_send(client, 0, enet_packet);
682 enet_host_flush(server);
683}
684
685void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) {
686 Packet packet;
687 packet.Write(static_cast<u8>(IdJoinSuccessAsMod));
688 packet.Write(mac_address);
689 ENetPacket* enet_packet =
690 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
691 enet_peer_send(client, 0, enet_packet);
692 enet_host_flush(server);
693}
694
695void Room::RoomImpl::SendUserKicked(ENetPeer* client) {
696 Packet packet;
697 packet.Write(static_cast<u8>(IdHostKicked));
698
699 ENetPacket* enet_packet =
700 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
701 enet_peer_send(client, 0, enet_packet);
702 enet_host_flush(server);
703}
704
705void Room::RoomImpl::SendUserBanned(ENetPeer* client) {
706 Packet packet;
707 packet.Write(static_cast<u8>(IdHostBanned));
708
709 ENetPacket* enet_packet =
710 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
711 enet_peer_send(client, 0, enet_packet);
712 enet_host_flush(server);
713}
714
715void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) {
716 Packet packet;
717 packet.Write(static_cast<u8>(IdModPermissionDenied));
718
719 ENetPacket* enet_packet =
720 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
721 enet_peer_send(client, 0, enet_packet);
722 enet_host_flush(server);
723}
724
725void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) {
726 Packet packet;
727 packet.Write(static_cast<u8>(IdModNoSuchUser));
728
729 ENetPacket* enet_packet =
730 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
731 enet_peer_send(client, 0, enet_packet);
732 enet_host_flush(server);
733}
734
735void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) {
736 Packet packet;
737 packet.Write(static_cast<u8>(IdModBanListResponse));
738 {
739 std::lock_guard lock(ban_list_mutex);
740 packet.Write(username_ban_list);
741 packet.Write(ip_ban_list);
742 }
743
744 ENetPacket* enet_packet =
745 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
746 enet_peer_send(client, 0, enet_packet);
747 enet_host_flush(server);
748}
749
750void Room::RoomImpl::SendCloseMessage() {
751 Packet packet;
752 packet.Write(static_cast<u8>(IdCloseRoom));
753 std::lock_guard lock(member_mutex);
754 if (!members.empty()) {
755 ENetPacket* enet_packet =
756 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
757 for (auto& member : members) {
758 enet_peer_send(member.peer, 0, enet_packet);
759 }
760 }
761 enet_host_flush(server);
762 for (auto& member : members) {
763 enet_peer_disconnect(member.peer, 0);
764 }
765}
766
767void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
768 const std::string& username, const std::string& ip) {
769 Packet packet;
770 packet.Write(static_cast<u8>(IdStatusMessage));
771 packet.Write(static_cast<u8>(type));
772 packet.Write(nickname);
773 packet.Write(username);
774 std::lock_guard lock(member_mutex);
775 if (!members.empty()) {
776 ENetPacket* enet_packet =
777 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
778 for (auto& member : members) {
779 enet_peer_send(member.peer, 0, enet_packet);
780 }
781 }
782 enet_host_flush(server);
783
784 const std::string display_name =
785 username.empty() ? nickname : fmt::format("{} ({})", nickname, username);
786
787 switch (type) {
788 case IdMemberJoin:
789 LOG_INFO(Network, "[{}] {} has joined.", ip, display_name);
790 break;
791 case IdMemberLeave:
792 LOG_INFO(Network, "[{}] {} has left.", ip, display_name);
793 break;
794 case IdMemberKicked:
795 LOG_INFO(Network, "[{}] {} has been kicked.", ip, display_name);
796 break;
797 case IdMemberBanned:
798 LOG_INFO(Network, "[{}] {} has been banned.", ip, display_name);
799 break;
800 case IdAddressUnbanned:
801 LOG_INFO(Network, "{} has been unbanned.", display_name);
802 break;
803 }
804}
805
806void Room::RoomImpl::BroadcastRoomInformation() {
807 Packet packet;
808 packet.Write(static_cast<u8>(IdRoomInformation));
809 packet.Write(room_information.name);
810 packet.Write(room_information.description);
811 packet.Write(room_information.member_slots);
812 packet.Write(room_information.port);
813 packet.Write(room_information.preferred_game.name);
814 packet.Write(room_information.host_username);
815
816 packet.Write(static_cast<u32>(members.size()));
817 {
818 std::lock_guard lock(member_mutex);
819 for (const auto& member : members) {
820 packet.Write(member.nickname);
821 packet.Write(member.mac_address);
822 packet.Write(member.game_info.name);
823 packet.Write(member.game_info.id);
824 packet.Write(member.user_data.username);
825 packet.Write(member.user_data.display_name);
826 packet.Write(member.user_data.avatar_url);
827 }
828 }
829
830 ENetPacket* enet_packet =
831 enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
832 enet_host_broadcast(server, 0, enet_packet);
833 enet_host_flush(server);
834}
835
836MacAddress Room::RoomImpl::GenerateMacAddress() {
837 MacAddress result_mac =
838 NintendoOUI; // The first three bytes of each MAC address will be the NintendoOUI
839 std::uniform_int_distribution<> dis(0x00, 0xFF); // Random byte between 0 and 0xFF
840 do {
841 for (std::size_t i = 3; i < result_mac.size(); ++i) {
842 result_mac[i] = dis(random_gen);
843 }
844 } while (!IsValidMacAddress(result_mac));
845 return result_mac;
846}
847
848void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) {
849 Packet in_packet;
850 in_packet.Append(event->packet->data, event->packet->dataLength);
851 in_packet.IgnoreBytes(sizeof(u8)); // Message type
852 in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Type
853 in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Channel
854 in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address
855 MacAddress destination_address;
856 in_packet.Read(destination_address);
857
858 Packet out_packet;
859 out_packet.Append(event->packet->data, event->packet->dataLength);
860 ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(),
861 ENET_PACKET_FLAG_RELIABLE);
862
863 if (destination_address == BroadcastMac) { // Send the data to everyone except the sender
864 std::lock_guard lock(member_mutex);
865 bool sent_packet = false;
866 for (const auto& member : members) {
867 if (member.peer != event->peer) {
868 sent_packet = true;
869 enet_peer_send(member.peer, 0, enet_packet);
870 }
871 }
872
873 if (!sent_packet) {
874 enet_packet_destroy(enet_packet);
875 }
876 } else { // Send the data only to the destination client
877 std::lock_guard lock(member_mutex);
878 auto member = std::find_if(members.begin(), members.end(),
879 [destination_address](const Member& member_entry) -> bool {
880 return member_entry.mac_address == destination_address;
881 });
882 if (member != members.end()) {
883 enet_peer_send(member->peer, 0, enet_packet);
884 } else {
885 LOG_ERROR(Network,
886 "Attempting to send to unknown MAC address: "
887 "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
888 destination_address[0], destination_address[1], destination_address[2],
889 destination_address[3], destination_address[4], destination_address[5]);
890 enet_packet_destroy(enet_packet);
891 }
892 }
893 enet_host_flush(server);
894}
895
896void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) {
897 Packet in_packet;
898 in_packet.Append(event->packet->data, event->packet->dataLength);
899
900 in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
901 std::string message;
902 in_packet.Read(message);
903 auto CompareNetworkAddress = [event](const Member member) -> bool {
904 return member.peer == event->peer;
905 };
906
907 std::lock_guard lock(member_mutex);
908 const auto sending_member = std::find_if(members.begin(), members.end(), CompareNetworkAddress);
909 if (sending_member == members.end()) {
910 return; // Received a chat message from a unknown sender
911 }
912
913 // Limit the size of chat messages to MaxMessageSize
914 message.resize(std::min(static_cast<u32>(message.size()), MaxMessageSize));
915
916 Packet out_packet;
917 out_packet.Write(static_cast<u8>(IdChatMessage));
918 out_packet.Write(sending_member->nickname);
919 out_packet.Write(sending_member->user_data.username);
920 out_packet.Write(message);
921
922 ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(),
923 ENET_PACKET_FLAG_RELIABLE);
924 bool sent_packet = false;
925 for (const auto& member : members) {
926 if (member.peer != event->peer) {
927 sent_packet = true;
928 enet_peer_send(member.peer, 0, enet_packet);
929 }
930 }
931
932 if (!sent_packet) {
933 enet_packet_destroy(enet_packet);
934 }
935
936 enet_host_flush(server);
937
938 if (sending_member->user_data.username.empty()) {
939 LOG_INFO(Network, "{}: {}", sending_member->nickname, message);
940 } else {
941 LOG_INFO(Network, "{} ({}): {}", sending_member->nickname,
942 sending_member->user_data.username, message);
943 }
944}
945
946void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
947 Packet in_packet;
948 in_packet.Append(event->packet->data, event->packet->dataLength);
949
950 in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
951 GameInfo game_info;
952 in_packet.Read(game_info.name);
953 in_packet.Read(game_info.id);
954
955 {
956 std::lock_guard lock(member_mutex);
957 auto member = std::find_if(members.begin(), members.end(),
958 [event](const Member& member_entry) -> bool {
959 return member_entry.peer == event->peer;
960 });
961 if (member != members.end()) {
962 member->game_info = game_info;
963
964 const std::string display_name =
965 member->user_data.username.empty()
966 ? member->nickname
967 : fmt::format("{} ({})", member->nickname, member->user_data.username);
968
969 if (game_info.name.empty()) {
970 LOG_INFO(Network, "{} is not playing", display_name);
971 } else {
972 LOG_INFO(Network, "{} is playing {}", display_name, game_info.name);
973 }
974 }
975 }
976 BroadcastRoomInformation();
977}
978
979void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) {
980 // Remove the client from the members list.
981 std::string nickname, username, ip;
982 {
983 std::lock_guard lock(member_mutex);
984 auto member =
985 std::find_if(members.begin(), members.end(), [client](const Member& member_entry) {
986 return member_entry.peer == client;
987 });
988 if (member != members.end()) {
989 nickname = member->nickname;
990 username = member->user_data.username;
991
992 std::array<char, 256> ip_raw{};
993 enet_address_get_host_ip(&member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1);
994 ip = ip_raw.data();
995
996 members.erase(member);
997 }
998 }
999
1000 // Announce the change to all clients.
1001 enet_peer_disconnect(client, 0);
1002 if (!nickname.empty())
1003 SendStatusMessage(IdMemberLeave, nickname, username, ip);
1004 BroadcastRoomInformation();
1005}
1006
1007// Room
1008Room::Room() : room_impl{std::make_unique<RoomImpl>()} {}
1009
1010Room::~Room() = default;
1011
1012bool Room::Create(const std::string& name, const std::string& description,
1013 const std::string& server_address, u16 server_port, const std::string& password,
1014 const u32 max_connections, const std::string& host_username,
1015 const GameInfo preferred_game,
1016 std::unique_ptr<VerifyUser::Backend> verify_backend,
1017 const Room::BanList& ban_list, bool enable_yuzu_mods) {
1018 ENetAddress address;
1019 address.host = ENET_HOST_ANY;
1020 if (!server_address.empty()) {
1021 enet_address_set_host(&address, server_address.c_str());
1022 }
1023 address.port = server_port;
1024
1025 // In order to send the room is full message to the connecting client, we need to leave one
1026 // slot open so enet won't reject the incoming connection without telling us
1027 room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0);
1028 if (!room_impl->server) {
1029 return false;
1030 }
1031 room_impl->state = State::Open;
1032
1033 room_impl->room_information.name = name;
1034 room_impl->room_information.description = description;
1035 room_impl->room_information.member_slots = max_connections;
1036 room_impl->room_information.port = server_port;
1037 room_impl->room_information.preferred_game = preferred_game;
1038 room_impl->room_information.host_username = host_username;
1039 room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods;
1040 room_impl->password = password;
1041 room_impl->verify_backend = std::move(verify_backend);
1042 room_impl->username_ban_list = ban_list.first;
1043 room_impl->ip_ban_list = ban_list.second;
1044
1045 room_impl->StartLoop();
1046 return true;
1047}
1048
1049Room::State Room::GetState() const {
1050 return room_impl->state;
1051}
1052
1053const RoomInformation& Room::GetRoomInformation() const {
1054 return room_impl->room_information;
1055}
1056
1057std::string Room::GetVerifyUID() const {
1058 std::lock_guard lock(room_impl->verify_uid_mutex);
1059 return room_impl->verify_uid;
1060}
1061
1062Room::BanList Room::GetBanList() const {
1063 std::lock_guard lock(room_impl->ban_list_mutex);
1064 return {room_impl->username_ban_list, room_impl->ip_ban_list};
1065}
1066
1067std::vector<Member> Room::GetRoomMemberList() const {
1068 std::vector<Member> member_list;
1069 std::lock_guard lock(room_impl->member_mutex);
1070 for (const auto& member_impl : room_impl->members) {
1071 Member member;
1072 member.nickname = member_impl.nickname;
1073 member.username = member_impl.user_data.username;
1074 member.display_name = member_impl.user_data.display_name;
1075 member.avatar_url = member_impl.user_data.avatar_url;
1076 member.mac_address = member_impl.mac_address;
1077 member.game = member_impl.game_info;
1078 member_list.push_back(member);
1079 }
1080 return member_list;
1081}
1082
1083bool Room::HasPassword() const {
1084 return !room_impl->password.empty();
1085}
1086
1087void Room::SetVerifyUID(const std::string& uid) {
1088 std::lock_guard lock(room_impl->verify_uid_mutex);
1089 room_impl->verify_uid = uid;
1090}
1091
1092void Room::Destroy() {
1093 room_impl->state = State::Closed;
1094 room_impl->room_thread->join();
1095 room_impl->room_thread.reset();
1096
1097 if (room_impl->server) {
1098 enet_host_destroy(room_impl->server);
1099 }
1100 room_impl->room_information = {};
1101 room_impl->server = nullptr;
1102 {
1103 std::lock_guard lock(room_impl->member_mutex);
1104 room_impl->members.clear();
1105 }
1106 room_impl->room_information.member_slots = 0;
1107 room_impl->room_information.name.clear();
1108}
1109
1110} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <array>
7#include <memory>
8#include <string>
9#include <vector>
10#include "common/announce_multiplayer_room.h"
11#include "common/common_types.h"
12#include "network/verify_user.h"
13
14namespace Network {
15
16using AnnounceMultiplayerRoom::GameInfo;
17using AnnounceMultiplayerRoom::MacAddress;
18using AnnounceMultiplayerRoom::Member;
19using AnnounceMultiplayerRoom::RoomInformation;
20
21constexpr u32 network_version = 1; ///< The version of this Room and RoomMember
22
23constexpr u16 DefaultRoomPort = 24872;
24
25constexpr u32 MaxMessageSize = 500;
26
27/// Maximum number of concurrent connections allowed to this room.
28static constexpr u32 MaxConcurrentConnections = 254;
29
30constexpr std::size_t NumChannels = 1; // Number of channels used for the connection
31
32/// A special MAC address that tells the room we're joining to assign us a MAC address
33/// automatically.
34constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
35
36// 802.11 broadcast MAC address
37constexpr MacAddress BroadcastMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
38
39// The different types of messages that can be sent. The first byte of each packet defines the type
40enum RoomMessageTypes : u8 {
41 IdJoinRequest = 1,
42 IdJoinSuccess,
43 IdRoomInformation,
44 IdSetGameInfo,
45 IdWifiPacket,
46 IdChatMessage,
47 IdNameCollision,
48 IdMacCollision,
49 IdVersionMismatch,
50 IdWrongPassword,
51 IdCloseRoom,
52 IdRoomIsFull,
53 IdConsoleIdCollision,
54 IdStatusMessage,
55 IdHostKicked,
56 IdHostBanned,
57 /// Moderation requests
58 IdModKick,
59 IdModBan,
60 IdModUnban,
61 IdModGetBanList,
62 // Moderation responses
63 IdModBanListResponse,
64 IdModPermissionDenied,
65 IdModNoSuchUser,
66 IdJoinSuccessAsMod,
67};
68
69/// Types of system status messages
70enum StatusMessageTypes : u8 {
71 IdMemberJoin = 1, ///< Member joining
72 IdMemberLeave, ///< Member leaving
73 IdMemberKicked, ///< A member is kicked from the room
74 IdMemberBanned, ///< A member is banned from the room
75 IdAddressUnbanned, ///< A username / ip address is unbanned from the room
76};
77
78/// This is what a server [person creating a server] would use.
79class Room final {
80public:
81 enum class State : u8 {
82 Open, ///< The room is open and ready to accept connections.
83 Closed, ///< The room is not opened and can not accept connections.
84 };
85
86 Room();
87 ~Room();
88
89 /**
90 * Gets the current state of the room.
91 */
92 State GetState() const;
93
94 /**
95 * Gets the room information of the room.
96 */
97 const RoomInformation& GetRoomInformation() const;
98
99 /**
100 * Gets the verify UID of this room.
101 */
102 std::string GetVerifyUID() const;
103
104 /**
105 * Gets a list of the mbmers connected to the room.
106 */
107 std::vector<Member> GetRoomMemberList() const;
108
109 /**
110 * Checks if the room is password protected
111 */
112 bool HasPassword() const;
113
114 using UsernameBanList = std::vector<std::string>;
115 using IPBanList = std::vector<std::string>;
116
117 using BanList = std::pair<UsernameBanList, IPBanList>;
118
119 /**
120 * Creates the socket for this room. Will bind to default address if
121 * server is empty string.
122 */
123 bool Create(const std::string& name, const std::string& description = "",
124 const std::string& server = "", u16 server_port = DefaultRoomPort,
125 const std::string& password = "",
126 const u32 max_connections = MaxConcurrentConnections,
127 const std::string& host_username = "", const GameInfo = {},
128 std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr,
129 const BanList& ban_list = {}, bool enable_yuzu_mods = false);
130
131 /**
132 * Sets the verification GUID of the room.
133 */
134 void SetVerifyUID(const std::string& uid);
135
136 /**
137 * Gets the ban list (including banned forum usernames and IPs) of the room.
138 */
139 BanList GetBanList() const;
140
141 /**
142 * Destroys the socket
143 */
144 void Destroy();
145
146private:
147 class RoomImpl;
148 std::unique_ptr<RoomImpl> room_impl;
149};
150
151} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <atomic>
5#include <list>
6#include <mutex>
7#include <set>
8#include <thread>
9#include "common/assert.h"
10#include "enet/enet.h"
11#include "network/packet.h"
12#include "network/room_member.h"
13
14namespace Network {
15
16constexpr u32 ConnectionTimeoutMs = 5000;
17
18class RoomMember::RoomMemberImpl {
19public:
20 ENetHost* client = nullptr; ///< ENet network interface.
21 ENetPeer* server = nullptr; ///< The server peer the client is connected to
22
23 /// Information about the clients connected to the same room as us.
24 MemberList member_information;
25 /// Information about the room we're connected to.
26 RoomInformation room_information;
27
28 /// The current game name, id and version
29 GameInfo current_game_info;
30
31 std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
32 void SetState(const State new_state);
33 void SetError(const Error new_error);
34 bool IsConnected() const;
35
36 std::string nickname; ///< The nickname of this member.
37
38 std::string username; ///< The username of this member.
39 mutable std::mutex username_mutex; ///< Mutex for locking username.
40
41 MacAddress mac_address; ///< The mac_address of this member.
42
43 std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
44 /// Thread that receives and dispatches network packets
45 std::unique_ptr<std::thread> loop_thread;
46 std::mutex send_list_mutex; ///< Mutex that controls access to the `send_list` variable.
47 std::list<Packet> send_list; ///< A list that stores all packets to send the async
48
49 template <typename T>
50 using CallbackSet = std::set<CallbackHandle<T>>;
51 std::mutex callback_mutex; ///< The mutex used for handling callbacks
52
53 class Callbacks {
54 public:
55 template <typename T>
56 CallbackSet<T>& Get();
57
58 private:
59 CallbackSet<WifiPacket> callback_set_wifi_packet;
60 CallbackSet<ChatEntry> callback_set_chat_messages;
61 CallbackSet<StatusMessageEntry> callback_set_status_messages;
62 CallbackSet<RoomInformation> callback_set_room_information;
63 CallbackSet<State> callback_set_state;
64 CallbackSet<Error> callback_set_error;
65 CallbackSet<Room::BanList> callback_set_ban_list;
66 };
67 Callbacks callbacks; ///< All CallbackSets to all events
68
69 void MemberLoop();
70
71 void StartLoop();
72
73 /**
74 * Sends data to the room. It will be send on channel 0 with flag RELIABLE
75 * @param packet The data to send
76 */
77 void Send(Packet&& packet);
78
79 /**
80 * Sends a request to the server, asking for permission to join a room with the specified
81 * nickname and preferred mac.
82 * @params nickname The desired nickname.
83 * @params console_id_hash A hash of the Console ID.
84 * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells
85 * @params password The password for the room
86 * the server to assign one for us.
87 */
88 void SendJoinRequest(const std::string& nickname_, const std::string& console_id_hash,
89 const MacAddress& preferred_mac = NoPreferredMac,
90 const std::string& password = "", const std::string& token = "");
91
92 /**
93 * Extracts a MAC Address from a received ENet packet.
94 * @param event The ENet event that was received.
95 */
96 void HandleJoinPacket(const ENetEvent* event);
97 /**
98 * Extracts RoomInformation and MemberInformation from a received ENet packet.
99 * @param event The ENet event that was received.
100 */
101 void HandleRoomInformationPacket(const ENetEvent* event);
102
103 /**
104 * Extracts a WifiPacket from a received ENet packet.
105 * @param event The ENet event that was received.
106 */
107 void HandleWifiPackets(const ENetEvent* event);
108
109 /**
110 * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
111 * @param event The ENet event that was received.
112 */
113 void HandleChatPacket(const ENetEvent* event);
114
115 /**
116 * Extracts a system message entry from a received ENet packet and adds it to the system message
117 * queue.
118 * @param event The ENet event that was received.
119 */
120 void HandleStatusMessagePacket(const ENetEvent* event);
121
122 /**
123 * Extracts a ban list request response from a received ENet packet.
124 * @param event The ENet event that was received.
125 */
126 void HandleModBanListResponsePacket(const ENetEvent* event);
127
128 /**
129 * Disconnects the RoomMember from the Room
130 */
131 void Disconnect();
132
133 template <typename T>
134 void Invoke(const T& data);
135
136 template <typename T>
137 CallbackHandle<T> Bind(std::function<void(const T&)> callback);
138};
139
140// RoomMemberImpl
141void RoomMember::RoomMemberImpl::SetState(const State new_state) {
142 if (state != new_state) {
143 state = new_state;
144 Invoke<State>(state);
145 }
146}
147
148void RoomMember::RoomMemberImpl::SetError(const Error new_error) {
149 Invoke<Error>(new_error);
150}
151
152bool RoomMember::RoomMemberImpl::IsConnected() const {
153 return state == State::Joining || state == State::Joined || state == State::Moderator;
154}
155
156void RoomMember::RoomMemberImpl::MemberLoop() {
157 // Receive packets while the connection is open
158 while (IsConnected()) {
159 std::lock_guard lock(network_mutex);
160 ENetEvent event;
161 if (enet_host_service(client, &event, 16) > 0) {
162 switch (event.type) {
163 case ENET_EVENT_TYPE_RECEIVE:
164 switch (event.packet->data[0]) {
165 case IdWifiPacket:
166 HandleWifiPackets(&event);
167 break;
168 case IdChatMessage:
169 HandleChatPacket(&event);
170 break;
171 case IdStatusMessage:
172 HandleStatusMessagePacket(&event);
173 break;
174 case IdRoomInformation:
175 HandleRoomInformationPacket(&event);
176 break;
177 case IdJoinSuccess:
178 case IdJoinSuccessAsMod:
179 // The join request was successful, we are now in the room.
180 // If we joined successfully, there must be at least one client in the room: us.
181 ASSERT_MSG(member_information.size() > 0,
182 "We have not yet received member information.");
183 HandleJoinPacket(&event); // Get the MAC Address for the client
184 if (event.packet->data[0] == IdJoinSuccessAsMod) {
185 SetState(State::Moderator);
186 } else {
187 SetState(State::Joined);
188 }
189 break;
190 case IdModBanListResponse:
191 HandleModBanListResponsePacket(&event);
192 break;
193 case IdRoomIsFull:
194 SetState(State::Idle);
195 SetError(Error::RoomIsFull);
196 break;
197 case IdNameCollision:
198 SetState(State::Idle);
199 SetError(Error::NameCollision);
200 break;
201 case IdMacCollision:
202 SetState(State::Idle);
203 SetError(Error::MacCollision);
204 break;
205 case IdConsoleIdCollision:
206 SetState(State::Idle);
207 SetError(Error::ConsoleIdCollision);
208 break;
209 case IdVersionMismatch:
210 SetState(State::Idle);
211 SetError(Error::WrongVersion);
212 break;
213 case IdWrongPassword:
214 SetState(State::Idle);
215 SetError(Error::WrongPassword);
216 break;
217 case IdCloseRoom:
218 SetState(State::Idle);
219 SetError(Error::LostConnection);
220 break;
221 case IdHostKicked:
222 SetState(State::Idle);
223 SetError(Error::HostKicked);
224 break;
225 case IdHostBanned:
226 SetState(State::Idle);
227 SetError(Error::HostBanned);
228 break;
229 case IdModPermissionDenied:
230 SetError(Error::PermissionDenied);
231 break;
232 case IdModNoSuchUser:
233 SetError(Error::NoSuchUser);
234 break;
235 }
236 enet_packet_destroy(event.packet);
237 break;
238 case ENET_EVENT_TYPE_DISCONNECT:
239 if (state == State::Joined || state == State::Moderator) {
240 SetState(State::Idle);
241 SetError(Error::LostConnection);
242 }
243 break;
244 case ENET_EVENT_TYPE_NONE:
245 break;
246 case ENET_EVENT_TYPE_CONNECT:
247 // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
248 // already connected
249 ASSERT_MSG(false, "Received unexpected connect event while already connected");
250 break;
251 }
252 }
253 std::list<Packet> packets;
254 {
255 std::lock_guard send_lock(send_list_mutex);
256 packets.swap(send_list);
257 }
258 for (const auto& packet : packets) {
259 ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(),
260 ENET_PACKET_FLAG_RELIABLE);
261 enet_peer_send(server, 0, enetPacket);
262 }
263 enet_host_flush(client);
264 }
265 Disconnect();
266};
267
268void RoomMember::RoomMemberImpl::StartLoop() {
269 loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this);
270}
271
272void RoomMember::RoomMemberImpl::Send(Packet&& packet) {
273 std::lock_guard lock(send_list_mutex);
274 send_list.push_back(std::move(packet));
275}
276
277void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_,
278 const std::string& console_id_hash,
279 const MacAddress& preferred_mac,
280 const std::string& password,
281 const std::string& token) {
282 Packet packet;
283 packet.Write(static_cast<u8>(IdJoinRequest));
284 packet.Write(nickname_);
285 packet.Write(console_id_hash);
286 packet.Write(preferred_mac);
287 packet.Write(network_version);
288 packet.Write(password);
289 packet.Write(token);
290 Send(std::move(packet));
291}
292
293void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) {
294 Packet packet;
295 packet.Append(event->packet->data, event->packet->dataLength);
296
297 // Ignore the first byte, which is the message id.
298 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
299
300 RoomInformation info{};
301 packet.Read(info.name);
302 packet.Read(info.description);
303 packet.Read(info.member_slots);
304 packet.Read(info.port);
305 packet.Read(info.preferred_game.name);
306 packet.Read(info.host_username);
307 room_information.name = info.name;
308 room_information.description = info.description;
309 room_information.member_slots = info.member_slots;
310 room_information.port = info.port;
311 room_information.preferred_game = info.preferred_game;
312 room_information.host_username = info.host_username;
313
314 u32 num_members;
315 packet.Read(num_members);
316 member_information.resize(num_members);
317
318 for (auto& member : member_information) {
319 packet.Read(member.nickname);
320 packet.Read(member.mac_address);
321 packet.Read(member.game_info.name);
322 packet.Read(member.game_info.id);
323 packet.Read(member.username);
324 packet.Read(member.display_name);
325 packet.Read(member.avatar_url);
326
327 {
328 std::lock_guard lock(username_mutex);
329 if (member.nickname == nickname) {
330 username = member.username;
331 }
332 }
333 }
334 Invoke(room_information);
335}
336
337void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
338 Packet packet;
339 packet.Append(event->packet->data, event->packet->dataLength);
340
341 // Ignore the first byte, which is the message id.
342 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
343
344 // Parse the MAC Address from the packet
345 packet.Read(mac_address);
346}
347
348void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) {
349 WifiPacket wifi_packet{};
350 Packet packet;
351 packet.Append(event->packet->data, event->packet->dataLength);
352
353 // Ignore the first byte, which is the message id.
354 packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
355
356 // Parse the WifiPacket from the packet
357 u8 frame_type;
358 packet.Read(frame_type);
359 WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type);
360
361 wifi_packet.type = type;
362 packet.Read(wifi_packet.channel);
363 packet.Read(wifi_packet.transmitter_address);
364 packet.Read(wifi_packet.destination_address);
365 packet.Read(wifi_packet.data);
366
367 Invoke<WifiPacket>(wifi_packet);
368}
369
370void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
371 Packet packet;
372 packet.Append(event->packet->data, event->packet->dataLength);
373
374 // Ignore the first byte, which is the message id.
375 packet.IgnoreBytes(sizeof(u8));
376
377 ChatEntry chat_entry{};
378 packet.Read(chat_entry.nickname);
379 packet.Read(chat_entry.username);
380 packet.Read(chat_entry.message);
381 Invoke<ChatEntry>(chat_entry);
382}
383
384void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
385 Packet packet;
386 packet.Append(event->packet->data, event->packet->dataLength);
387
388 // Ignore the first byte, which is the message id.
389 packet.IgnoreBytes(sizeof(u8));
390
391 StatusMessageEntry status_message_entry{};
392 u8 type{};
393 packet.Read(type);
394 status_message_entry.type = static_cast<StatusMessageTypes>(type);
395 packet.Read(status_message_entry.nickname);
396 packet.Read(status_message_entry.username);
397 Invoke<StatusMessageEntry>(status_message_entry);
398}
399
400void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) {
401 Packet packet;
402 packet.Append(event->packet->data, event->packet->dataLength);
403
404 // Ignore the first byte, which is the message id.
405 packet.IgnoreBytes(sizeof(u8));
406
407 Room::BanList ban_list = {};
408 packet.Read(ban_list.first);
409 packet.Read(ban_list.second);
410 Invoke<Room::BanList>(ban_list);
411}
412
413void RoomMember::RoomMemberImpl::Disconnect() {
414 member_information.clear();
415 room_information.member_slots = 0;
416 room_information.name.clear();
417
418 if (!server) {
419 return;
420 }
421 enet_peer_disconnect(server, 0);
422
423 ENetEvent event;
424 while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) {
425 switch (event.type) {
426 case ENET_EVENT_TYPE_RECEIVE:
427 enet_packet_destroy(event.packet); // Ignore all incoming data
428 break;
429 case ENET_EVENT_TYPE_DISCONNECT:
430 server = nullptr;
431 return;
432 case ENET_EVENT_TYPE_NONE:
433 case ENET_EVENT_TYPE_CONNECT:
434 break;
435 }
436 }
437 // didn't disconnect gracefully force disconnect
438 enet_peer_reset(server);
439 server = nullptr;
440}
441
442template <>
443RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
444 return callback_set_wifi_packet;
445}
446
447template <>
448RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
449RoomMember::RoomMemberImpl::Callbacks::Get() {
450 return callback_set_state;
451}
452
453template <>
454RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>&
455RoomMember::RoomMemberImpl::Callbacks::Get() {
456 return callback_set_error;
457}
458
459template <>
460RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>&
461RoomMember::RoomMemberImpl::Callbacks::Get() {
462 return callback_set_room_information;
463}
464
465template <>
466RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() {
467 return callback_set_chat_messages;
468}
469
470template <>
471RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>&
472RoomMember::RoomMemberImpl::Callbacks::Get() {
473 return callback_set_status_messages;
474}
475
476template <>
477RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>&
478RoomMember::RoomMemberImpl::Callbacks::Get() {
479 return callback_set_ban_list;
480}
481
482template <typename T>
483void RoomMember::RoomMemberImpl::Invoke(const T& data) {
484 std::lock_guard lock(callback_mutex);
485 CallbackSet<T> callback_set = callbacks.Get<T>();
486 for (auto const& callback : callback_set) {
487 (*callback)(data);
488 }
489}
490
491template <typename T>
492RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind(
493 std::function<void(const T&)> callback) {
494 std::lock_guard lock(callback_mutex);
495 CallbackHandle<T> handle;
496 handle = std::make_shared<std::function<void(const T&)>>(callback);
497 callbacks.Get<T>().insert(handle);
498 return handle;
499}
500
501// RoomMember
502RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {}
503
504RoomMember::~RoomMember() {
505 ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected");
506 if (room_member_impl->loop_thread) {
507 Leave();
508 }
509}
510
511RoomMember::State RoomMember::GetState() const {
512 return room_member_impl->state;
513}
514
515const RoomMember::MemberList& RoomMember::GetMemberInformation() const {
516 return room_member_impl->member_information;
517}
518
519const std::string& RoomMember::GetNickname() const {
520 return room_member_impl->nickname;
521}
522
523const std::string& RoomMember::GetUsername() const {
524 std::lock_guard lock(room_member_impl->username_mutex);
525 return room_member_impl->username;
526}
527
528const MacAddress& RoomMember::GetMacAddress() const {
529 ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected");
530 return room_member_impl->mac_address;
531}
532
533RoomInformation RoomMember::GetRoomInformation() const {
534 return room_member_impl->room_information;
535}
536
537void RoomMember::Join(const std::string& nick, const std::string& console_id_hash,
538 const char* server_addr, u16 server_port, u16 client_port,
539 const MacAddress& preferred_mac, const std::string& password,
540 const std::string& token) {
541 // If the member is connected, kill the connection first
542 if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) {
543 Leave();
544 }
545 // If the thread isn't running but the ptr still exists, reset it
546 else if (room_member_impl->loop_thread) {
547 room_member_impl->loop_thread.reset();
548 }
549
550 if (!room_member_impl->client) {
551 room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
552 ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
553 }
554
555 room_member_impl->SetState(State::Joining);
556
557 ENetAddress address{};
558 enet_address_set_host(&address, server_addr);
559 address.port = server_port;
560 room_member_impl->server =
561 enet_host_connect(room_member_impl->client, &address, NumChannels, 0);
562
563 if (!room_member_impl->server) {
564 room_member_impl->SetState(State::Idle);
565 room_member_impl->SetError(Error::UnknownError);
566 return;
567 }
568
569 ENetEvent event{};
570 int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
571 if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
572 room_member_impl->nickname = nick;
573 room_member_impl->StartLoop();
574 room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token);
575 SendGameInfo(room_member_impl->current_game_info);
576 } else {
577 enet_peer_disconnect(room_member_impl->server, 0);
578 room_member_impl->SetState(State::Idle);
579 room_member_impl->SetError(Error::CouldNotConnect);
580 }
581}
582
583bool RoomMember::IsConnected() const {
584 return room_member_impl->IsConnected();
585}
586
587void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) {
588 Packet packet;
589 packet.Write(static_cast<u8>(IdWifiPacket));
590 packet.Write(static_cast<u8>(wifi_packet.type));
591 packet.Write(wifi_packet.channel);
592 packet.Write(wifi_packet.transmitter_address);
593 packet.Write(wifi_packet.destination_address);
594 packet.Write(wifi_packet.data);
595 room_member_impl->Send(std::move(packet));
596}
597
598void RoomMember::SendChatMessage(const std::string& message) {
599 Packet packet;
600 packet.Write(static_cast<u8>(IdChatMessage));
601 packet.Write(message);
602 room_member_impl->Send(std::move(packet));
603}
604
605void RoomMember::SendGameInfo(const GameInfo& game_info) {
606 room_member_impl->current_game_info = game_info;
607 if (!IsConnected())
608 return;
609
610 Packet packet;
611 packet.Write(static_cast<u8>(IdSetGameInfo));
612 packet.Write(game_info.name);
613 packet.Write(game_info.id);
614 room_member_impl->Send(std::move(packet));
615}
616
617void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) {
618 ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban,
619 "type is not a moderation request");
620 if (!IsConnected())
621 return;
622
623 Packet packet;
624 packet.Write(static_cast<u8>(type));
625 packet.Write(nickname);
626 room_member_impl->Send(std::move(packet));
627}
628
629void RoomMember::RequestBanList() {
630 if (!IsConnected())
631 return;
632
633 Packet packet;
634 packet.Write(static_cast<u8>(IdModGetBanList));
635 room_member_impl->Send(std::move(packet));
636}
637
638RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged(
639 std::function<void(const RoomMember::State&)> callback) {
640 return room_member_impl->Bind(callback);
641}
642
643RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError(
644 std::function<void(const RoomMember::Error&)> callback) {
645 return room_member_impl->Bind(callback);
646}
647
648RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived(
649 std::function<void(const WifiPacket&)> callback) {
650 return room_member_impl->Bind(callback);
651}
652
653RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
654 std::function<void(const RoomInformation&)> callback) {
655 return room_member_impl->Bind(callback);
656}
657
658RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved(
659 std::function<void(const ChatEntry&)> callback) {
660 return room_member_impl->Bind(callback);
661}
662
663RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived(
664 std::function<void(const StatusMessageEntry&)> callback) {
665 return room_member_impl->Bind(callback);
666}
667
668RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived(
669 std::function<void(const Room::BanList&)> callback) {
670 return room_member_impl->Bind(callback);
671}
672
673template <typename T>
674void RoomMember::Unbind(CallbackHandle<T> handle) {
675 std::lock_guard lock(room_member_impl->callback_mutex);
676 room_member_impl->callbacks.Get<T>().erase(handle);
677}
678
679void RoomMember::Leave() {
680 room_member_impl->SetState(State::Idle);
681 room_member_impl->loop_thread->join();
682 room_member_impl->loop_thread.reset();
683
684 enet_host_destroy(room_member_impl->client);
685 room_member_impl->client = nullptr;
686}
687
688template void RoomMember::Unbind(CallbackHandle<WifiPacket>);
689template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
690template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
691template void RoomMember::Unbind(CallbackHandle<RoomInformation>);
692template void RoomMember::Unbind(CallbackHandle<ChatEntry>);
693template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>);
694template void RoomMember::Unbind(CallbackHandle<Room::BanList>);
695
696} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <functional>
7#include <memory>
8#include <string>
9#include <vector>
10#include "common/announce_multiplayer_room.h"
11#include "common/common_types.h"
12#include "network/room.h"
13
14namespace Network {
15
16using AnnounceMultiplayerRoom::GameInfo;
17using AnnounceMultiplayerRoom::RoomInformation;
18
19/// Information about the received WiFi packets.
20/// Acts as our own 802.11 header.
21struct WifiPacket {
22 enum class PacketType : u8 {
23 Beacon,
24 Data,
25 Authentication,
26 AssociationResponse,
27 Deauthentication,
28 NodeMap
29 };
30 PacketType type; ///< The type of 802.11 frame.
31 std::vector<u8> data; ///< Raw 802.11 frame data, starting at the management frame header
32 /// for management frames.
33 MacAddress transmitter_address; ///< Mac address of the transmitter.
34 MacAddress destination_address; ///< Mac address of the receiver.
35 u8 channel; ///< WiFi channel where this frame was transmitted.
36};
37
38/// Represents a chat message.
39struct ChatEntry {
40 std::string nickname; ///< Nickname of the client who sent this message.
41 /// Web services username of the client who sent this message, can be empty.
42 std::string username;
43 std::string message; ///< Body of the message.
44};
45
46/// Represents a system status message.
47struct StatusMessageEntry {
48 StatusMessageTypes type; ///< Type of the message
49 /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc.
50 std::string nickname;
51 std::string username;
52};
53
54/**
55 * This is what a client [person joining a server] would use.
56 * It also has to be used if you host a game yourself (You'd create both, a Room and a
57 * RoomMembership for yourself)
58 */
59class RoomMember final {
60public:
61 enum class State : u8 {
62 Uninitialized, ///< Not initialized
63 Idle, ///< Default state (i.e. not connected)
64 Joining, ///< The client is attempting to join a room.
65 Joined, ///< The client is connected to the room and is ready to send/receive packets.
66 Moderator, ///< The client is connnected to the room and is granted mod permissions.
67 };
68
69 enum class Error : u8 {
70 // Reasons why connection was closed
71 LostConnection, ///< Connection closed
72 HostKicked, ///< Kicked by the host
73
74 // Reasons why connection was rejected
75 UnknownError, ///< Some error [permissions to network device missing or something]
76 NameCollision, ///< Somebody is already using this name
77 MacCollision, ///< Somebody is already using that mac-address
78 ConsoleIdCollision, ///< Somebody in the room has the same Console ID
79 WrongVersion, ///< The room version is not the same as for this RoomMember
80 WrongPassword, ///< The password doesn't match the one from the Room
81 CouldNotConnect, ///< The room is not responding to a connection attempt
82 RoomIsFull, ///< Room is already at the maximum number of players
83 HostBanned, ///< The user is banned by the host
84
85 // Reasons why moderation request failed
86 PermissionDenied, ///< The user does not have mod permissions
87 NoSuchUser, ///< The nickname the user attempts to kick/ban does not exist
88 };
89
90 struct MemberInformation {
91 std::string nickname; ///< Nickname of the member.
92 std::string username; ///< The web services username of the member. Can be empty.
93 std::string display_name; ///< The web services display name of the member. Can be empty.
94 std::string avatar_url; ///< Url to the member's avatar. Can be empty.
95 GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're
96 /// not playing anything.
97 MacAddress mac_address; ///< MAC address associated with this member.
98 };
99 using MemberList = std::vector<MemberInformation>;
100
101 // The handle for the callback functions
102 template <typename T>
103 using CallbackHandle = std::shared_ptr<std::function<void(const T&)>>;
104
105 /**
106 * Unbinds a callback function from the events.
107 * @param handle The connection handle to disconnect
108 */
109 template <typename T>
110 void Unbind(CallbackHandle<T> handle);
111
112 RoomMember();
113 ~RoomMember();
114
115 /**
116 * Returns the status of our connection to the room.
117 */
118 State GetState() const;
119
120 /**
121 * Returns information about the members in the room we're currently connected to.
122 */
123 const MemberList& GetMemberInformation() const;
124
125 /**
126 * Returns the nickname of the RoomMember.
127 */
128 const std::string& GetNickname() const;
129
130 /**
131 * Returns the username of the RoomMember.
132 */
133 const std::string& GetUsername() const;
134
135 /**
136 * Returns the MAC address of the RoomMember.
137 */
138 const MacAddress& GetMacAddress() const;
139
140 /**
141 * Returns information about the room we're currently connected to.
142 */
143 RoomInformation GetRoomInformation() const;
144
145 /**
146 * Returns whether we're connected to a server or not.
147 */
148 bool IsConnected() const;
149
150 /**
151 * Attempts to join a room at the specified address and port, using the specified nickname.
152 * A console ID hash is passed in to check console ID conflicts.
153 * This may fail if the username or console ID is already taken.
154 */
155 void Join(const std::string& nickname, const std::string& console_id_hash,
156 const char* server_addr = "127.0.0.1", u16 server_port = DefaultRoomPort,
157 u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac,
158 const std::string& password = "", const std::string& token = "");
159
160 /**
161 * Sends a WiFi packet to the room.
162 * @param packet The WiFi packet to send.
163 */
164 void SendWifiPacket(const WifiPacket& packet);
165
166 /**
167 * Sends a chat message to the room.
168 * @param message The contents of the message.
169 */
170 void SendChatMessage(const std::string& message);
171
172 /**
173 * Sends the current game info to the room.
174 * @param game_info The game information.
175 */
176 void SendGameInfo(const GameInfo& game_info);
177
178 /**
179 * Sends a moderation request to the room.
180 * @param type Moderation request type.
181 * @param nickname The subject of the request. (i.e. the user you want to kick/ban)
182 */
183 void SendModerationRequest(RoomMessageTypes type, const std::string& nickname);
184
185 /**
186 * Attempts to retrieve ban list from the room.
187 * If success, the ban list callback would be called. Otherwise an error would be emitted.
188 */
189 void RequestBanList();
190
191 /**
192 * Binds a function to an event that will be triggered every time the State of the member
193 * changed. The function wil be called every time the event is triggered. The callback function
194 * must not bind or unbind a function. Doing so will cause a deadlock
195 * @param callback The function to call
196 * @return A handle used for removing the function from the registered list
197 */
198 CallbackHandle<State> BindOnStateChanged(std::function<void(const State&)> callback);
199
200 /**
201 * Binds a function to an event that will be triggered every time an error happened. The
202 * function wil be called every time the event is triggered. The callback function must not bind
203 * or unbind a function. Doing so will cause a deadlock
204 * @param callback The function to call
205 * @return A handle used for removing the function from the registered list
206 */
207 CallbackHandle<Error> BindOnError(std::function<void(const Error&)> callback);
208
209 /**
210 * Binds a function to an event that will be triggered every time a WifiPacket is received.
211 * The function wil be called everytime the event is triggered.
212 * The callback function must not bind or unbind a function. Doing so will cause a deadlock
213 * @param callback The function to call
214 * @return A handle used for removing the function from the registered list
215 */
216 CallbackHandle<WifiPacket> BindOnWifiPacketReceived(
217 std::function<void(const WifiPacket&)> callback);
218
219 /**
220 * Binds a function to an event that will be triggered every time the RoomInformation changes.
221 * The function wil be called every time the event is triggered.
222 * The callback function must not bind or unbind a function. Doing so will cause a deadlock
223 * @param callback The function to call
224 * @return A handle used for removing the function from the registered list
225 */
226 CallbackHandle<RoomInformation> BindOnRoomInformationChanged(
227 std::function<void(const RoomInformation&)> callback);
228
229 /**
230 * Binds a function to an event that will be triggered every time a ChatMessage is received.
231 * The function wil be called every time the event is triggered.
232 * The callback function must not bind or unbind a function. Doing so will cause a deadlock
233 * @param callback The function to call
234 * @return A handle used for removing the function from the registered list
235 */
236 CallbackHandle<ChatEntry> BindOnChatMessageRecieved(
237 std::function<void(const ChatEntry&)> callback);
238
239 /**
240 * Binds a function to an event that will be triggered every time a StatusMessage is
241 * received. The function will be called every time the event is triggered. The callback
242 * function must not bind or unbind a function. Doing so will cause a deadlock
243 * @param callback The function to call
244 * @return A handle used for removing the function from the registered list
245 */
246 CallbackHandle<StatusMessageEntry> BindOnStatusMessageReceived(
247 std::function<void(const StatusMessageEntry&)> callback);
248
249 /**
250 * Binds a function to an event that will be triggered every time a requested ban list
251 * received. The function will be called every time the event is triggered. The callback
252 * function must not bind or unbind a function. Doing so will cause a deadlock
253 * @param callback The function to call
254 * @return A handle used for removing the function from the registered list
255 */
256 CallbackHandle<Room::BanList> BindOnBanListReceived(
257 std::function<void(const Room::BanList&)> callback);
258
259 /**
260 * Leaves the current room.
261 */
262 void Leave();
263
264private:
265 class RoomMemberImpl;
266 std::unique_ptr<RoomMemberImpl> room_member_impl;
267};
268
269inline const char* GetStateStr(const RoomMember::State& s) {
270 switch (s) {
271 case RoomMember::State::Uninitialized:
272 return "Uninitialized";
273 case RoomMember::State::Idle:
274 return "Idle";
275 case RoomMember::State::Joining:
276 return "Joining";
277 case RoomMember::State::Joined:
278 return "Joined";
279 case RoomMember::State::Moderator:
280 return "Moderator";
281 }
282 return "Unknown";
283}
284
285inline const char* GetErrorStr(const RoomMember::Error& e) {
286 switch (e) {
287 case RoomMember::Error::LostConnection:
288 return "LostConnection";
289 case RoomMember::Error::HostKicked:
290 return "HostKicked";
291 case RoomMember::Error::UnknownError:
292 return "UnknownError";
293 case RoomMember::Error::NameCollision:
294 return "NameCollision";
295 case RoomMember::Error::MacCollision:
296 return "MaxCollision";
297 case RoomMember::Error::ConsoleIdCollision:
298 return "ConsoleIdCollision";
299 case RoomMember::Error::WrongVersion:
300 return "WrongVersion";
301 case RoomMember::Error::WrongPassword:
302 return "WrongPassword";
303 case RoomMember::Error::CouldNotConnect:
304 return "CouldNotConnect";
305 case RoomMember::Error::RoomIsFull:
306 return "RoomIsFull";
307 case RoomMember::Error::HostBanned:
308 return "HostBanned";
309 case RoomMember::Error::PermissionDenied:
310 return "PermissionDenied";
311 case RoomMember::Error::NoSuchUser:
312 return "NoSuchUser";
313 default:
314 return "Unknown";
315 }
316}
317
318} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "network/verify_user.h"
5
6namespace Network::VerifyUser {
7
8Backend::~Backend() = default;
9
10NullBackend::~NullBackend() = default;
11
12UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_uid,
13 [[maybe_unused]] const std::string& token) {
14 return {};
15}
16
17} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <string>
7#include "common/logging/log.h"
8
9namespace Network::VerifyUser {
10
11struct UserData {
12 std::string username;
13 std::string display_name;
14 std::string avatar_url;
15 bool moderator = false; ///< Whether the user is a yuzu Moderator.
16};
17
18/**
19 * A backend used for verifying users and loading user data.
20 */
21class Backend {
22public:
23 virtual ~Backend();
24
25 /**
26 * Verifies the given token and loads the information into a UserData struct.
27 * @param verify_uid A GUID that may be used for verification.
28 * @param token A token that contains user data and verification data. The format and content is
29 * decided by backends.
30 */
31 virtual UserData LoadUserData(const std::string& verify_uid, const std::string& token) = 0;
32};
33
34/**
35 * A null backend where the token is ignored.
36 * No verification is performed here and the function returns an empty UserData.
37 */
38class NullBackend final : public Backend {
39public:
40 ~NullBackend();
41
42 UserData LoadUserData(const std::string& verify_uid, const std::string& token) override;
43};
44
45} // namespace Network::VerifyUser
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index a69ccb264..fbbcf673a 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -7,7 +7,7 @@ add_executable(tests
7 common/ring_buffer.cpp 7 common/ring_buffer.cpp
8 common/unique_function.cpp 8 common/unique_function.cpp
9 core/core_timing.cpp 9 core/core_timing.cpp
10 core/network/network.cpp 10 core/internal_network/network.cpp
11 tests.cpp 11 tests.cpp
12 video_core/buffer_base.cpp 12 video_core/buffer_base.cpp
13 input_common/calibration_configuration_job.cpp 13 input_common/calibration_configuration_job.cpp
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 @@
3 3
4#include <catch2/catch.hpp> 4#include <catch2/catch.hpp>
5 5
6#include "core/network/network.h" 6#include "core/internal_network/network.h"
7#include "core/network/sockets.h" 7#include "core/internal_network/sockets.h"
8 8
9TEST_CASE("Network::Errors", "[core]") { 9TEST_CASE("Network::Errors", "[core]") {
10 Network::NetworkInstance network_instance; // initialize network 10 Network::NetworkInstance network_instance; // initialize network
diff --git a/src/web_service/CMakeLists.txt b/src/web_service/CMakeLists.txt
index ae85a72ea..753fb6e7a 100644
--- a/src/web_service/CMakeLists.txt
+++ b/src/web_service/CMakeLists.txt
@@ -1,12 +1,16 @@
1add_library(web_service STATIC 1add_library(web_service STATIC
2 announce_room_json.cpp
3 announce_room_json.h
2 telemetry_json.cpp 4 telemetry_json.cpp
3 telemetry_json.h 5 telemetry_json.h
4 verify_login.cpp 6 verify_login.cpp
5 verify_login.h 7 verify_login.h
8 verify_user_jwt.cpp
9 verify_user_jwt.h
6 web_backend.cpp 10 web_backend.cpp
7 web_backend.h 11 web_backend.h
8 web_result.h 12 web_result.h
9) 13)
10 14
11create_target_directory_groups(web_service) 15create_target_directory_groups(web_service)
12target_link_libraries(web_service PRIVATE common nlohmann_json::nlohmann_json httplib) 16target_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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <future>
5#include <nlohmann/json.hpp>
6#include "common/detached_tasks.h"
7#include "common/logging/log.h"
8#include "web_service/announce_room_json.h"
9#include "web_service/web_backend.h"
10
11namespace AnnounceMultiplayerRoom {
12
13static void to_json(nlohmann::json& json, const Member& member) {
14 if (!member.username.empty()) {
15 json["username"] = member.username;
16 }
17 json["nickname"] = member.nickname;
18 if (!member.avatar_url.empty()) {
19 json["avatarUrl"] = member.avatar_url;
20 }
21 json["gameName"] = member.game.name;
22 json["gameId"] = member.game.id;
23}
24
25static void from_json(const nlohmann::json& json, Member& member) {
26 member.nickname = json.at("nickname").get<std::string>();
27 member.game.name = json.at("gameName").get<std::string>();
28 member.game.id = json.at("gameId").get<u64>();
29 try {
30 member.username = json.at("username").get<std::string>();
31 member.avatar_url = json.at("avatarUrl").get<std::string>();
32 } catch (const nlohmann::detail::out_of_range&) {
33 member.username = member.avatar_url = "";
34 LOG_DEBUG(Network, "Member \'{}\' isn't authenticated", member.nickname);
35 }
36}
37
38static void to_json(nlohmann::json& json, const Room& room) {
39 json["port"] = room.information.port;
40 json["name"] = room.information.name;
41 if (!room.information.description.empty()) {
42 json["description"] = room.information.description;
43 }
44 json["preferredGameName"] = room.information.preferred_game.name;
45 json["preferredGameId"] = room.information.preferred_game.id;
46 json["maxPlayers"] = room.information.member_slots;
47 json["netVersion"] = room.net_version;
48 json["hasPassword"] = room.has_password;
49 if (room.members.size() > 0) {
50 nlohmann::json member_json = room.members;
51 json["players"] = member_json;
52 }
53}
54
55static void from_json(const nlohmann::json& json, Room& room) {
56 room.verify_uid = json.at("externalGuid").get<std::string>();
57 room.ip = json.at("address").get<std::string>();
58 room.information.name = json.at("name").get<std::string>();
59 try {
60 room.information.description = json.at("description").get<std::string>();
61 } catch (const nlohmann::detail::out_of_range&) {
62 room.information.description = "";
63 LOG_DEBUG(Network, "Room \'{}\' doesn't contain a description", room.information.name);
64 }
65 room.information.host_username = json.at("owner").get<std::string>();
66 room.information.port = json.at("port").get<u16>();
67 room.information.preferred_game.name = json.at("preferredGameName").get<std::string>();
68 room.information.preferred_game.id = json.at("preferredGameId").get<u64>();
69 room.information.member_slots = json.at("maxPlayers").get<u32>();
70 room.net_version = json.at("netVersion").get<u32>();
71 room.has_password = json.at("hasPassword").get<bool>();
72 try {
73 room.members = json.at("players").get<std::vector<Member>>();
74 } catch (const nlohmann::detail::out_of_range& e) {
75 LOG_DEBUG(Network, "Out of range {}", e.what());
76 }
77}
78
79} // namespace AnnounceMultiplayerRoom
80
81namespace WebService {
82
83void RoomJson::SetRoomInformation(const std::string& name, const std::string& description,
84 const u16 port, const u32 max_player, const u32 net_version,
85 const bool has_password,
86 const AnnounceMultiplayerRoom::GameInfo& preferred_game) {
87 room.information.name = name;
88 room.information.description = description;
89 room.information.port = port;
90 room.information.member_slots = max_player;
91 room.net_version = net_version;
92 room.has_password = has_password;
93 room.information.preferred_game = preferred_game;
94}
95void RoomJson::AddPlayer(const AnnounceMultiplayerRoom::Member& member) {
96 room.members.push_back(member);
97}
98
99WebService::WebResult RoomJson::Update() {
100 if (room_id.empty()) {
101 LOG_ERROR(WebService, "Room must be registered to be updated");
102 return WebService::WebResult{WebService::WebResult::Code::LibError,
103 "Room is not registered", ""};
104 }
105 nlohmann::json json{{"players", room.members}};
106 return client.PostJson(fmt::format("/lobby/{}", room_id), json.dump(), false);
107}
108
109WebService::WebResult RoomJson::Register() {
110 nlohmann::json json = room;
111 auto result = client.PostJson("/lobby", json.dump(), false);
112 if (result.result_code != WebService::WebResult::Code::Success) {
113 return result;
114 }
115 auto reply_json = nlohmann::json::parse(result.returned_data);
116 room = reply_json.get<AnnounceMultiplayerRoom::Room>();
117 room_id = reply_json.at("id").get<std::string>();
118 return WebService::WebResult{WebService::WebResult::Code::Success, "", room.verify_uid};
119}
120
121void RoomJson::ClearPlayers() {
122 room.members.clear();
123}
124
125AnnounceMultiplayerRoom::RoomList RoomJson::GetRoomList() {
126 auto reply = client.GetJson("/lobby", true).returned_data;
127 if (reply.empty()) {
128 return {};
129 }
130 return nlohmann::json::parse(reply).at("rooms").get<AnnounceMultiplayerRoom::RoomList>();
131}
132
133void RoomJson::Delete() {
134 if (room_id.empty()) {
135 LOG_ERROR(WebService, "Room must be registered to be deleted");
136 return;
137 }
138 Common::DetachedTasks::AddTask(
139 [host{this->host}, username{this->username}, token{this->token}, room_id{this->room_id}]() {
140 // create a new client here because the this->client might be destroyed.
141 Client{host, username, token}.DeleteJson(fmt::format("/lobby/{}", room_id), "", false);
142 });
143}
144
145} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <functional>
7#include <string>
8#include "common/announce_multiplayer_room.h"
9#include "web_service/web_backend.h"
10
11namespace WebService {
12
13/**
14 * Implementation of AnnounceMultiplayerRoom::Backend that (de)serializes room information into/from
15 * JSON, and submits/gets it to/from the yuzu web service
16 */
17class RoomJson : public AnnounceMultiplayerRoom::Backend {
18public:
19 RoomJson(const std::string& host_, const std::string& username_, const std::string& token_)
20 : client(host_, username_, token_), host(host_), username(username_), token(token_) {}
21 ~RoomJson() = default;
22 void SetRoomInformation(const std::string& name, const std::string& description, const u16 port,
23 const u32 max_player, const u32 net_version, const bool has_password,
24 const AnnounceMultiplayerRoom::GameInfo& preferred_game) override;
25 void AddPlayer(const AnnounceMultiplayerRoom::Member& member) override;
26 WebResult Update() override;
27 WebResult Register() override;
28 void ClearPlayers() override;
29 AnnounceMultiplayerRoom::RoomList GetRoomList() override;
30 void Delete() override;
31
32private:
33 AnnounceMultiplayerRoom::Room room;
34 Client client;
35 std::string host;
36 std::string username;
37 std::string token;
38 std::string room_id;
39};
40
41} // namespace WebService
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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#if defined(__GNUC__) || defined(__clang__)
5#pragma GCC diagnostic push
6#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
7#endif
8#include <jwt/jwt.hpp>
9#if defined(__GNUC__) || defined(__clang__)
10#pragma GCC diagnostic pop
11#endif
12
13#include <system_error>
14#include "common/logging/log.h"
15#include "web_service/verify_user_jwt.h"
16#include "web_service/web_backend.h"
17#include "web_service/web_result.h"
18
19namespace WebService {
20
21static std::string public_key;
22std::string GetPublicKey(const std::string& host) {
23 if (public_key.empty()) {
24 Client client(host, "", ""); // no need for credentials here
25 public_key = client.GetPlain("/jwt/external/key.pem", true).returned_data;
26 if (public_key.empty()) {
27 LOG_ERROR(WebService, "Could not fetch external JWT public key, verification may fail");
28 } else {
29 LOG_INFO(WebService, "Fetched external JWT public key (size={})", public_key.size());
30 }
31 }
32 return public_key;
33}
34
35VerifyUserJWT::VerifyUserJWT(const std::string& host) : pub_key(GetPublicKey(host)) {}
36
37Network::VerifyUser::UserData VerifyUserJWT::LoadUserData(const std::string& verify_uid,
38 const std::string& token) {
39 const std::string audience = fmt::format("external-{}", verify_uid);
40 using namespace jwt::params;
41 std::error_code error;
42 auto decoded =
43 jwt::decode(token, algorithms({"rs256"}), error, secret(pub_key), issuer("yuzu-core"),
44 aud(audience), validate_iat(true), validate_jti(true));
45 if (error) {
46 LOG_INFO(WebService, "Verification failed: category={}, code={}, message={}",
47 error.category().name(), error.value(), error.message());
48 return {};
49 }
50 Network::VerifyUser::UserData user_data{};
51 if (decoded.payload().has_claim("username")) {
52 user_data.username = decoded.payload().get_claim_value<std::string>("username");
53 }
54 if (decoded.payload().has_claim("displayName")) {
55 user_data.display_name = decoded.payload().get_claim_value<std::string>("displayName");
56 }
57 if (decoded.payload().has_claim("avatarUrl")) {
58 user_data.avatar_url = decoded.payload().get_claim_value<std::string>("avatarUrl");
59 }
60 if (decoded.payload().has_claim("roles")) {
61 auto roles = decoded.payload().get_claim_value<std::vector<std::string>>("roles");
62 user_data.moderator = std::find(roles.begin(), roles.end(), "moderator") != roles.end();
63 }
64 return user_data;
65}
66
67} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <fmt/format.h>
7#include "network/verify_user.h"
8#include "web_service/web_backend.h"
9
10namespace WebService {
11
12std::string GetPublicKey(const std::string& host);
13
14class VerifyUserJWT final : public Network::VerifyUser::Backend {
15public:
16 VerifyUserJWT(const std::string& host);
17 ~VerifyUserJWT() = default;
18
19 Network::VerifyUser::UserData LoadUserData(const std::string& verify_uid,
20 const std::string& token) override;
21
22private:
23 std::string pub_key;
24};
25
26} // namespace WebService
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 57e0e7025..66873143e 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -156,10 +156,36 @@ add_executable(yuzu
156 main.cpp 156 main.cpp
157 main.h 157 main.h
158 main.ui 158 main.ui
159 multiplayer/chat_room.cpp
160 multiplayer/chat_room.h
161 multiplayer/chat_room.ui
162 multiplayer/client_room.h
163 multiplayer/client_room.cpp
164 multiplayer/client_room.ui
165 multiplayer/direct_connect.cpp
166 multiplayer/direct_connect.h
167 multiplayer/direct_connect.ui
168 multiplayer/host_room.cpp
169 multiplayer/host_room.h
170 multiplayer/host_room.ui
171 multiplayer/lobby.cpp
172 multiplayer/lobby.h
173 multiplayer/lobby.ui
174 multiplayer/lobby_p.h
175 multiplayer/message.cpp
176 multiplayer/message.h
177 multiplayer/moderation_dialog.cpp
178 multiplayer/moderation_dialog.h
179 multiplayer/moderation_dialog.ui
180 multiplayer/state.cpp
181 multiplayer/state.h
182 multiplayer/validation.h
159 startup_checks.cpp 183 startup_checks.cpp
160 startup_checks.h 184 startup_checks.h
161 uisettings.cpp 185 uisettings.cpp
162 uisettings.h 186 uisettings.h
187 util/clickable_label.cpp
188 util/clickable_label.h
163 util/controller_navigation.cpp 189 util/controller_navigation.cpp
164 util/controller_navigation.h 190 util/controller_navigation.h
165 util/limitable_input_dialog.cpp 191 util/limitable_input_dialog.cpp
@@ -256,7 +282,7 @@ endif()
256 282
257create_target_directory_groups(yuzu) 283create_target_directory_groups(yuzu)
258 284
259target_link_libraries(yuzu PRIVATE common core input_common video_core) 285target_link_libraries(yuzu PRIVATE common core input_common network video_core)
260target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia) 286target_link_libraries(yuzu PRIVATE Boost::boost glad Qt::Widgets Qt::Multimedia)
261target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) 287target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
262 288
@@ -300,6 +326,10 @@ if (USE_DISCORD_PRESENCE)
300 target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE) 326 target_compile_definitions(yuzu PRIVATE -DUSE_DISCORD_PRESENCE)
301endif() 327endif()
302 328
329if (ENABLE_WEB_SERVICE)
330 target_compile_definitions(yuzu PRIVATE -DENABLE_WEB_SERVICE)
331endif()
332
303if (YUZU_USE_QT_WEB_ENGINE) 333if (YUZU_USE_QT_WEB_ENGINE)
304 target_link_libraries(yuzu PRIVATE Qt::WebEngineCore Qt::WebEngineWidgets) 334 target_link_libraries(yuzu PRIVATE Qt::WebEngineCore Qt::WebEngineWidgets)
305 target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) 335 target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE)
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index c841843f0..3b22102a8 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -11,6 +11,7 @@
11#include "core/hle/service/acc/profile_manager.h" 11#include "core/hle/service/acc/profile_manager.h"
12#include "core/hle/service/hid/controllers/npad.h" 12#include "core/hle/service/hid/controllers/npad.h"
13#include "input_common/main.h" 13#include "input_common/main.h"
14#include "network/network.h"
14#include "yuzu/configuration/config.h" 15#include "yuzu/configuration/config.h"
15 16
16namespace FS = Common::FS; 17namespace FS = Common::FS;
@@ -794,6 +795,7 @@ void Config::ReadUIValues() {
794 ReadPathValues(); 795 ReadPathValues();
795 ReadScreenshotValues(); 796 ReadScreenshotValues();
796 ReadShortcutValues(); 797 ReadShortcutValues();
798 ReadMultiplayerValues();
797 799
798 ReadBasicSetting(UISettings::values.single_window_mode); 800 ReadBasicSetting(UISettings::values.single_window_mode);
799 ReadBasicSetting(UISettings::values.fullscreen); 801 ReadBasicSetting(UISettings::values.fullscreen);
@@ -860,6 +862,42 @@ void Config::ReadWebServiceValues() {
860 qt_config->endGroup(); 862 qt_config->endGroup();
861} 863}
862 864
865void Config::ReadMultiplayerValues() {
866 qt_config->beginGroup(QStringLiteral("Multiplayer"));
867
868 ReadBasicSetting(UISettings::values.multiplayer_nickname);
869 ReadBasicSetting(UISettings::values.multiplayer_ip);
870 ReadBasicSetting(UISettings::values.multiplayer_port);
871 ReadBasicSetting(UISettings::values.multiplayer_room_nickname);
872 ReadBasicSetting(UISettings::values.multiplayer_room_name);
873 ReadBasicSetting(UISettings::values.multiplayer_room_port);
874 ReadBasicSetting(UISettings::values.multiplayer_host_type);
875 ReadBasicSetting(UISettings::values.multiplayer_port);
876 ReadBasicSetting(UISettings::values.multiplayer_max_player);
877 ReadBasicSetting(UISettings::values.multiplayer_game_id);
878 ReadBasicSetting(UISettings::values.multiplayer_room_description);
879
880 // Read ban list back
881 int size = qt_config->beginReadArray(QStringLiteral("username_ban_list"));
882 UISettings::values.multiplayer_ban_list.first.resize(size);
883 for (int i = 0; i < size; ++i) {
884 qt_config->setArrayIndex(i);
885 UISettings::values.multiplayer_ban_list.first[i] =
886 ReadSetting(QStringLiteral("username")).toString().toStdString();
887 }
888 qt_config->endArray();
889 size = qt_config->beginReadArray(QStringLiteral("ip_ban_list"));
890 UISettings::values.multiplayer_ban_list.second.resize(size);
891 for (int i = 0; i < size; ++i) {
892 qt_config->setArrayIndex(i);
893 UISettings::values.multiplayer_ban_list.second[i] =
894 ReadSetting(QStringLiteral("ip")).toString().toStdString();
895 }
896 qt_config->endArray();
897
898 qt_config->endGroup();
899}
900
863void Config::ReadValues() { 901void Config::ReadValues() {
864 if (global) { 902 if (global) {
865 ReadControlValues(); 903 ReadControlValues();
@@ -876,6 +914,7 @@ void Config::ReadValues() {
876 ReadRendererValues(); 914 ReadRendererValues();
877 ReadAudioValues(); 915 ReadAudioValues();
878 ReadSystemValues(); 916 ReadSystemValues();
917 ReadMultiplayerValues();
879} 918}
880 919
881void Config::SavePlayerValue(std::size_t player_index) { 920void Config::SavePlayerValue(std::size_t player_index) {
@@ -1025,6 +1064,7 @@ void Config::SaveValues() {
1025 SaveRendererValues(); 1064 SaveRendererValues();
1026 SaveAudioValues(); 1065 SaveAudioValues();
1027 SaveSystemValues(); 1066 SaveSystemValues();
1067 SaveMultiplayerValues();
1028} 1068}
1029 1069
1030void Config::SaveAudioValues() { 1070void Config::SaveAudioValues() {
@@ -1347,6 +1387,7 @@ void Config::SaveUIValues() {
1347 SavePathValues(); 1387 SavePathValues();
1348 SaveScreenshotValues(); 1388 SaveScreenshotValues();
1349 SaveShortcutValues(); 1389 SaveShortcutValues();
1390 SaveMultiplayerValues();
1350 1391
1351 WriteBasicSetting(UISettings::values.single_window_mode); 1392 WriteBasicSetting(UISettings::values.single_window_mode);
1352 WriteBasicSetting(UISettings::values.fullscreen); 1393 WriteBasicSetting(UISettings::values.fullscreen);
@@ -1411,6 +1452,40 @@ void Config::SaveWebServiceValues() {
1411 qt_config->endGroup(); 1452 qt_config->endGroup();
1412} 1453}
1413 1454
1455void Config::SaveMultiplayerValues() {
1456 qt_config->beginGroup(QStringLiteral("Multiplayer"));
1457
1458 WriteBasicSetting(UISettings::values.multiplayer_nickname);
1459 WriteBasicSetting(UISettings::values.multiplayer_ip);
1460 WriteBasicSetting(UISettings::values.multiplayer_port);
1461 WriteBasicSetting(UISettings::values.multiplayer_room_nickname);
1462 WriteBasicSetting(UISettings::values.multiplayer_room_name);
1463 WriteBasicSetting(UISettings::values.multiplayer_room_port);
1464 WriteBasicSetting(UISettings::values.multiplayer_host_type);
1465 WriteBasicSetting(UISettings::values.multiplayer_port);
1466 WriteBasicSetting(UISettings::values.multiplayer_max_player);
1467 WriteBasicSetting(UISettings::values.multiplayer_game_id);
1468 WriteBasicSetting(UISettings::values.multiplayer_room_description);
1469
1470 // Write ban list
1471 qt_config->beginWriteArray(QStringLiteral("username_ban_list"));
1472 for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.first.size(); ++i) {
1473 qt_config->setArrayIndex(static_cast<int>(i));
1474 WriteSetting(QStringLiteral("username"),
1475 QString::fromStdString(UISettings::values.multiplayer_ban_list.first[i]));
1476 }
1477 qt_config->endArray();
1478 qt_config->beginWriteArray(QStringLiteral("ip_ban_list"));
1479 for (std::size_t i = 0; i < UISettings::values.multiplayer_ban_list.second.size(); ++i) {
1480 qt_config->setArrayIndex(static_cast<int>(i));
1481 WriteSetting(QStringLiteral("ip"),
1482 QString::fromStdString(UISettings::values.multiplayer_ban_list.second[i]));
1483 }
1484 qt_config->endArray();
1485
1486 qt_config->endGroup();
1487}
1488
1414QVariant Config::ReadSetting(const QString& name) const { 1489QVariant Config::ReadSetting(const QString& name) const {
1415 return qt_config->value(name); 1490 return qt_config->value(name);
1416} 1491}
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index a71eabe8e..937b2d95b 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -89,6 +89,7 @@ private:
89 void ReadUIGamelistValues(); 89 void ReadUIGamelistValues();
90 void ReadUILayoutValues(); 90 void ReadUILayoutValues();
91 void ReadWebServiceValues(); 91 void ReadWebServiceValues();
92 void ReadMultiplayerValues();
92 93
93 void SaveValues(); 94 void SaveValues();
94 void SavePlayerValue(std::size_t player_index); 95 void SavePlayerValue(std::size_t player_index);
@@ -118,6 +119,7 @@ private:
118 void SaveUIGamelistValues(); 119 void SaveUIGamelistValues();
119 void SaveUILayoutValues(); 120 void SaveUILayoutValues();
120 void SaveWebServiceValues(); 121 void SaveWebServiceValues();
122 void SaveMultiplayerValues();
121 123
122 /** 124 /**
123 * Reads a setting from the qt_config. 125 * Reads a setting from the qt_config.
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index e99657bd6..92ef4467b 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -29,9 +29,10 @@
29 29
30ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, 30ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
31 InputCommon::InputSubsystem* input_subsystem, 31 InputCommon::InputSubsystem* input_subsystem,
32 Core::System& system_) 32 Core::System& system_, bool enable_web_config)
33 : QDialog(parent), ui{std::make_unique<Ui::ConfigureDialog>()}, registry{registry_}, 33 : QDialog(parent), ui{std::make_unique<Ui::ConfigureDialog>()},
34 system{system_}, audio_tab{std::make_unique<ConfigureAudio>(system_, this)}, 34 registry(registry_), system{system_}, audio_tab{std::make_unique<ConfigureAudio>(system_,
35 this)},
35 cpu_tab{std::make_unique<ConfigureCpu>(system_, this)}, 36 cpu_tab{std::make_unique<ConfigureCpu>(system_, this)},
36 debug_tab_tab{std::make_unique<ConfigureDebugTab>(system_, this)}, 37 debug_tab_tab{std::make_unique<ConfigureDebugTab>(system_, this)},
37 filesystem_tab{std::make_unique<ConfigureFilesystem>(this)}, 38 filesystem_tab{std::make_unique<ConfigureFilesystem>(this)},
@@ -64,6 +65,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
64 ui->tabWidget->addTab(ui_tab.get(), tr("Game List")); 65 ui->tabWidget->addTab(ui_tab.get(), tr("Game List"));
65 ui->tabWidget->addTab(web_tab.get(), tr("Web")); 66 ui->tabWidget->addTab(web_tab.get(), tr("Web"));
66 67
68 web_tab->SetWebServiceConfigEnabled(enable_web_config);
67 hotkeys_tab->Populate(registry); 69 hotkeys_tab->Populate(registry);
68 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); 70 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
69 71
diff --git a/src/yuzu/configuration/configure_dialog.h b/src/yuzu/configuration/configure_dialog.h
index 12cf25daf..cec1610ad 100644
--- a/src/yuzu/configuration/configure_dialog.h
+++ b/src/yuzu/configuration/configure_dialog.h
@@ -41,7 +41,8 @@ class ConfigureDialog : public QDialog {
41 41
42public: 42public:
43 explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, 43 explicit ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
44 InputCommon::InputSubsystem* input_subsystem, Core::System& system_); 44 InputCommon::InputSubsystem* input_subsystem, Core::System& system_,
45 bool enable_web_config = true);
45 ~ConfigureDialog() override; 46 ~ConfigureDialog() override;
46 47
47 void ApplyConfiguration(); 48 void ApplyConfiguration();
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 @@
4#include <QtConcurrent/QtConcurrent> 4#include <QtConcurrent/QtConcurrent>
5#include "common/settings.h" 5#include "common/settings.h"
6#include "core/core.h" 6#include "core/core.h"
7#include "core/network/network_interface.h" 7#include "core/internal_network/network_interface.h"
8#include "ui_configure_network.h" 8#include "ui_configure_network.h"
9#include "yuzu/configuration/configure_network.h" 9#include "yuzu/configuration/configure_network.h"
10 10
diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp
index d779251b4..ff4bf44f4 100644
--- a/src/yuzu/configuration/configure_web.cpp
+++ b/src/yuzu/configuration/configure_web.cpp
@@ -169,3 +169,8 @@ void ConfigureWeb::OnLoginVerified() {
169 "correctly, and that your internet connection is working.")); 169 "correctly, and that your internet connection is working."));
170 } 170 }
171} 171}
172
173void ConfigureWeb::SetWebServiceConfigEnabled(bool enabled) {
174 ui->label_disable_info->setVisible(!enabled);
175 ui->groupBoxWebConfig->setEnabled(enabled);
176}
diff --git a/src/yuzu/configuration/configure_web.h b/src/yuzu/configuration/configure_web.h
index 9054711ea..041b51149 100644
--- a/src/yuzu/configuration/configure_web.h
+++ b/src/yuzu/configuration/configure_web.h
@@ -20,6 +20,7 @@ public:
20 ~ConfigureWeb() override; 20 ~ConfigureWeb() override;
21 21
22 void ApplyConfiguration(); 22 void ApplyConfiguration();
23 void SetWebServiceConfigEnabled(bool enabled);
23 24
24private: 25private:
25 void changeEvent(QEvent* event) override; 26 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 @@
113 </widget> 113 </widget>
114 </item> 114 </item>
115 <item> 115 <item>
116 <widget class="QLabel" name="label_disable_info">
117 <property name="text">
118 <string>Web Service configuration can only be changed when a public room isn't being hosted.</string>
119 </property>
120 <property name="wordWrap">
121 <bool>true</bool>
122 </property>
123 </widget>
124 </item>
125 <item>
116 <widget class="QGroupBox" name="groupBox"> 126 <widget class="QGroupBox" name="groupBox">
117 <property name="title"> 127 <property name="title">
118 <string>Telemetry</string> 128 <string>Telemetry</string>
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp
index 05d309827..5bcf582bf 100644
--- a/src/yuzu/game_list.cpp
+++ b/src/yuzu/game_list.cpp
@@ -499,6 +499,8 @@ void GameList::DonePopulating(const QStringList& watch_list) {
499 } 499 }
500 item_model->sort(tree_view->header()->sortIndicatorSection(), 500 item_model->sort(tree_view->header()->sortIndicatorSection(),
501 tree_view->header()->sortIndicatorOrder()); 501 tree_view->header()->sortIndicatorOrder());
502
503 emit PopulatingCompleted();
502} 504}
503 505
504void GameList::PopupContextMenu(const QPoint& menu_location) { 506void GameList::PopupContextMenu(const QPoint& menu_location) {
@@ -752,6 +754,10 @@ void GameList::LoadCompatibilityList() {
752 } 754 }
753} 755}
754 756
757QStandardItemModel* GameList::GetModel() const {
758 return item_model;
759}
760
755void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { 761void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
756 tree_view->setEnabled(false); 762 tree_view->setEnabled(false);
757 763
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h
index bc36d015a..9605985cc 100644
--- a/src/yuzu/game_list.h
+++ b/src/yuzu/game_list.h
@@ -16,9 +16,14 @@
16#include <QWidget> 16#include <QWidget>
17 17
18#include "common/common_types.h" 18#include "common/common_types.h"
19#include "core/core.h"
19#include "uisettings.h" 20#include "uisettings.h"
20#include "yuzu/compatibility_list.h" 21#include "yuzu/compatibility_list.h"
21 22
23namespace Core {
24class System;
25}
26
22class ControllerNavigation; 27class ControllerNavigation;
23class GameListWorker; 28class GameListWorker;
24class GameListSearchField; 29class GameListSearchField;
@@ -84,6 +89,8 @@ public:
84 void SaveInterfaceLayout(); 89 void SaveInterfaceLayout();
85 void LoadInterfaceLayout(); 90 void LoadInterfaceLayout();
86 91
92 QStandardItemModel* GetModel() const;
93
87 /// Disables events from the emulated controller 94 /// Disables events from the emulated controller
88 void UnloadController(); 95 void UnloadController();
89 96
@@ -108,6 +115,7 @@ signals:
108 void OpenDirectory(const QString& directory); 115 void OpenDirectory(const QString& directory);
109 void AddDirectory(); 116 void AddDirectory();
110 void ShowList(bool show); 117 void ShowList(bool show);
118 void PopulatingCompleted();
111 119
112private slots: 120private slots:
113 void OnItemExpanded(const QModelIndex& item); 121 void OnItemExpanded(const QModelIndex& item);
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 2814548eb..e56fcabff 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -32,6 +32,7 @@
32#include "core/hle/service/am/applet_ae.h" 32#include "core/hle/service/am/applet_ae.h"
33#include "core/hle/service/am/applet_oe.h" 33#include "core/hle/service/am/applet_oe.h"
34#include "core/hle/service/am/applets/applets.h" 34#include "core/hle/service/am/applets/applets.h"
35#include "yuzu/multiplayer/state.h"
35#include "yuzu/util/controller_navigation.h" 36#include "yuzu/util/controller_navigation.h"
36 37
37// These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows 38// These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows
@@ -132,6 +133,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
132#include "yuzu/main.h" 133#include "yuzu/main.h"
133#include "yuzu/startup_checks.h" 134#include "yuzu/startup_checks.h"
134#include "yuzu/uisettings.h" 135#include "yuzu/uisettings.h"
136#include "yuzu/util/clickable_label.h"
135 137
136using namespace Common::Literals; 138using namespace Common::Literals;
137 139
@@ -271,6 +273,8 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
271 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); 273 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
272 discord_rpc->Update(); 274 discord_rpc->Update();
273 275
276 system->GetRoomNetwork().Init();
277
274 RegisterMetaTypes(); 278 RegisterMetaTypes();
275 279
276 InitializeWidgets(); 280 InitializeWidgets();
@@ -459,6 +463,7 @@ GMainWindow::~GMainWindow() {
459 if (render_window->parent() == nullptr) { 463 if (render_window->parent() == nullptr) {
460 delete render_window; 464 delete render_window;
461 } 465 }
466 system->GetRoomNetwork().Shutdown();
462} 467}
463 468
464void GMainWindow::RegisterMetaTypes() { 469void GMainWindow::RegisterMetaTypes() {
@@ -822,6 +827,10 @@ void GMainWindow::InitializeWidgets() {
822 } 827 }
823 }); 828 });
824 829
830 multiplayer_state = new MultiplayerState(this, game_list->GetModel(), ui->action_Leave_Room,
831 ui->action_Show_Room, system->GetRoomNetwork());
832 multiplayer_state->setVisible(false);
833
825 // Create status bar 834 // Create status bar
826 message_label = new QLabel(); 835 message_label = new QLabel();
827 // Configured separately for left alignment 836 // Configured separately for left alignment
@@ -854,6 +863,10 @@ void GMainWindow::InitializeWidgets() {
854 statusBar()->addPermanentWidget(label); 863 statusBar()->addPermanentWidget(label);
855 } 864 }
856 865
866 // TODO (flTobi): Add the widget when multiplayer is fully implemented
867 // statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
868 // statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
869
857 tas_label = new QLabel(); 870 tas_label = new QLabel();
858 tas_label->setObjectName(QStringLiteral("TASlabel")); 871 tas_label->setObjectName(QStringLiteral("TASlabel"));
859 tas_label->setFocusPolicy(Qt::NoFocus); 872 tas_label->setFocusPolicy(Qt::NoFocus);
@@ -1163,6 +1176,8 @@ void GMainWindow::ConnectWidgetEvents() {
1163 connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, 1176 connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this,
1164 &GMainWindow::OnGameListAddDirectory); 1177 &GMainWindow::OnGameListAddDirectory);
1165 connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); 1178 connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList);
1179 connect(game_list, &GameList::PopulatingCompleted,
1180 [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); });
1166 1181
1167 connect(game_list, &GameList::OpenPerGameGeneralRequested, this, 1182 connect(game_list, &GameList::OpenPerGameGeneralRequested, this,
1168 &GMainWindow::OnGameListOpenPerGameProperties); 1183 &GMainWindow::OnGameListOpenPerGameProperties);
@@ -1180,6 +1195,9 @@ void GMainWindow::ConnectWidgetEvents() {
1180 connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit); 1195 connect(this, &GMainWindow::EmulationStopping, this, &GMainWindow::SoftwareKeyboardExit);
1181 1196
1182 connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); 1197 connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar);
1198
1199 connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state,
1200 &MultiplayerState::UpdateThemedIcons);
1183} 1201}
1184 1202
1185void GMainWindow::ConnectMenuEvents() { 1203void GMainWindow::ConnectMenuEvents() {
@@ -1223,6 +1241,18 @@ void GMainWindow::ConnectMenuEvents() {
1223 ui->action_Reset_Window_Size_900, 1241 ui->action_Reset_Window_Size_900,
1224 ui->action_Reset_Window_Size_1080}); 1242 ui->action_Reset_Window_Size_1080});
1225 1243
1244 // Multiplayer
1245 connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state,
1246 &MultiplayerState::OnViewLobby);
1247 connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state,
1248 &MultiplayerState::OnCreateRoom);
1249 connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state,
1250 &MultiplayerState::OnCloseRoom);
1251 connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state,
1252 &MultiplayerState::OnDirectConnectToRoom);
1253 connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state,
1254 &MultiplayerState::OnOpenNetworkRoom);
1255
1226 // Tools 1256 // Tools
1227 connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this, 1257 connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this,
1228 ReinitializeKeyBehavior::Warning)); 1258 ReinitializeKeyBehavior::Warning));
@@ -2783,7 +2813,8 @@ void GMainWindow::OnConfigure() {
2783 const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); 2813 const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
2784 2814
2785 Settings::SetConfiguringGlobal(true); 2815 Settings::SetConfiguringGlobal(true);
2786 ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system); 2816 ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(), *system,
2817 !multiplayer_state->IsHostingPublicRoom());
2787 connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this, 2818 connect(&configure_dialog, &ConfigureDialog::LanguageChanged, this,
2788 &GMainWindow::OnLanguageChanged); 2819 &GMainWindow::OnLanguageChanged);
2789 2820
@@ -2840,6 +2871,11 @@ void GMainWindow::OnConfigure() {
2840 if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { 2871 if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) {
2841 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); 2872 SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
2842 } 2873 }
2874
2875 if (!multiplayer_state->IsHostingPublicRoom()) {
2876 multiplayer_state->UpdateCredentials();
2877 }
2878
2843 emit UpdateThemedIcons(); 2879 emit UpdateThemedIcons();
2844 2880
2845 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); 2881 const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false);
@@ -3660,6 +3696,7 @@ void GMainWindow::closeEvent(QCloseEvent* event) {
3660 } 3696 }
3661 3697
3662 render_window->close(); 3698 render_window->close();
3699 multiplayer_state->Close();
3663 3700
3664 QWidget::closeEvent(event); 3701 QWidget::closeEvent(event);
3665} 3702}
@@ -3856,6 +3893,7 @@ void GMainWindow::OnLanguageChanged(const QString& locale) {
3856 UISettings::values.language = locale; 3893 UISettings::values.language = locale;
3857 LoadTranslation(); 3894 LoadTranslation();
3858 ui->retranslateUi(this); 3895 ui->retranslateUi(this);
3896 multiplayer_state->retranslateUi();
3859 UpdateWindowTitle(); 3897 UpdateWindowTitle();
3860} 3898}
3861 3899
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index 27204f5a2..8d5c1398f 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -11,6 +11,7 @@
11#include <QTimer> 11#include <QTimer>
12#include <QTranslator> 12#include <QTranslator>
13 13
14#include "common/announce_multiplayer_room.h"
14#include "common/common_types.h" 15#include "common/common_types.h"
15#include "yuzu/compatibility_list.h" 16#include "yuzu/compatibility_list.h"
16#include "yuzu/hotkeys.h" 17#include "yuzu/hotkeys.h"
@@ -22,6 +23,7 @@
22#endif 23#endif
23 24
24class Config; 25class Config;
26class ClickableLabel;
25class EmuThread; 27class EmuThread;
26class GameList; 28class GameList;
27class GImageInfo; 29class GImageInfo;
@@ -31,6 +33,7 @@ class MicroProfileDialog;
31class ProfilerWidget; 33class ProfilerWidget;
32class ControllerDialog; 34class ControllerDialog;
33class QLabel; 35class QLabel;
36class MultiplayerState;
34class QPushButton; 37class QPushButton;
35class QProgressDialog; 38class QProgressDialog;
36class WaitTreeWidget; 39class WaitTreeWidget;
@@ -200,6 +203,8 @@ private:
200 void ConnectMenuEvents(); 203 void ConnectMenuEvents();
201 void UpdateMenuState(); 204 void UpdateMenuState();
202 205
206 MultiplayerState* multiplayer_state = nullptr;
207
203 void PreventOSSleep(); 208 void PreventOSSleep();
204 void AllowOSSleep(); 209 void AllowOSSleep();
205 210
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 @@
154 <addaction name="menu_Emulation"/> 154 <addaction name="menu_Emulation"/>
155 <addaction name="menu_View"/> 155 <addaction name="menu_View"/>
156 <addaction name="menu_Tools"/> 156 <addaction name="menu_Tools"/>
157 <addaction name="menu_Multiplayer"/>
157 <addaction name="menu_Help"/> 158 <addaction name="menu_Help"/>
158 </widget> 159 </widget>
159 <action name="action_Install_File_NAND"> 160 <action name="action_Install_File_NAND">
@@ -245,6 +246,43 @@
245 <string>Show Status Bar</string> 246 <string>Show Status Bar</string>
246 </property> 247 </property>
247 </action> 248 </action>
249 <action name="action_View_Lobby">
250 <property name="enabled">
251 <bool>true</bool>
252 </property>
253 <property name="text">
254 <string>Browse Public Game Lobby</string>
255 </property>
256 </action>
257 <action name="action_Start_Room">
258 <property name="enabled">
259 <bool>true</bool>
260 </property>
261 <property name="text">
262 <string>Create Room</string>
263 </property>
264 </action>
265 <action name="action_Leave_Room">
266 <property name="enabled">
267 <bool>false</bool>
268 </property>
269 <property name="text">
270 <string>Leave Room</string>
271 </property>
272 </action>
273 <action name="action_Connect_To_Room">
274 <property name="text">
275 <string>Direct Connect to Room</string>
276 </property>
277 </action>
278 <action name="action_Show_Room">
279 <property name="enabled">
280 <bool>false</bool>
281 </property>
282 <property name="text">
283 <string>Show Current Room</string>
284 </property>
285 </action>
248 <action name="action_Fullscreen"> 286 <action name="action_Fullscreen">
249 <property name="checkable"> 287 <property name="checkable">
250 <bool>true</bool> 288 <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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <array>
5#include <future>
6#include <QColor>
7#include <QDesktopServices>
8#include <QFutureWatcher>
9#include <QImage>
10#include <QList>
11#include <QLocale>
12#include <QMenu>
13#include <QMessageBox>
14#include <QMetaType>
15#include <QTime>
16#include <QUrl>
17#include <QtConcurrent/QtConcurrentRun>
18#include "common/logging/log.h"
19#include "core/announce_multiplayer_session.h"
20#include "ui_chat_room.h"
21#include "yuzu/game_list_p.h"
22#include "yuzu/multiplayer/chat_room.h"
23#include "yuzu/multiplayer/message.h"
24#ifdef ENABLE_WEB_SERVICE
25#include "web_service/web_backend.h"
26#endif
27
28class ChatMessage {
29public:
30 explicit ChatMessage(const Network::ChatEntry& chat, Network::RoomNetwork& room_network,
31 QTime ts = {}) {
32 /// Convert the time to their default locale defined format
33 QLocale locale;
34 timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
35 nickname = QString::fromStdString(chat.nickname);
36 username = QString::fromStdString(chat.username);
37 message = QString::fromStdString(chat.message);
38
39 // Check for user pings
40 QString cur_nickname, cur_username;
41 if (auto room = room_network.GetRoomMember().lock()) {
42 cur_nickname = QString::fromStdString(room->GetNickname());
43 cur_username = QString::fromStdString(room->GetUsername());
44 }
45
46 // Handle pings at the beginning and end of message
47 QString fixed_message = QStringLiteral(" %1 ").arg(message);
48 if (fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_nickname)) ||
49 (!cur_username.isEmpty() &&
50 fixed_message.contains(QStringLiteral(" @%1 ").arg(cur_username)))) {
51
52 contains_ping = true;
53 } else {
54 contains_ping = false;
55 }
56 }
57
58 bool ContainsPing() const {
59 return contains_ping;
60 }
61
62 /// Format the message using the players color
63 QString GetPlayerChatMessage(u16 player) const {
64 auto color = player_color[player % 16];
65 QString name;
66 if (username.isEmpty() || username == nickname) {
67 name = nickname;
68 } else {
69 name = QStringLiteral("%1 (%2)").arg(nickname, username);
70 }
71
72 QString style, text_color;
73 if (ContainsPing()) {
74 // Add a background color to these messages
75 style = QStringLiteral("background-color: %1").arg(QString::fromStdString(ping_color));
76 // Add a font color
77 text_color = QStringLiteral("color='#000000'");
78 }
79
80 return QStringLiteral("[%1] <font color='%2'>&lt;%3&gt;</font> <font style='%4' "
81 "%5>%6</font>")
82 .arg(timestamp, QString::fromStdString(color), name.toHtmlEscaped(), style, text_color,
83 message.toHtmlEscaped());
84 }
85
86private:
87 static constexpr std::array<const char*, 16> player_color = {
88 {"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222",
89 "#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}};
90 static constexpr char ping_color[] = "#FFFF00";
91
92 QString timestamp;
93 QString nickname;
94 QString username;
95 QString message;
96 bool contains_ping;
97};
98
99class StatusMessage {
100public:
101 explicit StatusMessage(const QString& msg, QTime ts = {}) {
102 /// Convert the time to their default locale defined format
103 QLocale locale;
104 timestamp = locale.toString(ts.isValid() ? ts : QTime::currentTime(), QLocale::ShortFormat);
105 message = msg;
106 }
107
108 QString GetSystemChatMessage() const {
109 return QStringLiteral("[%1] <font color='%2'>* %3</font>")
110 .arg(timestamp, QString::fromStdString(system_color), message);
111 }
112
113private:
114 static constexpr const char system_color[] = "#FF8C00";
115 QString timestamp;
116 QString message;
117};
118
119class PlayerListItem : public QStandardItem {
120public:
121 static const int NicknameRole = Qt::UserRole + 1;
122 static const int UsernameRole = Qt::UserRole + 2;
123 static const int AvatarUrlRole = Qt::UserRole + 3;
124 static const int GameNameRole = Qt::UserRole + 4;
125
126 PlayerListItem() = default;
127 explicit PlayerListItem(const std::string& nickname, const std::string& username,
128 const std::string& avatar_url, const std::string& game_name) {
129 setEditable(false);
130 setData(QString::fromStdString(nickname), NicknameRole);
131 setData(QString::fromStdString(username), UsernameRole);
132 setData(QString::fromStdString(avatar_url), AvatarUrlRole);
133 if (game_name.empty()) {
134 setData(QObject::tr("Not playing a game"), GameNameRole);
135 } else {
136 setData(QString::fromStdString(game_name), GameNameRole);
137 }
138 }
139
140 QVariant data(int role) const override {
141 if (role != Qt::DisplayRole) {
142 return QStandardItem::data(role);
143 }
144 QString name;
145 const QString nickname = data(NicknameRole).toString();
146 const QString username = data(UsernameRole).toString();
147 if (username.isEmpty() || username == nickname) {
148 name = nickname;
149 } else {
150 name = QStringLiteral("%1 (%2)").arg(nickname, username);
151 }
152 return QStringLiteral("%1\n %2").arg(name, data(GameNameRole).toString());
153 }
154};
155
156ChatRoom::ChatRoom(QWidget* parent) : QWidget(parent), ui(std::make_unique<Ui::ChatRoom>()) {
157 ui->setupUi(this);
158
159 // set the item_model for player_view
160
161 player_list = new QStandardItemModel(ui->player_view);
162 ui->player_view->setModel(player_list);
163 ui->player_view->setContextMenuPolicy(Qt::CustomContextMenu);
164 // set a header to make it look better though there is only one column
165 player_list->insertColumns(0, 1);
166 player_list->setHeaderData(0, Qt::Horizontal, tr("Members"));
167
168 ui->chat_history->document()->setMaximumBlockCount(max_chat_lines);
169
170 // register the network structs to use in slots and signals
171 qRegisterMetaType<Network::ChatEntry>();
172 qRegisterMetaType<Network::StatusMessageEntry>();
173 qRegisterMetaType<Network::RoomInformation>();
174 qRegisterMetaType<Network::RoomMember::State>();
175
176 // Connect all the widgets to the appropriate events
177 connect(ui->player_view, &QTreeView::customContextMenuRequested, this,
178 &ChatRoom::PopupContextMenu);
179 connect(ui->chat_message, &QLineEdit::returnPressed, this, &ChatRoom::OnSendChat);
180 connect(ui->chat_message, &QLineEdit::textChanged, this, &ChatRoom::OnChatTextChanged);
181 connect(ui->send_message, &QPushButton::clicked, this, &ChatRoom::OnSendChat);
182}
183
184ChatRoom::~ChatRoom() = default;
185
186void ChatRoom::Initialize(Network::RoomNetwork* room_network_) {
187 room_network = room_network_;
188 // setup the callbacks for network updates
189 if (auto member = room_network->GetRoomMember().lock()) {
190 member->BindOnChatMessageRecieved(
191 [this](const Network::ChatEntry& chat) { emit ChatReceived(chat); });
192 member->BindOnStatusMessageReceived(
193 [this](const Network::StatusMessageEntry& status_message) {
194 emit StatusMessageReceived(status_message);
195 });
196 connect(this, &ChatRoom::ChatReceived, this, &ChatRoom::OnChatReceive);
197 connect(this, &ChatRoom::StatusMessageReceived, this, &ChatRoom::OnStatusMessageReceive);
198 }
199}
200
201void ChatRoom::SetModPerms(bool is_mod) {
202 has_mod_perms = is_mod;
203}
204
205void ChatRoom::RetranslateUi() {
206 ui->retranslateUi(this);
207}
208
209void ChatRoom::Clear() {
210 ui->chat_history->clear();
211 block_list.clear();
212}
213
214void ChatRoom::AppendStatusMessage(const QString& msg) {
215 ui->chat_history->append(StatusMessage(msg).GetSystemChatMessage());
216}
217
218void ChatRoom::AppendChatMessage(const QString& msg) {
219 ui->chat_history->append(msg);
220}
221
222void ChatRoom::SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname) {
223 if (auto room = room_network->GetRoomMember().lock()) {
224 auto members = room->GetMemberInformation();
225 auto it = std::find_if(members.begin(), members.end(),
226 [&nickname](const Network::RoomMember::MemberInformation& member) {
227 return member.nickname == nickname;
228 });
229 if (it == members.end()) {
230 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER);
231 return;
232 }
233 room->SendModerationRequest(type, nickname);
234 }
235}
236
237bool ChatRoom::ValidateMessage(const std::string& msg) {
238 return !msg.empty();
239}
240
241void ChatRoom::OnRoomUpdate(const Network::RoomInformation& info) {
242 // TODO(B3N30): change title
243 if (auto room_member = room_network->GetRoomMember().lock()) {
244 SetPlayerList(room_member->GetMemberInformation());
245 }
246}
247
248void ChatRoom::Disable() {
249 ui->send_message->setDisabled(true);
250 ui->chat_message->setDisabled(true);
251}
252
253void ChatRoom::Enable() {
254 ui->send_message->setEnabled(true);
255 ui->chat_message->setEnabled(true);
256}
257
258void ChatRoom::OnChatReceive(const Network::ChatEntry& chat) {
259 if (!ValidateMessage(chat.message)) {
260 return;
261 }
262 if (auto room = room_network->GetRoomMember().lock()) {
263 // get the id of the player
264 auto members = room->GetMemberInformation();
265 auto it = std::find_if(members.begin(), members.end(),
266 [&chat](const Network::RoomMember::MemberInformation& member) {
267 return member.nickname == chat.nickname &&
268 member.username == chat.username;
269 });
270 if (it == members.end()) {
271 LOG_INFO(Network, "Chat message received from unknown player. Ignoring it.");
272 return;
273 }
274 if (block_list.count(chat.nickname)) {
275 LOG_INFO(Network, "Chat message received from blocked player {}. Ignoring it.",
276 chat.nickname);
277 return;
278 }
279 auto player = std::distance(members.begin(), it);
280 ChatMessage m(chat, *room_network);
281 if (m.ContainsPing()) {
282 emit UserPinged();
283 }
284 AppendChatMessage(m.GetPlayerChatMessage(player));
285 }
286}
287
288void ChatRoom::OnStatusMessageReceive(const Network::StatusMessageEntry& status_message) {
289 QString name;
290 if (status_message.username.empty() || status_message.username == status_message.nickname) {
291 name = QString::fromStdString(status_message.nickname);
292 } else {
293 name = QStringLiteral("%1 (%2)").arg(QString::fromStdString(status_message.nickname),
294 QString::fromStdString(status_message.username));
295 }
296 QString message;
297 switch (status_message.type) {
298 case Network::IdMemberJoin:
299 message = tr("%1 has joined").arg(name);
300 break;
301 case Network::IdMemberLeave:
302 message = tr("%1 has left").arg(name);
303 break;
304 case Network::IdMemberKicked:
305 message = tr("%1 has been kicked").arg(name);
306 break;
307 case Network::IdMemberBanned:
308 message = tr("%1 has been banned").arg(name);
309 break;
310 case Network::IdAddressUnbanned:
311 message = tr("%1 has been unbanned").arg(name);
312 break;
313 }
314 if (!message.isEmpty())
315 AppendStatusMessage(message);
316}
317
318void ChatRoom::OnSendChat() {
319 if (auto room = room_network->GetRoomMember().lock()) {
320 if (room->GetState() != Network::RoomMember::State::Joined &&
321 room->GetState() != Network::RoomMember::State::Moderator) {
322
323 return;
324 }
325 auto message = ui->chat_message->text().toStdString();
326 if (!ValidateMessage(message)) {
327 return;
328 }
329 auto nick = room->GetNickname();
330 auto username = room->GetUsername();
331 Network::ChatEntry chat{nick, username, message};
332
333 auto members = room->GetMemberInformation();
334 auto it = std::find_if(members.begin(), members.end(),
335 [&chat](const Network::RoomMember::MemberInformation& member) {
336 return member.nickname == chat.nickname &&
337 member.username == chat.username;
338 });
339 if (it == members.end()) {
340 LOG_INFO(Network, "Cannot find self in the player list when sending a message.");
341 }
342 auto player = std::distance(members.begin(), it);
343 ChatMessage m(chat, *room_network);
344 room->SendChatMessage(message);
345 AppendChatMessage(m.GetPlayerChatMessage(player));
346 ui->chat_message->clear();
347 }
348}
349
350void ChatRoom::UpdateIconDisplay() {
351 for (int row = 0; row < player_list->invisibleRootItem()->rowCount(); ++row) {
352 QStandardItem* item = player_list->invisibleRootItem()->child(row);
353 const std::string avatar_url =
354 item->data(PlayerListItem::AvatarUrlRole).toString().toStdString();
355 if (icon_cache.count(avatar_url)) {
356 item->setData(icon_cache.at(avatar_url), Qt::DecorationRole);
357 } else {
358 item->setData(QIcon::fromTheme(QStringLiteral("no_avatar")).pixmap(48),
359 Qt::DecorationRole);
360 }
361 }
362}
363
364void ChatRoom::SetPlayerList(const Network::RoomMember::MemberList& member_list) {
365 // TODO(B3N30): Remember which row is selected
366 player_list->removeRows(0, player_list->rowCount());
367 for (const auto& member : member_list) {
368 if (member.nickname.empty())
369 continue;
370 QStandardItem* name_item = new PlayerListItem(member.nickname, member.username,
371 member.avatar_url, member.game_info.name);
372
373#ifdef ENABLE_WEB_SERVICE
374 if (!icon_cache.count(member.avatar_url) && !member.avatar_url.empty()) {
375 // Start a request to get the member's avatar
376 const QUrl url(QString::fromStdString(member.avatar_url));
377 QFuture<std::string> future = QtConcurrent::run([url] {
378 WebService::Client client(
379 QStringLiteral("%1://%2").arg(url.scheme(), url.host()).toStdString(), "", "");
380 auto result = client.GetImage(url.path().toStdString(), true);
381 if (result.returned_data.empty()) {
382 LOG_ERROR(WebService, "Failed to get avatar");
383 }
384 return result.returned_data;
385 });
386 auto* future_watcher = new QFutureWatcher<std::string>(this);
387 connect(future_watcher, &QFutureWatcher<std::string>::finished, this,
388 [this, future_watcher, avatar_url = member.avatar_url] {
389 const std::string result = future_watcher->result();
390 if (result.empty())
391 return;
392 QPixmap pixmap;
393 if (!pixmap.loadFromData(reinterpret_cast<const u8*>(result.data()),
394 static_cast<uint>(result.size())))
395 return;
396 icon_cache[avatar_url] =
397 pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
398 // Update all the displayed icons with the new icon_cache
399 UpdateIconDisplay();
400 });
401 future_watcher->setFuture(future);
402 }
403#endif
404
405 player_list->invisibleRootItem()->appendRow(name_item);
406 }
407 UpdateIconDisplay();
408 // TODO(B3N30): Restore row selection
409}
410
411void ChatRoom::OnChatTextChanged() {
412 if (ui->chat_message->text().length() > static_cast<int>(Network::MaxMessageSize))
413 ui->chat_message->setText(
414 ui->chat_message->text().left(static_cast<int>(Network::MaxMessageSize)));
415}
416
417void ChatRoom::PopupContextMenu(const QPoint& menu_location) {
418 QModelIndex item = ui->player_view->indexAt(menu_location);
419 if (!item.isValid())
420 return;
421
422 std::string nickname =
423 player_list->item(item.row())->data(PlayerListItem::NicknameRole).toString().toStdString();
424
425 QMenu context_menu;
426
427 QString username = player_list->item(item.row())->data(PlayerListItem::UsernameRole).toString();
428 if (!username.isEmpty()) {
429 QAction* view_profile_action = context_menu.addAction(tr("View Profile"));
430 connect(view_profile_action, &QAction::triggered, [username] {
431 QDesktopServices::openUrl(
432 QUrl(QStringLiteral("https://community.citra-emu.org/u/%1").arg(username)));
433 });
434 }
435
436 std::string cur_nickname;
437 if (auto room = room_network->GetRoomMember().lock()) {
438 cur_nickname = room->GetNickname();
439 }
440
441 if (nickname != cur_nickname) { // You can't block yourself
442 QAction* block_action = context_menu.addAction(tr("Block Player"));
443
444 block_action->setCheckable(true);
445 block_action->setChecked(block_list.count(nickname) > 0);
446
447 connect(block_action, &QAction::triggered, [this, nickname] {
448 if (block_list.count(nickname)) {
449 block_list.erase(nickname);
450 } else {
451 QMessageBox::StandardButton result = QMessageBox::question(
452 this, tr("Block Player"),
453 tr("When you block a player, you will no longer receive chat messages from "
454 "them.<br><br>Are you sure you would like to block %1?")
455 .arg(QString::fromStdString(nickname)),
456 QMessageBox::Yes | QMessageBox::No);
457 if (result == QMessageBox::Yes)
458 block_list.emplace(nickname);
459 }
460 });
461 }
462
463 if (has_mod_perms && nickname != cur_nickname) { // You can't kick or ban yourself
464 context_menu.addSeparator();
465
466 QAction* kick_action = context_menu.addAction(tr("Kick"));
467 QAction* ban_action = context_menu.addAction(tr("Ban"));
468
469 connect(kick_action, &QAction::triggered, [this, nickname] {
470 QMessageBox::StandardButton result =
471 QMessageBox::question(this, tr("Kick Player"),
472 tr("Are you sure you would like to <b>kick</b> %1?")
473 .arg(QString::fromStdString(nickname)),
474 QMessageBox::Yes | QMessageBox::No);
475 if (result == QMessageBox::Yes)
476 SendModerationRequest(Network::IdModKick, nickname);
477 });
478 connect(ban_action, &QAction::triggered, [this, nickname] {
479 QMessageBox::StandardButton result = QMessageBox::question(
480 this, tr("Ban Player"),
481 tr("Are you sure you would like to <b>kick and ban</b> %1?\n\nThis would "
482 "ban both their forum username and their IP address.")
483 .arg(QString::fromStdString(nickname)),
484 QMessageBox::Yes | QMessageBox::No);
485 if (result == QMessageBox::Yes)
486 SendModerationRequest(Network::IdModBan, nickname);
487 });
488 }
489
490 context_menu.exec(ui->player_view->viewport()->mapToGlobal(menu_location));
491}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <unordered_set>
8#include <QDialog>
9#include <QSortFilterProxyModel>
10#include <QStandardItemModel>
11#include <QVariant>
12#include "network/network.h"
13
14namespace Ui {
15class ChatRoom;
16}
17
18namespace Core {
19class AnnounceMultiplayerSession;
20}
21
22class ConnectionError;
23class ComboBoxProxyModel;
24
25class ChatMessage;
26
27class ChatRoom : public QWidget {
28 Q_OBJECT
29
30public:
31 explicit ChatRoom(QWidget* parent);
32 void Initialize(Network::RoomNetwork* room_network);
33 void RetranslateUi();
34 void SetPlayerList(const Network::RoomMember::MemberList& member_list);
35 void Clear();
36 void AppendStatusMessage(const QString& msg);
37 ~ChatRoom();
38
39 void SetModPerms(bool is_mod);
40 void UpdateIconDisplay();
41
42public slots:
43 void OnRoomUpdate(const Network::RoomInformation& info);
44 void OnChatReceive(const Network::ChatEntry&);
45 void OnStatusMessageReceive(const Network::StatusMessageEntry&);
46 void OnSendChat();
47 void OnChatTextChanged();
48 void PopupContextMenu(const QPoint& menu_location);
49 void Disable();
50 void Enable();
51
52signals:
53 void ChatReceived(const Network::ChatEntry&);
54 void StatusMessageReceived(const Network::StatusMessageEntry&);
55 void UserPinged();
56
57private:
58 static constexpr u32 max_chat_lines = 1000;
59 void AppendChatMessage(const QString&);
60 bool ValidateMessage(const std::string&);
61 void SendModerationRequest(Network::RoomMessageTypes type, const std::string& nickname);
62
63 bool has_mod_perms = false;
64 QStandardItemModel* player_list;
65 std::unique_ptr<Ui::ChatRoom> ui;
66 std::unordered_set<std::string> block_list;
67 std::unordered_map<std::string, QPixmap> icon_cache;
68 Network::RoomNetwork* room_network;
69};
70
71Q_DECLARE_METATYPE(Network::ChatEntry);
72Q_DECLARE_METATYPE(Network::StatusMessageEntry);
73Q_DECLARE_METATYPE(Network::RoomInformation);
74Q_DECLARE_METATYPE(Network::RoomMember::State);
75Q_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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>ChatRoom</class>
4 <widget class="QWidget" name="ChatRoom">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>807</width>
10 <height>432</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Room Window</string>
15 </property>
16 <layout class="QHBoxLayout" name="horizontalLayout">
17 <item>
18 <widget class="QTreeView" name="player_view"/>
19 </item>
20 <item>
21 <layout class="QVBoxLayout" name="verticalLayout_4">
22 <item>
23 <widget class="QTextEdit" name="chat_history">
24 <property name="undoRedoEnabled">
25 <bool>false</bool>
26 </property>
27 <property name="readOnly">
28 <bool>true</bool>
29 </property>
30 <property name="textInteractionFlags">
31 <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
32 </property>
33 </widget>
34 </item>
35 <item>
36 <layout class="QHBoxLayout" name="horizontalLayout_3">
37 <item>
38 <widget class="QLineEdit" name="chat_message">
39 <property name="placeholderText">
40 <string>Send Chat Message</string>
41 </property>
42 </widget>
43 </item>
44 <item>
45 <widget class="QPushButton" name="send_message">
46 <property name="text">
47 <string>Send Message</string>
48 </property>
49 </widget>
50 </item>
51 </layout>
52 </item>
53 </layout>
54 </item>
55 </layout>
56 </widget>
57 <resources/>
58 <connections/>
59</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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <future>
5#include <QColor>
6#include <QImage>
7#include <QList>
8#include <QLocale>
9#include <QMetaType>
10#include <QTime>
11#include <QtConcurrent/QtConcurrentRun>
12#include "common/logging/log.h"
13#include "core/announce_multiplayer_session.h"
14#include "ui_client_room.h"
15#include "yuzu/game_list_p.h"
16#include "yuzu/multiplayer/client_room.h"
17#include "yuzu/multiplayer/message.h"
18#include "yuzu/multiplayer/moderation_dialog.h"
19#include "yuzu/multiplayer/state.h"
20
21ClientRoomWindow::ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_)
22 : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
23 ui(std::make_unique<Ui::ClientRoom>()), room_network{room_network_} {
24 ui->setupUi(this);
25 ui->chat->Initialize(&room_network);
26
27 // setup the callbacks for network updates
28 if (auto member = room_network.GetRoomMember().lock()) {
29 member->BindOnRoomInformationChanged(
30 [this](const Network::RoomInformation& info) { emit RoomInformationChanged(info); });
31 member->BindOnStateChanged(
32 [this](const Network::RoomMember::State& state) { emit StateChanged(state); });
33
34 connect(this, &ClientRoomWindow::RoomInformationChanged, this,
35 &ClientRoomWindow::OnRoomUpdate);
36 connect(this, &ClientRoomWindow::StateChanged, this, &::ClientRoomWindow::OnStateChange);
37 // Update the state
38 OnStateChange(member->GetState());
39 } else {
40 // TODO (jroweboy) network was not initialized?
41 }
42
43 connect(ui->disconnect, &QPushButton::clicked, this, &ClientRoomWindow::Disconnect);
44 ui->disconnect->setDefault(false);
45 ui->disconnect->setAutoDefault(false);
46 connect(ui->moderation, &QPushButton::clicked, [this] {
47 ModerationDialog dialog(room_network, this);
48 dialog.exec();
49 });
50 ui->moderation->setDefault(false);
51 ui->moderation->setAutoDefault(false);
52 connect(ui->chat, &ChatRoom::UserPinged, this, &ClientRoomWindow::ShowNotification);
53 UpdateView();
54}
55
56ClientRoomWindow::~ClientRoomWindow() = default;
57
58void ClientRoomWindow::SetModPerms(bool is_mod) {
59 ui->chat->SetModPerms(is_mod);
60 ui->moderation->setVisible(is_mod);
61 ui->moderation->setDefault(false);
62 ui->moderation->setAutoDefault(false);
63}
64
65void ClientRoomWindow::RetranslateUi() {
66 ui->retranslateUi(this);
67 ui->chat->RetranslateUi();
68}
69
70void ClientRoomWindow::OnRoomUpdate(const Network::RoomInformation& info) {
71 UpdateView();
72}
73
74void ClientRoomWindow::OnStateChange(const Network::RoomMember::State& state) {
75 if (state == Network::RoomMember::State::Joined ||
76 state == Network::RoomMember::State::Moderator) {
77
78 ui->chat->Clear();
79 ui->chat->AppendStatusMessage(tr("Connected"));
80 SetModPerms(state == Network::RoomMember::State::Moderator);
81 }
82 UpdateView();
83}
84
85void ClientRoomWindow::Disconnect() {
86 auto parent = static_cast<MultiplayerState*>(parentWidget());
87 if (parent->OnCloseRoom()) {
88 ui->chat->AppendStatusMessage(tr("Disconnected"));
89 close();
90 }
91}
92
93void ClientRoomWindow::UpdateView() {
94 if (auto member = room_network.GetRoomMember().lock()) {
95 if (member->IsConnected()) {
96 ui->chat->Enable();
97 ui->disconnect->setEnabled(true);
98 auto memberlist = member->GetMemberInformation();
99 ui->chat->SetPlayerList(memberlist);
100 const auto information = member->GetRoomInformation();
101 setWindowTitle(QString(tr("%1 (%2/%3 members) - connected"))
102 .arg(QString::fromStdString(information.name))
103 .arg(memberlist.size())
104 .arg(information.member_slots));
105 ui->description->setText(QString::fromStdString(information.description));
106 return;
107 }
108 }
109 // TODO(B3N30): can't get RoomMember*, show error and close window
110 close();
111}
112
113void ClientRoomWindow::UpdateIconDisplay() {
114 ui->chat->UpdateIconDisplay();
115}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "yuzu/multiplayer/chat_room.h"
7
8namespace Ui {
9class ClientRoom;
10}
11
12class ClientRoomWindow : public QDialog {
13 Q_OBJECT
14
15public:
16 explicit ClientRoomWindow(QWidget* parent, Network::RoomNetwork& room_network_);
17 ~ClientRoomWindow();
18
19 void RetranslateUi();
20 void UpdateIconDisplay();
21
22public slots:
23 void OnRoomUpdate(const Network::RoomInformation&);
24 void OnStateChange(const Network::RoomMember::State&);
25
26signals:
27 void RoomInformationChanged(const Network::RoomInformation&);
28 void StateChanged(const Network::RoomMember::State&);
29 void ShowNotification();
30
31private:
32 void Disconnect();
33 void UpdateView();
34 void SetModPerms(bool is_mod);
35
36 QStandardItemModel* player_list;
37 std::unique_ptr<Ui::ClientRoom> ui;
38 Network::RoomNetwork& room_network;
39};
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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>ClientRoom</class>
4 <widget class="QWidget" name="ClientRoom">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>807</width>
10 <height>432</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Room Window</string>
15 </property>
16 <layout class="QVBoxLayout" name="verticalLayout">
17 <item>
18 <layout class="QVBoxLayout" name="verticalLayout_3">
19 <item>
20 <layout class="QHBoxLayout" name="horizontalLayout">
21 <property name="rightMargin">
22 <number>0</number>
23 </property>
24 <item>
25 <widget class="QLabel" name="description">
26 <property name="text">
27 <string>Room Description</string>
28 </property>
29 </widget>
30 </item>
31 <item>
32 <spacer name="horizontalSpacer">
33 <property name="orientation">
34 <enum>Qt::Horizontal</enum>
35 </property>
36 <property name="sizeHint" stdset="0">
37 <size>
38 <width>40</width>
39 <height>20</height>
40 </size>
41 </property>
42 </spacer>
43 </item>
44 <item>
45 <widget class="QPushButton" name="moderation">
46 <property name="text">
47 <string>Moderation...</string>
48 </property>
49 <property name="visible">
50 <bool>false</bool>
51 </property>
52 </widget>
53 </item>
54 <item>
55 <widget class="QPushButton" name="disconnect">
56 <property name="text">
57 <string>Leave Room</string>
58 </property>
59 </widget>
60 </item>
61 </layout>
62 </item>
63 <item>
64 <widget class="ChatRoom" name="chat" native="true"/>
65 </item>
66 </layout>
67 </item>
68 </layout>
69 </widget>
70 <customwidgets>
71 <customwidget>
72 <class>ChatRoom</class>
73 <extends>QWidget</extends>
74 <header>multiplayer/chat_room.h</header>
75 <container>1</container>
76 </customwidget>
77 </customwidgets>
78 <resources/>
79 <connections/>
80</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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <QComboBox>
5#include <QFuture>
6#include <QIntValidator>
7#include <QRegExpValidator>
8#include <QString>
9#include <QtConcurrent/QtConcurrentRun>
10#include "common/settings.h"
11#include "network/network.h"
12#include "ui_direct_connect.h"
13#include "yuzu/main.h"
14#include "yuzu/multiplayer/client_room.h"
15#include "yuzu/multiplayer/direct_connect.h"
16#include "yuzu/multiplayer/message.h"
17#include "yuzu/multiplayer/state.h"
18#include "yuzu/multiplayer/validation.h"
19#include "yuzu/uisettings.h"
20
21enum class ConnectionType : u8 { TraversalServer, IP };
22
23DirectConnectWindow::DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent)
24 : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
25 ui(std::make_unique<Ui::DirectConnect>()), room_network{room_network_} {
26
27 ui->setupUi(this);
28
29 // setup the watcher for background connections
30 watcher = new QFutureWatcher<void>;
31 connect(watcher, &QFutureWatcher<void>::finished, this, &DirectConnectWindow::OnConnection);
32
33 ui->nickname->setValidator(validation.GetNickname());
34 ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue());
35 if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) {
36 // Use yuzu Web Service user name as nickname by default
37 ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue()));
38 }
39 ui->ip->setValidator(validation.GetIP());
40 ui->ip->setText(UISettings::values.multiplayer_ip.GetValue());
41 ui->port->setValidator(validation.GetPort());
42 ui->port->setText(QString::number(UISettings::values.multiplayer_port.GetValue()));
43
44 // TODO(jroweboy): Show or hide the connection options based on the current value of the combo
45 // box. Add this back in when the traversal server support is added.
46 connect(ui->connect, &QPushButton::clicked, this, &DirectConnectWindow::Connect);
47}
48
49DirectConnectWindow::~DirectConnectWindow() = default;
50
51void DirectConnectWindow::RetranslateUi() {
52 ui->retranslateUi(this);
53}
54
55void DirectConnectWindow::Connect() {
56 if (!ui->nickname->hasAcceptableInput()) {
57 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
58 return;
59 }
60 if (const auto member = room_network.GetRoomMember().lock()) {
61 // Prevent the user from trying to join a room while they are already joining.
62 if (member->GetState() == Network::RoomMember::State::Joining) {
63 return;
64 } else if (member->IsConnected()) {
65 // And ask if they want to leave the room if they are already in one.
66 if (!NetworkMessage::WarnDisconnect()) {
67 return;
68 }
69 }
70 }
71 switch (static_cast<ConnectionType>(ui->connection_type->currentIndex())) {
72 case ConnectionType::TraversalServer:
73 break;
74 case ConnectionType::IP:
75 if (!ui->ip->hasAcceptableInput()) {
76 NetworkMessage::ErrorManager::ShowError(
77 NetworkMessage::ErrorManager::IP_ADDRESS_NOT_VALID);
78 return;
79 }
80 if (!ui->port->hasAcceptableInput()) {
81 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID);
82 return;
83 }
84 break;
85 }
86
87 // Store settings
88 UISettings::values.multiplayer_nickname = ui->nickname->text();
89 UISettings::values.multiplayer_ip = ui->ip->text();
90 if (ui->port->isModified() && !ui->port->text().isEmpty()) {
91 UISettings::values.multiplayer_port = ui->port->text().toInt();
92 } else {
93 UISettings::values.multiplayer_port = UISettings::values.multiplayer_port.GetDefault();
94 }
95
96 // attempt to connect in a different thread
97 QFuture<void> f = QtConcurrent::run([&] {
98 if (auto room_member = room_network.GetRoomMember().lock()) {
99 auto port = UISettings::values.multiplayer_port.GetValue();
100 room_member->Join(ui->nickname->text().toStdString(), "",
101 ui->ip->text().toStdString().c_str(), port, 0,
102 Network::NoPreferredMac, ui->password->text().toStdString().c_str());
103 }
104 });
105 watcher->setFuture(f);
106 // and disable widgets and display a connecting while we wait
107 BeginConnecting();
108}
109
110void DirectConnectWindow::BeginConnecting() {
111 ui->connect->setEnabled(false);
112 ui->connect->setText(tr("Connecting"));
113}
114
115void DirectConnectWindow::EndConnecting() {
116 ui->connect->setEnabled(true);
117 ui->connect->setText(tr("Connect"));
118}
119
120void DirectConnectWindow::OnConnection() {
121 EndConnecting();
122
123 if (auto room_member = room_network.GetRoomMember().lock()) {
124 if (room_member->GetState() == Network::RoomMember::State::Joined ||
125 room_member->GetState() == Network::RoomMember::State::Moderator) {
126
127 close();
128 }
129 }
130}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <QDialog>
8#include <QFutureWatcher>
9#include "yuzu/multiplayer/validation.h"
10
11namespace Ui {
12class DirectConnect;
13}
14
15class DirectConnectWindow : public QDialog {
16 Q_OBJECT
17
18public:
19 explicit DirectConnectWindow(Network::RoomNetwork& room_network_, QWidget* parent = nullptr);
20 ~DirectConnectWindow();
21
22 void RetranslateUi();
23
24signals:
25 /**
26 * Signalled by this widget when it is closing itself and destroying any state such as
27 * connections that it might have.
28 */
29 void Closed();
30
31private slots:
32 void OnConnection();
33
34private:
35 void Connect();
36 void BeginConnecting();
37 void EndConnecting();
38
39 QFutureWatcher<void>* watcher;
40 std::unique_ptr<Ui::DirectConnect> ui;
41 Validation validation;
42 Network::RoomNetwork& room_network;
43};
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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>DirectConnect</class>
4 <widget class="QWidget" name="DirectConnect">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>455</width>
10 <height>161</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Direct Connect</string>
15 </property>
16 <layout class="QVBoxLayout" name="verticalLayout">
17 <item>
18 <layout class="QVBoxLayout" name="verticalLayout_3">
19 <item>
20 <layout class="QVBoxLayout" name="verticalLayout_2">
21 <item>
22 <layout class="QHBoxLayout" name="horizontalLayout">
23 <property name="spacing">
24 <number>0</number>
25 </property>
26 <property name="leftMargin">
27 <number>0</number>
28 </property>
29 <item>
30 <widget class="QComboBox" name="connection_type">
31 <item>
32 <property name="text">
33 <string>IP Address</string>
34 </property>
35 </item>
36 </widget>
37 </item>
38 <item>
39 <widget class="QWidget" name="ip_container" native="true">
40 <layout class="QHBoxLayout" name="ip_layout">
41 <property name="leftMargin">
42 <number>5</number>
43 </property>
44 <property name="topMargin">
45 <number>0</number>
46 </property>
47 <property name="rightMargin">
48 <number>0</number>
49 </property>
50 <property name="bottomMargin">
51 <number>0</number>
52 </property>
53 <item>
54 <widget class="QLabel" name="label_2">
55 <property name="text">
56 <string>IP</string>
57 </property>
58 </widget>
59 </item>
60 <item>
61 <widget class="QLineEdit" name="ip">
62 <property name="toolTip">
63 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;IPv4 address of the host&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
64 </property>
65 <property name="maxLength">
66 <number>16</number>
67 </property>
68 </widget>
69 </item>
70 <item>
71 <widget class="QLabel" name="label_3">
72 <property name="text">
73 <string>Port</string>
74 </property>
75 </widget>
76 </item>
77 <item>
78 <widget class="QLineEdit" name="port">
79 <property name="toolTip">
80 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Port number the host is listening on&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
81 </property>
82 <property name="maxLength">
83 <number>5</number>
84 </property>
85 <property name="placeholderText">
86 <string>24872</string>
87 </property>
88 </widget>
89 </item>
90 </layout>
91 </widget>
92 </item>
93 </layout>
94 </item>
95 <item>
96 <layout class="QHBoxLayout" name="horizontalLayout_2">
97 <item>
98 <widget class="QLabel" name="label_5">
99 <property name="text">
100 <string>Nickname</string>
101 </property>
102 </widget>
103 </item>
104 <item>
105 <widget class="QLineEdit" name="nickname">
106 <property name="maxLength">
107 <number>20</number>
108 </property>
109 </widget>
110 </item>
111 <item>
112 <widget class="QLabel" name="label">
113 <property name="text">
114 <string>Password</string>
115 </property>
116 </widget>
117 </item>
118 <item>
119 <widget class="QLineEdit" name="password"/>
120 </item>
121 </layout>
122 </item>
123 </layout>
124 </item>
125 <item>
126 <spacer name="verticalSpacer">
127 <property name="orientation">
128 <enum>Qt::Vertical</enum>
129 </property>
130 <property name="sizeHint" stdset="0">
131 <size>
132 <width>20</width>
133 <height>20</height>
134 </size>
135 </property>
136 </spacer>
137 </item>
138 <item>
139 <layout class="QHBoxLayout" name="horizontalLayout_3">
140 <item>
141 <spacer name="horizontalSpacer">
142 <property name="orientation">
143 <enum>Qt::Horizontal</enum>
144 </property>
145 <property name="sizeHint" stdset="0">
146 <size>
147 <width>40</width>
148 <height>20</height>
149 </size>
150 </property>
151 </spacer>
152 </item>
153 <item>
154 <widget class="QPushButton" name="connect">
155 <property name="text">
156 <string>Connect</string>
157 </property>
158 </widget>
159 </item>
160 </layout>
161 </item>
162 </layout>
163 </item>
164 </layout>
165 </widget>
166 <resources/>
167 <connections/>
168</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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <future>
5#include <QColor>
6#include <QImage>
7#include <QList>
8#include <QLocale>
9#include <QMessageBox>
10#include <QMetaType>
11#include <QTime>
12#include <QtConcurrent/QtConcurrentRun>
13#include "common/logging/log.h"
14#include "common/settings.h"
15#include "core/announce_multiplayer_session.h"
16#include "ui_host_room.h"
17#include "yuzu/game_list_p.h"
18#include "yuzu/main.h"
19#include "yuzu/multiplayer/host_room.h"
20#include "yuzu/multiplayer/message.h"
21#include "yuzu/multiplayer/state.h"
22#include "yuzu/multiplayer/validation.h"
23#include "yuzu/uisettings.h"
24#ifdef ENABLE_WEB_SERVICE
25#include "web_service/verify_user_jwt.h"
26#endif
27
28HostRoomWindow::HostRoomWindow(QWidget* parent, QStandardItemModel* list,
29 std::shared_ptr<Core::AnnounceMultiplayerSession> session,
30 Network::RoomNetwork& room_network_)
31 : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
32 ui(std::make_unique<Ui::HostRoom>()),
33 announce_multiplayer_session(session), room_network{room_network_} {
34 ui->setupUi(this);
35
36 // set up validation for all of the fields
37 ui->room_name->setValidator(validation.GetRoomName());
38 ui->username->setValidator(validation.GetNickname());
39 ui->port->setValidator(validation.GetPort());
40 ui->port->setPlaceholderText(QString::number(Network::DefaultRoomPort));
41
42 // Create a proxy to the game list to display the list of preferred games
43 game_list = new QStandardItemModel;
44 UpdateGameList(list);
45
46 proxy = new ComboBoxProxyModel;
47 proxy->setSourceModel(game_list);
48 proxy->sort(0, Qt::AscendingOrder);
49 ui->game_list->setModel(proxy);
50
51 // Connect all the widgets to the appropriate events
52 connect(ui->host, &QPushButton::clicked, this, &HostRoomWindow::Host);
53
54 // Restore the settings:
55 ui->username->setText(UISettings::values.multiplayer_room_nickname.GetValue());
56 if (ui->username->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) {
57 // Use yuzu Web Service user name as nickname by default
58 ui->username->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue()));
59 }
60 ui->room_name->setText(UISettings::values.multiplayer_room_name.GetValue());
61 ui->port->setText(QString::number(UISettings::values.multiplayer_room_port.GetValue()));
62 ui->max_player->setValue(UISettings::values.multiplayer_max_player.GetValue());
63 int index = UISettings::values.multiplayer_host_type.GetValue();
64 if (index < ui->host_type->count()) {
65 ui->host_type->setCurrentIndex(index);
66 }
67 index = ui->game_list->findData(UISettings::values.multiplayer_game_id.GetValue(),
68 GameListItemPath::ProgramIdRole);
69 if (index != -1) {
70 ui->game_list->setCurrentIndex(index);
71 }
72 ui->room_description->setText(UISettings::values.multiplayer_room_description.GetValue());
73}
74
75HostRoomWindow::~HostRoomWindow() = default;
76
77void HostRoomWindow::UpdateGameList(QStandardItemModel* list) {
78 game_list->clear();
79 for (int i = 0; i < list->rowCount(); i++) {
80 auto parent = list->item(i, 0);
81 for (int j = 0; j < parent->rowCount(); j++) {
82 game_list->appendRow(parent->child(j)->clone());
83 }
84 }
85}
86
87void HostRoomWindow::RetranslateUi() {
88 ui->retranslateUi(this);
89}
90
91std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBackend(
92 bool use_validation) const {
93 std::unique_ptr<Network::VerifyUser::Backend> verify_backend;
94 if (use_validation) {
95#ifdef ENABLE_WEB_SERVICE
96 verify_backend =
97 std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue());
98#else
99 verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
100#endif
101 } else {
102 verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
103 }
104 return verify_backend;
105}
106
107void HostRoomWindow::Host() {
108 if (!ui->username->hasAcceptableInput()) {
109 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
110 return;
111 }
112 if (!ui->room_name->hasAcceptableInput()) {
113 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOMNAME_NOT_VALID);
114 return;
115 }
116 if (!ui->port->hasAcceptableInput()) {
117 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PORT_NOT_VALID);
118 return;
119 }
120 if (ui->game_list->currentIndex() == -1) {
121 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::GAME_NOT_SELECTED);
122 return;
123 }
124 if (auto member = room_network.GetRoomMember().lock()) {
125 if (member->GetState() == Network::RoomMember::State::Joining) {
126 return;
127 } else if (member->IsConnected()) {
128 auto parent = static_cast<MultiplayerState*>(parentWidget());
129 if (!parent->OnCloseRoom()) {
130 close();
131 return;
132 }
133 }
134 ui->host->setDisabled(true);
135
136 const AnnounceMultiplayerRoom::GameInfo game{
137 .name = ui->game_list->currentData(Qt::DisplayRole).toString().toStdString(),
138 .id = ui->game_list->currentData(GameListItemPath::ProgramIdRole).toULongLong(),
139 };
140 const auto port =
141 ui->port->isModified() ? ui->port->text().toInt() : Network::DefaultRoomPort;
142 const auto password = ui->password->text().toStdString();
143 const bool is_public = ui->host_type->currentIndex() == 0;
144 Network::Room::BanList ban_list{};
145 if (ui->load_ban_list->isChecked()) {
146 ban_list = UISettings::values.multiplayer_ban_list;
147 }
148 if (auto room = room_network.GetRoom().lock()) {
149 const bool created =
150 room->Create(ui->room_name->text().toStdString(),
151 ui->room_description->toPlainText().toStdString(), "", port, password,
152 ui->max_player->value(), Settings::values.yuzu_username.GetValue(),
153 game, CreateVerifyBackend(is_public), ban_list);
154 if (!created) {
155 NetworkMessage::ErrorManager::ShowError(
156 NetworkMessage::ErrorManager::COULD_NOT_CREATE_ROOM);
157 LOG_ERROR(Network, "Could not create room!");
158 ui->host->setEnabled(true);
159 return;
160 }
161 }
162 // Start the announce session if they chose Public
163 if (is_public) {
164 if (auto session = announce_multiplayer_session.lock()) {
165 // Register the room first to ensure verify_uid is present when we connect
166 WebService::WebResult result = session->Register();
167 if (result.result_code != WebService::WebResult::Code::Success) {
168 QMessageBox::warning(
169 this, tr("Error"),
170 tr("Failed to announce the room to the public lobby. In order to host a "
171 "room publicly, you must have a valid yuzu account configured in "
172 "Emulation -> Configure -> Web. If you do not want to publish a room in "
173 "the public lobby, then select Unlisted instead.\nDebug Message: ") +
174 QString::fromStdString(result.result_string),
175 QMessageBox::Ok);
176 ui->host->setEnabled(true);
177 if (auto room = room_network.GetRoom().lock()) {
178 room->Destroy();
179 }
180 return;
181 }
182 session->Start();
183 } else {
184 LOG_ERROR(Network, "Starting announce session failed");
185 }
186 }
187 std::string token;
188#ifdef ENABLE_WEB_SERVICE
189 if (is_public) {
190 WebService::Client client(Settings::values.web_api_url.GetValue(),
191 Settings::values.yuzu_username.GetValue(),
192 Settings::values.yuzu_token.GetValue());
193 if (auto room = room_network.GetRoom().lock()) {
194 token = client.GetExternalJWT(room->GetVerifyUID()).returned_data;
195 }
196 if (token.empty()) {
197 LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
198 } else {
199 LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
200 }
201 }
202#endif
203 // TODO: Check what to do with this
204 member->Join(ui->username->text().toStdString(), "", "127.0.0.1", port, 0,
205 Network::NoPreferredMac, password, token);
206
207 // Store settings
208 UISettings::values.multiplayer_room_nickname = ui->username->text();
209 UISettings::values.multiplayer_room_name = ui->room_name->text();
210 UISettings::values.multiplayer_game_id =
211 ui->game_list->currentData(GameListItemPath::ProgramIdRole).toLongLong();
212 UISettings::values.multiplayer_max_player = ui->max_player->value();
213
214 UISettings::values.multiplayer_host_type = ui->host_type->currentIndex();
215 if (ui->port->isModified() && !ui->port->text().isEmpty()) {
216 UISettings::values.multiplayer_room_port = ui->port->text().toInt();
217 } else {
218 UISettings::values.multiplayer_room_port = Network::DefaultRoomPort;
219 }
220 UISettings::values.multiplayer_room_description = ui->room_description->toPlainText();
221 ui->host->setEnabled(true);
222 close();
223 }
224}
225
226QVariant ComboBoxProxyModel::data(const QModelIndex& idx, int role) const {
227 if (role != Qt::DisplayRole) {
228 auto val = QSortFilterProxyModel::data(idx, role);
229 // If its the icon, shrink it to 16x16
230 if (role == Qt::DecorationRole)
231 val = val.value<QImage>().scaled(16, 16, Qt::KeepAspectRatio);
232 return val;
233 }
234 std::string filename;
235 Common::SplitPath(
236 QSortFilterProxyModel::data(idx, GameListItemPath::FullPathRole).toString().toStdString(),
237 nullptr, &filename, nullptr);
238 QString title = QSortFilterProxyModel::data(idx, GameListItemPath::TitleRole).toString();
239 return title.isEmpty() ? QString::fromStdString(filename) : title;
240}
241
242bool ComboBoxProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
243 auto leftData = left.data(GameListItemPath::TitleRole).toString();
244 auto rightData = right.data(GameListItemPath::TitleRole).toString();
245 return leftData.compare(rightData) < 0;
246}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <QDialog>
8#include <QSortFilterProxyModel>
9#include <QStandardItemModel>
10#include <QVariant>
11#include "network/network.h"
12#include "yuzu/multiplayer/chat_room.h"
13#include "yuzu/multiplayer/validation.h"
14
15namespace Ui {
16class HostRoom;
17}
18
19namespace Core {
20class AnnounceMultiplayerSession;
21}
22
23class ConnectionError;
24class ComboBoxProxyModel;
25
26class ChatMessage;
27
28namespace Network::VerifyUser {
29class Backend;
30};
31
32class HostRoomWindow : public QDialog {
33 Q_OBJECT
34
35public:
36 explicit HostRoomWindow(QWidget* parent, QStandardItemModel* list,
37 std::shared_ptr<Core::AnnounceMultiplayerSession> session,
38 Network::RoomNetwork& room_network_);
39 ~HostRoomWindow();
40
41 /**
42 * Updates the dialog with a new game list model.
43 * This model should be the original model of the game list.
44 */
45 void UpdateGameList(QStandardItemModel* list);
46 void RetranslateUi();
47
48private:
49 void Host();
50 std::unique_ptr<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const;
51
52 std::unique_ptr<Ui::HostRoom> ui;
53 std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
54 QStandardItemModel* game_list;
55 ComboBoxProxyModel* proxy;
56 Validation validation;
57 Network::RoomNetwork& room_network;
58};
59
60/**
61 * Proxy Model for the game list combo box so we can reuse the game list model while still
62 * displaying the fields slightly differently
63 */
64class ComboBoxProxyModel : public QSortFilterProxyModel {
65 Q_OBJECT
66
67public:
68 int columnCount(const QModelIndex& idx) const override {
69 return 1;
70 }
71
72 QVariant data(const QModelIndex& idx, int role) const override;
73
74 bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
75};
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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>HostRoom</class>
4 <widget class="QWidget" name="HostRoom">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>607</width>
10 <height>211</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Create Room</string>
15 </property>
16 <layout class="QVBoxLayout" name="verticalLayout_3">
17 <item>
18 <widget class="QWidget" name="settings" native="true">
19 <layout class="QHBoxLayout">
20 <property name="leftMargin">
21 <number>0</number>
22 </property>
23 <property name="topMargin">
24 <number>0</number>
25 </property>
26 <property name="rightMargin">
27 <number>0</number>
28 </property>
29 <item>
30 <layout class="QFormLayout" name="formLayout_2">
31 <property name="labelAlignment">
32 <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
33 </property>
34 <item row="0" column="0">
35 <widget class="QLabel" name="label">
36 <property name="text">
37 <string>Room Name</string>
38 </property>
39 </widget>
40 </item>
41 <item row="0" column="1">
42 <widget class="QLineEdit" name="room_name">
43 <property name="maxLength">
44 <number>50</number>
45 </property>
46 </widget>
47 </item>
48 <item row="1" column="0">
49 <widget class="QLabel" name="label_3">
50 <property name="text">
51 <string>Preferred Game</string>
52 </property>
53 </widget>
54 </item>
55 <item row="1" column="1">
56 <widget class="QComboBox" name="game_list"/>
57 </item>
58 <item row="2" column="0">
59 <widget class="QLabel" name="label_2">
60 <property name="text">
61 <string>Max Players</string>
62 </property>
63 </widget>
64 </item>
65 <item row="2" column="1">
66 <widget class="QSpinBox" name="max_player">
67 <property name="minimum">
68 <number>2</number>
69 </property>
70 <property name="maximum">
71 <number>16</number>
72 </property>
73 <property name="value">
74 <number>8</number>
75 </property>
76 </widget>
77 </item>
78 </layout>
79 </item>
80 <item>
81 <layout class="QFormLayout" name="formLayout">
82 <property name="labelAlignment">
83 <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
84 </property>
85 <item row="0" column="1">
86 <widget class="QLineEdit" name="username"/>
87 </item>
88 <item row="0" column="0">
89 <widget class="QLabel" name="label_6">
90 <property name="text">
91 <string>Username</string>
92 </property>
93 </widget>
94 </item>
95 <item row="1" column="1">
96 <widget class="QLineEdit" name="password">
97 <property name="echoMode">
98 <enum>QLineEdit::PasswordEchoOnEdit</enum>
99 </property>
100 <property name="placeholderText">
101 <string>(Leave blank for open game)</string>
102 </property>
103 </widget>
104 </item>
105 <item row="2" column="1">
106 <widget class="QLineEdit" name="port">
107 <property name="inputMethodHints">
108 <set>Qt::ImhDigitsOnly</set>
109 </property>
110 <property name="maxLength">
111 <number>5</number>
112 </property>
113 </widget>
114 </item>
115 <item row="1" column="0">
116 <widget class="QLabel" name="label_5">
117 <property name="text">
118 <string>Password</string>
119 </property>
120 </widget>
121 </item>
122 <item row="2" column="0">
123 <widget class="QLabel" name="label_4">
124 <property name="text">
125 <string>Port</string>
126 </property>
127 </widget>
128 </item>
129 </layout>
130 </item>
131 </layout>
132 </widget>
133 </item>
134 <item>
135 <layout class="QHBoxLayout" name="horizontalLayout_3">
136 <item>
137 <widget class="QLabel" name="label_7">
138 <property name="text">
139 <string>Room Description</string>
140 </property>
141 </widget>
142 </item>
143 <item>
144 <widget class="QTextEdit" name="room_description"/>
145 </item>
146 </layout>
147 </item>
148 <item>
149 <layout class="QHBoxLayout">
150 <item>
151 <widget class="QCheckBox" name="load_ban_list">
152 <property name="text">
153 <string>Load Previous Ban List</string>
154 </property>
155 <property name="checked">
156 <bool>true</bool>
157 </property>
158 </widget>
159 </item>
160 </layout>
161 </item>
162 <item>
163 <layout class="QHBoxLayout" name="horizontalLayout">
164 <property name="rightMargin">
165 <number>0</number>
166 </property>
167 <item>
168 <spacer name="horizontalSpacer">
169 <property name="orientation">
170 <enum>Qt::Horizontal</enum>
171 </property>
172 <property name="sizeHint" stdset="0">
173 <size>
174 <width>40</width>
175 <height>20</height>
176 </size>
177 </property>
178 </spacer>
179 </item>
180 <item>
181 <widget class="QComboBox" name="host_type">
182 <item>
183 <property name="text">
184 <string>Public</string>
185 </property>
186 </item>
187 <item>
188 <property name="text">
189 <string>Unlisted</string>
190 </property>
191 </item>
192 </widget>
193 </item>
194 <item>
195 <widget class="QPushButton" name="host">
196 <property name="text">
197 <string>Host Room</string>
198 </property>
199 </widget>
200 </item>
201 </layout>
202 </item>
203 </layout>
204 </widget>
205 <resources/>
206 <connections/>
207</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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <QInputDialog>
5#include <QList>
6#include <QtConcurrent/QtConcurrentRun>
7#include "common/logging/log.h"
8#include "common/settings.h"
9#include "network/network.h"
10#include "ui_lobby.h"
11#include "yuzu/game_list_p.h"
12#include "yuzu/main.h"
13#include "yuzu/multiplayer/client_room.h"
14#include "yuzu/multiplayer/lobby.h"
15#include "yuzu/multiplayer/lobby_p.h"
16#include "yuzu/multiplayer/message.h"
17#include "yuzu/multiplayer/state.h"
18#include "yuzu/multiplayer/validation.h"
19#include "yuzu/uisettings.h"
20#ifdef ENABLE_WEB_SERVICE
21#include "web_service/web_backend.h"
22#endif
23
24Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
25 std::shared_ptr<Core::AnnounceMultiplayerSession> session,
26 Network::RoomNetwork& room_network_)
27 : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
28 ui(std::make_unique<Ui::Lobby>()),
29 announce_multiplayer_session(session), room_network{room_network_} {
30 ui->setupUi(this);
31
32 // setup the watcher for background connections
33 watcher = new QFutureWatcher<void>;
34
35 model = new QStandardItemModel(ui->room_list);
36
37 // Create a proxy to the game list to get the list of games owned
38 game_list = new QStandardItemModel;
39 UpdateGameList(list);
40
41 proxy = new LobbyFilterProxyModel(this, game_list);
42 proxy->setSourceModel(model);
43 proxy->setDynamicSortFilter(true);
44 proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
45 proxy->setSortLocaleAware(true);
46 ui->room_list->setModel(proxy);
47 ui->room_list->header()->setSectionResizeMode(QHeaderView::Interactive);
48 ui->room_list->header()->stretchLastSection();
49 ui->room_list->setAlternatingRowColors(true);
50 ui->room_list->setSelectionMode(QHeaderView::SingleSelection);
51 ui->room_list->setSelectionBehavior(QHeaderView::SelectRows);
52 ui->room_list->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
53 ui->room_list->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
54 ui->room_list->setSortingEnabled(true);
55 ui->room_list->setEditTriggers(QHeaderView::NoEditTriggers);
56 ui->room_list->setExpandsOnDoubleClick(false);
57 ui->room_list->setContextMenuPolicy(Qt::CustomContextMenu);
58
59 ui->nickname->setValidator(validation.GetNickname());
60 ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue());
61 if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) {
62 // Use yuzu Web Service user name as nickname by default
63 ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue()));
64 }
65
66 // UI Buttons
67 connect(ui->refresh_list, &QPushButton::clicked, this, &Lobby::RefreshLobby);
68 connect(ui->games_owned, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterOwned);
69 connect(ui->hide_full, &QCheckBox::toggled, proxy, &LobbyFilterProxyModel::SetFilterFull);
70 connect(ui->search, &QLineEdit::textChanged, proxy, &LobbyFilterProxyModel::SetFilterSearch);
71 connect(ui->room_list, &QTreeView::doubleClicked, this, &Lobby::OnJoinRoom);
72 connect(ui->room_list, &QTreeView::clicked, this, &Lobby::OnExpandRoom);
73
74 // Actions
75 connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
76 &Lobby::OnRefreshLobby);
77
78 // manually start a refresh when the window is opening
79 // TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
80 // part of the constructor, but offload the refresh until after the window shown. perhaps emit a
81 // refreshroomlist signal from places that open the lobby
82 RefreshLobby();
83}
84
85Lobby::~Lobby() = default;
86
87void Lobby::UpdateGameList(QStandardItemModel* list) {
88 game_list->clear();
89 for (int i = 0; i < list->rowCount(); i++) {
90 auto parent = list->item(i, 0);
91 for (int j = 0; j < parent->rowCount(); j++) {
92 game_list->appendRow(parent->child(j)->clone());
93 }
94 }
95 if (proxy)
96 proxy->UpdateGameList(game_list);
97}
98
99void Lobby::RetranslateUi() {
100 ui->retranslateUi(this);
101}
102
103QString Lobby::PasswordPrompt() {
104 bool ok;
105 const QString text =
106 QInputDialog::getText(this, tr("Password Required to Join"), tr("Password:"),
107 QLineEdit::Password, QString(), &ok);
108 return ok ? text : QString();
109}
110
111void Lobby::OnExpandRoom(const QModelIndex& index) {
112 QModelIndex member_index = proxy->index(index.row(), Column::MEMBER);
113 auto member_list = proxy->data(member_index, LobbyItemMemberList::MemberListRole).toList();
114}
115
116void Lobby::OnJoinRoom(const QModelIndex& source) {
117 if (const auto member = room_network.GetRoomMember().lock()) {
118 // Prevent the user from trying to join a room while they are already joining.
119 if (member->GetState() == Network::RoomMember::State::Joining) {
120 return;
121 } else if (member->IsConnected()) {
122 // And ask if they want to leave the room if they are already in one.
123 if (!NetworkMessage::WarnDisconnect()) {
124 return;
125 }
126 }
127 }
128 QModelIndex index = source;
129 // If the user double clicks on a child row (aka the player list) then use the parent instead
130 if (source.parent() != QModelIndex()) {
131 index = source.parent();
132 }
133 if (!ui->nickname->hasAcceptableInput()) {
134 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::USERNAME_NOT_VALID);
135 return;
136 }
137
138 // Get a password to pass if the room is password protected
139 QModelIndex password_index = proxy->index(index.row(), Column::ROOM_NAME);
140 bool has_password = proxy->data(password_index, LobbyItemName::PasswordRole).toBool();
141 const std::string password = has_password ? PasswordPrompt().toStdString() : "";
142 if (has_password && password.empty()) {
143 return;
144 }
145
146 QModelIndex connection_index = proxy->index(index.row(), Column::HOST);
147 const std::string nickname = ui->nickname->text().toStdString();
148 const std::string ip =
149 proxy->data(connection_index, LobbyItemHost::HostIPRole).toString().toStdString();
150 int port = proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
151 const std::string verify_uid =
152 proxy->data(connection_index, LobbyItemHost::HostVerifyUIDRole).toString().toStdString();
153
154 // attempt to connect in a different thread
155 QFuture<void> f = QtConcurrent::run([nickname, ip, port, password, verify_uid, this] {
156 std::string token;
157#ifdef ENABLE_WEB_SERVICE
158 if (!Settings::values.yuzu_username.GetValue().empty() &&
159 !Settings::values.yuzu_token.GetValue().empty()) {
160 WebService::Client client(Settings::values.web_api_url.GetValue(),
161 Settings::values.yuzu_username.GetValue(),
162 Settings::values.yuzu_token.GetValue());
163 token = client.GetExternalJWT(verify_uid).returned_data;
164 if (token.empty()) {
165 LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
166 } else {
167 LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
168 }
169 }
170#endif
171 if (auto room_member = room_network.GetRoomMember().lock()) {
172 room_member->Join(nickname, "", ip.c_str(), port, 0, Network::NoPreferredMac, password,
173 token);
174 }
175 });
176 watcher->setFuture(f);
177
178 // TODO(jroweboy): disable widgets and display a connecting while we wait
179
180 // Save settings
181 UISettings::values.multiplayer_nickname = ui->nickname->text();
182 UISettings::values.multiplayer_ip =
183 proxy->data(connection_index, LobbyItemHost::HostIPRole).toString();
184 UISettings::values.multiplayer_port =
185 proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
186}
187
188void Lobby::ResetModel() {
189 model->clear();
190 model->insertColumns(0, Column::TOTAL);
191 model->setHeaderData(Column::EXPAND, Qt::Horizontal, QString(), Qt::DisplayRole);
192 model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
193 model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
194 model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
195 model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
196}
197
198void Lobby::RefreshLobby() {
199 if (auto session = announce_multiplayer_session.lock()) {
200 ResetModel();
201 ui->refresh_list->setEnabled(false);
202 ui->refresh_list->setText(tr("Refreshing"));
203 room_list_watcher.setFuture(
204 QtConcurrent::run([session]() { return session->GetRoomList(); }));
205 } else {
206 // TODO(jroweboy): Display an error box about announce couldn't be started
207 }
208}
209
210void Lobby::OnRefreshLobby() {
211 AnnounceMultiplayerRoom::RoomList new_room_list = room_list_watcher.result();
212 for (auto room : new_room_list) {
213 // find the icon for the game if this person owns that game.
214 QPixmap smdh_icon;
215 for (int r = 0; r < game_list->rowCount(); ++r) {
216 auto index = game_list->index(r, 0);
217 auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
218 if (game_id != 0 && room.information.preferred_game.id == game_id) {
219 smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
220 }
221 }
222
223 QList<QVariant> members;
224 for (auto member : room.members) {
225 QVariant var;
226 var.setValue(LobbyMember{QString::fromStdString(member.username),
227 QString::fromStdString(member.nickname), member.game.id,
228 QString::fromStdString(member.game.name)});
229 members.append(var);
230 }
231
232 auto first_item = new LobbyItem();
233 auto row = QList<QStandardItem*>({
234 first_item,
235 new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)),
236 new LobbyItemGame(room.information.preferred_game.id,
237 QString::fromStdString(room.information.preferred_game.name),
238 smdh_icon),
239 new LobbyItemHost(QString::fromStdString(room.information.host_username),
240 QString::fromStdString(room.ip), room.information.port,
241 QString::fromStdString(room.verify_uid)),
242 new LobbyItemMemberList(members, room.information.member_slots),
243 });
244 model->appendRow(row);
245 // To make the rows expandable, add the member data as a child of the first column of the
246 // rows with people in them and have qt set them to colspan after the model is finished
247 // resetting
248 if (!room.information.description.empty()) {
249 first_item->appendRow(
250 new LobbyItemDescription(QString::fromStdString(room.information.description)));
251 }
252 if (!room.members.empty()) {
253 first_item->appendRow(new LobbyItemExpandedMemberList(members));
254 }
255 }
256
257 // Reenable the refresh button and resize the columns
258 ui->refresh_list->setEnabled(true);
259 ui->refresh_list->setText(tr("Refresh List"));
260 ui->room_list->header()->stretchLastSection();
261 for (int i = 0; i < Column::TOTAL - 1; ++i) {
262 ui->room_list->resizeColumnToContents(i);
263 }
264
265 // Set the member list child items to span all columns
266 for (int i = 0; i < proxy->rowCount(); i++) {
267 auto parent = model->item(i, 0);
268 for (int j = 0; j < parent->rowCount(); j++) {
269 ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true);
270 }
271 }
272}
273
274LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)
275 : QSortFilterProxyModel(parent), game_list(list) {}
276
277void LobbyFilterProxyModel::UpdateGameList(QStandardItemModel* list) {
278 game_list = list;
279}
280
281bool LobbyFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const {
282 // Prioritize filters by fastest to compute
283
284 // pass over any child rows (aka row that shows the players in the room)
285 if (sourceParent != QModelIndex()) {
286 return true;
287 }
288
289 // filter by filled rooms
290 if (filter_full) {
291 QModelIndex member_list = sourceModel()->index(sourceRow, Column::MEMBER, sourceParent);
292 int player_count =
293 sourceModel()->data(member_list, LobbyItemMemberList::MemberListRole).toList().size();
294 int max_players =
295 sourceModel()->data(member_list, LobbyItemMemberList::MaxPlayerRole).toInt();
296 if (player_count >= max_players) {
297 return false;
298 }
299 }
300
301 // filter by search parameters
302 if (!filter_search.isEmpty()) {
303 QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
304 QModelIndex room_name = sourceModel()->index(sourceRow, Column::ROOM_NAME, sourceParent);
305 QModelIndex host_name = sourceModel()->index(sourceRow, Column::HOST, sourceParent);
306 bool preferred_game_match = sourceModel()
307 ->data(game_name, LobbyItemGame::GameNameRole)
308 .toString()
309 .contains(filter_search, filterCaseSensitivity());
310 bool room_name_match = sourceModel()
311 ->data(room_name, LobbyItemName::NameRole)
312 .toString()
313 .contains(filter_search, filterCaseSensitivity());
314 bool username_match = sourceModel()
315 ->data(host_name, LobbyItemHost::HostUsernameRole)
316 .toString()
317 .contains(filter_search, filterCaseSensitivity());
318 if (!preferred_game_match && !room_name_match && !username_match) {
319 return false;
320 }
321 }
322
323 // filter by game owned
324 if (filter_owned) {
325 QModelIndex game_name = sourceModel()->index(sourceRow, Column::GAME_NAME, sourceParent);
326 QList<QModelIndex> owned_games;
327 for (int r = 0; r < game_list->rowCount(); ++r) {
328 owned_games.append(QModelIndex(game_list->index(r, 0)));
329 }
330 auto current_id = sourceModel()->data(game_name, LobbyItemGame::TitleIDRole).toLongLong();
331 if (current_id == 0) {
332 // TODO(jroweboy): homebrew often doesn't have a game id and this hides them
333 return false;
334 }
335 bool owned = false;
336 for (const auto& game : owned_games) {
337 auto game_id = game_list->data(game, GameListItemPath::ProgramIdRole).toLongLong();
338 if (current_id == game_id) {
339 owned = true;
340 }
341 }
342 if (!owned) {
343 return false;
344 }
345 }
346
347 return true;
348}
349
350void LobbyFilterProxyModel::sort(int column, Qt::SortOrder order) {
351 sourceModel()->sort(column, order);
352}
353
354void LobbyFilterProxyModel::SetFilterOwned(bool filter) {
355 filter_owned = filter;
356 invalidate();
357}
358
359void LobbyFilterProxyModel::SetFilterFull(bool filter) {
360 filter_full = filter;
361 invalidate();
362}
363
364void LobbyFilterProxyModel::SetFilterSearch(const QString& filter) {
365 filter_search = filter;
366 invalidate();
367}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <QDialog>
8#include <QFutureWatcher>
9#include <QSortFilterProxyModel>
10#include <QStandardItemModel>
11#include "common/announce_multiplayer_room.h"
12#include "core/announce_multiplayer_session.h"
13#include "network/network.h"
14#include "yuzu/multiplayer/validation.h"
15
16namespace Ui {
17class Lobby;
18}
19
20class LobbyModel;
21class LobbyFilterProxyModel;
22
23/**
24 * Listing of all public games pulled from services. The lobby should be simple enough for users to
25 * find the game they want to play, and join it.
26 */
27class Lobby : public QDialog {
28 Q_OBJECT
29
30public:
31 explicit Lobby(QWidget* parent, QStandardItemModel* list,
32 std::shared_ptr<Core::AnnounceMultiplayerSession> session,
33 Network::RoomNetwork& room_network_);
34 ~Lobby() override;
35
36 /**
37 * Updates the lobby with a new game list model.
38 * This model should be the original model of the game list.
39 */
40 void UpdateGameList(QStandardItemModel* list);
41 void RetranslateUi();
42
43public slots:
44 /**
45 * Begin the process to pull the latest room list from web services. After the listing is
46 * returned from web services, `LobbyRefreshed` will be signalled
47 */
48 void RefreshLobby();
49
50private slots:
51 /**
52 * Pulls the list of rooms from network and fills out the lobby model with the results
53 */
54 void OnRefreshLobby();
55
56 /**
57 * Handler for single clicking on a room in the list. Expands the treeitem to show player
58 * information for the people in the room
59 *
60 * index - The row of the proxy model that the user wants to join.
61 */
62 void OnExpandRoom(const QModelIndex&);
63
64 /**
65 * Handler for double clicking on a room in the list. Gathers the host ip and port and attempts
66 * to connect. Will also prompt for a password in case one is required.
67 *
68 * index - The row of the proxy model that the user wants to join.
69 */
70 void OnJoinRoom(const QModelIndex&);
71
72signals:
73 void StateChanged(const Network::RoomMember::State&);
74
75private:
76 /**
77 * Removes all entries in the Lobby before refreshing.
78 */
79 void ResetModel();
80
81 /**
82 * Prompts for a password. Returns an empty QString if the user either did not provide a
83 * password or if the user closed the window.
84 */
85 QString PasswordPrompt();
86
87 std::unique_ptr<Ui::Lobby> ui;
88
89 QStandardItemModel* model{};
90 QStandardItemModel* game_list{};
91 LobbyFilterProxyModel* proxy{};
92
93 QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher;
94 std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
95 QFutureWatcher<void>* watcher;
96 Validation validation;
97 Network::RoomNetwork& room_network;
98};
99
100/**
101 * Proxy Model for filtering the lobby
102 */
103class LobbyFilterProxyModel : public QSortFilterProxyModel {
104 Q_OBJECT;
105
106public:
107 explicit LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list);
108
109 /**
110 * Updates the filter with a new game list model.
111 * This model should be the processed one created by the Lobby.
112 */
113 void UpdateGameList(QStandardItemModel* list);
114
115 bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
116 void sort(int column, Qt::SortOrder order) override;
117
118public slots:
119 void SetFilterOwned(bool);
120 void SetFilterFull(bool);
121 void SetFilterSearch(const QString&);
122
123private:
124 QStandardItemModel* game_list;
125 bool filter_owned = false;
126 bool filter_full = false;
127 QString filter_search;
128};
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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>Lobby</class>
4 <widget class="QWidget" name="Lobby">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>903</width>
10 <height>487</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Public Room Browser</string>
15 </property>
16 <layout class="QVBoxLayout" name="verticalLayout">
17 <item>
18 <layout class="QVBoxLayout" name="verticalLayout_2">
19 <property name="spacing">
20 <number>3</number>
21 </property>
22 <item>
23 <layout class="QHBoxLayout" name="horizontalLayout_3">
24 <property name="spacing">
25 <number>6</number>
26 </property>
27 <item>
28 <layout class="QHBoxLayout" name="horizontalLayout_5">
29 <item>
30 <widget class="QLabel" name="label">
31 <property name="text">
32 <string>Nickname</string>
33 </property>
34 </widget>
35 </item>
36 <item>
37 <widget class="QLineEdit" name="nickname">
38 <property name="placeholderText">
39 <string>Nickname</string>
40 </property>
41 </widget>
42 </item>
43 <item>
44 <spacer name="horizontalSpacer_2">
45 <property name="orientation">
46 <enum>Qt::Horizontal</enum>
47 </property>
48 <property name="sizeHint" stdset="0">
49 <size>
50 <width>40</width>
51 <height>20</height>
52 </size>
53 </property>
54 </spacer>
55 </item>
56 <item>
57 <widget class="QLabel" name="label_2">
58 <property name="text">
59 <string>Filters</string>
60 </property>
61 </widget>
62 </item>
63 <item>
64 <widget class="QLineEdit" name="search">
65 <property name="placeholderText">
66 <string>Search</string>
67 </property>
68 <property name="clearButtonEnabled">
69 <bool>true</bool>
70 </property>
71 </widget>
72 </item>
73 <item>
74 <widget class="QCheckBox" name="games_owned">
75 <property name="text">
76 <string>Games I Own</string>
77 </property>
78 </widget>
79 </item>
80 <item>
81 <widget class="QCheckBox" name="hide_full">
82 <property name="text">
83 <string>Hide Full Rooms</string>
84 </property>
85 </widget>
86 </item>
87 <item>
88 <spacer name="horizontalSpacer">
89 <property name="orientation">
90 <enum>Qt::Horizontal</enum>
91 </property>
92 <property name="sizeHint" stdset="0">
93 <size>
94 <width>40</width>
95 <height>20</height>
96 </size>
97 </property>
98 </spacer>
99 </item>
100 <item>
101 <widget class="QPushButton" name="refresh_list">
102 <property name="text">
103 <string>Refresh Lobby</string>
104 </property>
105 </widget>
106 </item>
107 </layout>
108 </item>
109 </layout>
110 </item>
111 <item>
112 <widget class="QTreeView" name="room_list"/>
113 </item>
114 <item>
115 <widget class="QWidget" name="widget" native="true"/>
116 </item>
117 </layout>
118 </item>
119 </layout>
120 </widget>
121 <resources/>
122 <connections/>
123</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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <utility>
7#include <QPixmap>
8#include <QStandardItem>
9#include <QStandardItemModel>
10#include "common/common_types.h"
11
12namespace Column {
13enum List {
14 EXPAND,
15 ROOM_NAME,
16 GAME_NAME,
17 HOST,
18 MEMBER,
19 TOTAL,
20};
21}
22
23class LobbyItem : public QStandardItem {
24public:
25 LobbyItem() = default;
26 explicit LobbyItem(const QString& string) : QStandardItem(string) {}
27 virtual ~LobbyItem() override = default;
28};
29
30class LobbyItemName : public LobbyItem {
31public:
32 static const int NameRole = Qt::UserRole + 1;
33 static const int PasswordRole = Qt::UserRole + 2;
34
35 LobbyItemName() = default;
36 explicit LobbyItemName(bool has_password, QString name) : LobbyItem() {
37 setData(name, NameRole);
38 setData(has_password, PasswordRole);
39 }
40
41 QVariant data(int role) const override {
42 if (role == Qt::DecorationRole) {
43 bool has_password = data(PasswordRole).toBool();
44 return has_password ? QIcon::fromTheme(QStringLiteral("lock")).pixmap(16) : QIcon();
45 }
46 if (role != Qt::DisplayRole) {
47 return LobbyItem::data(role);
48 }
49 return data(NameRole).toString();
50 }
51
52 bool operator<(const QStandardItem& other) const override {
53 return data(NameRole).toString().localeAwareCompare(other.data(NameRole).toString()) < 0;
54 }
55};
56
57class LobbyItemDescription : public LobbyItem {
58public:
59 static const int DescriptionRole = Qt::UserRole + 1;
60
61 LobbyItemDescription() = default;
62 explicit LobbyItemDescription(QString description) {
63 setData(description, DescriptionRole);
64 }
65
66 QVariant data(int role) const override {
67 if (role != Qt::DisplayRole) {
68 return LobbyItem::data(role);
69 }
70 auto description = data(DescriptionRole).toString();
71 description.prepend(QStringLiteral("Description: "));
72 return description;
73 }
74
75 bool operator<(const QStandardItem& other) const override {
76 return data(DescriptionRole)
77 .toString()
78 .localeAwareCompare(other.data(DescriptionRole).toString()) < 0;
79 }
80};
81
82class LobbyItemGame : public LobbyItem {
83public:
84 static const int TitleIDRole = Qt::UserRole + 1;
85 static const int GameNameRole = Qt::UserRole + 2;
86 static const int GameIconRole = Qt::UserRole + 3;
87
88 LobbyItemGame() = default;
89 explicit LobbyItemGame(u64 title_id, QString game_name, QPixmap smdh_icon) {
90 setData(static_cast<unsigned long long>(title_id), TitleIDRole);
91 setData(game_name, GameNameRole);
92 if (!smdh_icon.isNull()) {
93 setData(smdh_icon, GameIconRole);
94 }
95 }
96
97 QVariant data(int role) const override {
98 if (role == Qt::DecorationRole) {
99 auto val = data(GameIconRole);
100 if (val.isValid()) {
101 val = val.value<QPixmap>().scaled(16, 16, Qt::KeepAspectRatio);
102 }
103 return val;
104 } else if (role != Qt::DisplayRole) {
105 return LobbyItem::data(role);
106 }
107 return data(GameNameRole).toString();
108 }
109
110 bool operator<(const QStandardItem& other) const override {
111 return data(GameNameRole)
112 .toString()
113 .localeAwareCompare(other.data(GameNameRole).toString()) < 0;
114 }
115};
116
117class LobbyItemHost : public LobbyItem {
118public:
119 static const int HostUsernameRole = Qt::UserRole + 1;
120 static const int HostIPRole = Qt::UserRole + 2;
121 static const int HostPortRole = Qt::UserRole + 3;
122 static const int HostVerifyUIDRole = Qt::UserRole + 4;
123
124 LobbyItemHost() = default;
125 explicit LobbyItemHost(QString username, QString ip, u16 port, QString verify_uid) {
126 setData(username, HostUsernameRole);
127 setData(ip, HostIPRole);
128 setData(port, HostPortRole);
129 setData(verify_uid, HostVerifyUIDRole);
130 }
131
132 QVariant data(int role) const override {
133 if (role != Qt::DisplayRole) {
134 return LobbyItem::data(role);
135 }
136 return data(HostUsernameRole).toString();
137 }
138
139 bool operator<(const QStandardItem& other) const override {
140 return data(HostUsernameRole)
141 .toString()
142 .localeAwareCompare(other.data(HostUsernameRole).toString()) < 0;
143 }
144};
145
146class LobbyMember {
147public:
148 LobbyMember() = default;
149 LobbyMember(const LobbyMember& other) = default;
150 explicit LobbyMember(QString username_, QString nickname_, u64 title_id_, QString game_name_)
151 : username(std::move(username_)), nickname(std::move(nickname_)), title_id(title_id_),
152 game_name(std::move(game_name_)) {}
153 ~LobbyMember() = default;
154
155 QString GetName() const {
156 if (username.isEmpty() || username == nickname) {
157 return nickname;
158 } else {
159 return QStringLiteral("%1 (%2)").arg(nickname, username);
160 }
161 }
162 u64 GetTitleId() const {
163 return title_id;
164 }
165 QString GetGameName() const {
166 return game_name;
167 }
168
169private:
170 QString username;
171 QString nickname;
172 u64 title_id;
173 QString game_name;
174};
175
176Q_DECLARE_METATYPE(LobbyMember);
177
178class LobbyItemMemberList : public LobbyItem {
179public:
180 static const int MemberListRole = Qt::UserRole + 1;
181 static const int MaxPlayerRole = Qt::UserRole + 2;
182
183 LobbyItemMemberList() = default;
184 explicit LobbyItemMemberList(QList<QVariant> members, u32 max_players) {
185 setData(members, MemberListRole);
186 setData(max_players, MaxPlayerRole);
187 }
188
189 QVariant data(int role) const override {
190 if (role != Qt::DisplayRole) {
191 return LobbyItem::data(role);
192 }
193 auto members = data(MemberListRole).toList();
194 return QStringLiteral("%1 / %2").arg(QString::number(members.size()),
195 data(MaxPlayerRole).toString());
196 }
197
198 bool operator<(const QStandardItem& other) const override {
199 // sort by rooms that have the most players
200 int left_members = data(MemberListRole).toList().size();
201 int right_members = other.data(MemberListRole).toList().size();
202 return left_members < right_members;
203 }
204};
205
206/**
207 * Member information for when a lobby is expanded in the UI
208 */
209class LobbyItemExpandedMemberList : public LobbyItem {
210public:
211 static const int MemberListRole = Qt::UserRole + 1;
212
213 LobbyItemExpandedMemberList() = default;
214 explicit LobbyItemExpandedMemberList(QList<QVariant> members) {
215 setData(members, MemberListRole);
216 }
217
218 QVariant data(int role) const override {
219 if (role != Qt::DisplayRole) {
220 return LobbyItem::data(role);
221 }
222 auto members = data(MemberListRole).toList();
223 QString out;
224 bool first = true;
225 for (const auto& member : members) {
226 if (!first)
227 out.append(QStringLiteral("\n"));
228 const auto& m = member.value<LobbyMember>();
229 if (m.GetGameName().isEmpty()) {
230 out += QString(QObject::tr("%1 is not playing a game")).arg(m.GetName());
231 } else {
232 out += QString(QObject::tr("%1 is playing %2")).arg(m.GetName(), m.GetGameName());
233 }
234 first = false;
235 }
236 return out;
237 }
238};
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <QMessageBox>
5#include <QString>
6
7#include "yuzu/multiplayer/message.h"
8
9namespace NetworkMessage {
10const ConnectionError ErrorManager::USERNAME_NOT_VALID(
11 QT_TR_NOOP("Username is not valid. Must be 4 to 20 alphanumeric characters."));
12const ConnectionError ErrorManager::ROOMNAME_NOT_VALID(
13 QT_TR_NOOP("Room name is not valid. Must be 4 to 20 alphanumeric characters."));
14const ConnectionError ErrorManager::USERNAME_NOT_VALID_SERVER(
15 QT_TR_NOOP("Username is already in use or not valid. Please choose another."));
16const ConnectionError ErrorManager::IP_ADDRESS_NOT_VALID(
17 QT_TR_NOOP("IP is not a valid IPv4 address."));
18const ConnectionError ErrorManager::PORT_NOT_VALID(
19 QT_TR_NOOP("Port must be a number between 0 to 65535."));
20const ConnectionError ErrorManager::GAME_NOT_SELECTED(QT_TR_NOOP(
21 "You must choose a Preferred Game to host a room. If you do not have any games in your game "
22 "list yet, add a game folder by clicking on the plus icon in the game list."));
23const ConnectionError ErrorManager::NO_INTERNET(
24 QT_TR_NOOP("Unable to find an internet connection. Check your internet settings."));
25const ConnectionError ErrorManager::UNABLE_TO_CONNECT(
26 QT_TR_NOOP("Unable to connect to the host. Verify that the connection settings are correct. If "
27 "you still cannot connect, contact the room host and verify that the host is "
28 "properly configured with the external port forwarded."));
29const ConnectionError ErrorManager::ROOM_IS_FULL(
30 QT_TR_NOOP("Unable to connect to the room because it is already full."));
31const ConnectionError ErrorManager::COULD_NOT_CREATE_ROOM(
32 QT_TR_NOOP("Creating a room failed. Please retry. Restarting yuzu might be necessary."));
33const ConnectionError ErrorManager::HOST_BANNED(
34 QT_TR_NOOP("The host of the room has banned you. Speak with the host to unban you "
35 "or try a different room."));
36const ConnectionError ErrorManager::WRONG_VERSION(
37 QT_TR_NOOP("Version mismatch! Please update to the latest version of yuzu. If the problem "
38 "persists, contact the room host and ask them to update the server."));
39const ConnectionError ErrorManager::WRONG_PASSWORD(QT_TR_NOOP("Incorrect password."));
40const ConnectionError ErrorManager::GENERIC_ERROR(QT_TR_NOOP(
41 "An unknown error occurred. If this error continues to occur, please open an issue"));
42const ConnectionError ErrorManager::LOST_CONNECTION(
43 QT_TR_NOOP("Connection to room lost. Try to reconnect."));
44const ConnectionError ErrorManager::HOST_KICKED(
45 QT_TR_NOOP("You have been kicked by the room host."));
46const ConnectionError ErrorManager::MAC_COLLISION(
47 QT_TR_NOOP("MAC address is already in use. Please choose another."));
48const ConnectionError ErrorManager::CONSOLE_ID_COLLISION(QT_TR_NOOP(
49 "Your Console ID conflicted with someone else's in the room.\n\nPlease go to Emulation "
50 "> Configure > System to regenerate your Console ID."));
51const ConnectionError ErrorManager::PERMISSION_DENIED(
52 QT_TR_NOOP("You do not have enough permission to perform this action."));
53const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP(
54 "The user you are trying to kick/ban could not be found.\nThey may have left the room."));
55
56static bool WarnMessage(const std::string& title, const std::string& text) {
57 return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),
58 QObject::tr(text.c_str()),
59 QMessageBox::Ok | QMessageBox::Cancel);
60}
61
62void ErrorManager::ShowError(const ConnectionError& e) {
63 QMessageBox::critical(nullptr, tr("Error"), tr(e.GetString().c_str()));
64}
65
66bool WarnCloseRoom() {
67 return WarnMessage(
68 QT_TR_NOOP("Leave Room"),
69 QT_TR_NOOP("You are about to close the room. Any network connections will be closed."));
70}
71
72bool WarnDisconnect() {
73 return WarnMessage(
74 QT_TR_NOOP("Disconnect"),
75 QT_TR_NOOP("You are about to leave the room. Any network connections will be closed."));
76}
77
78} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <utility>
7
8namespace NetworkMessage {
9
10class ConnectionError {
11
12public:
13 explicit ConnectionError(std::string str) : err(std::move(str)) {}
14 const std::string& GetString() const {
15 return err;
16 }
17
18private:
19 std::string err;
20};
21
22class ErrorManager : QObject {
23 Q_OBJECT
24public:
25 /// When the nickname is considered invalid by the client
26 static const ConnectionError USERNAME_NOT_VALID;
27 static const ConnectionError ROOMNAME_NOT_VALID;
28 /// When the nickname is considered invalid by the room server
29 static const ConnectionError USERNAME_NOT_VALID_SERVER;
30 static const ConnectionError IP_ADDRESS_NOT_VALID;
31 static const ConnectionError PORT_NOT_VALID;
32 static const ConnectionError GAME_NOT_SELECTED;
33 static const ConnectionError NO_INTERNET;
34 static const ConnectionError UNABLE_TO_CONNECT;
35 static const ConnectionError ROOM_IS_FULL;
36 static const ConnectionError COULD_NOT_CREATE_ROOM;
37 static const ConnectionError HOST_BANNED;
38 static const ConnectionError WRONG_VERSION;
39 static const ConnectionError WRONG_PASSWORD;
40 static const ConnectionError GENERIC_ERROR;
41 static const ConnectionError LOST_CONNECTION;
42 static const ConnectionError HOST_KICKED;
43 static const ConnectionError MAC_COLLISION;
44 static const ConnectionError CONSOLE_ID_COLLISION;
45 static const ConnectionError PERMISSION_DENIED;
46 static const ConnectionError NO_SUCH_USER;
47 /**
48 * Shows a standard QMessageBox with a error message
49 */
50 static void ShowError(const ConnectionError& e);
51};
52/**
53 * Show a standard QMessageBox with a warning message about leaving the room
54 * return true if the user wants to close the network connection
55 */
56bool WarnCloseRoom();
57
58/**
59 * Show a standard QMessageBox with a warning message about disconnecting from the room
60 * return true if the user wants to disconnect
61 */
62bool WarnDisconnect();
63
64} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <QStandardItem>
5#include <QStandardItemModel>
6#include "network/network.h"
7#include "network/room_member.h"
8#include "ui_moderation_dialog.h"
9#include "yuzu/multiplayer/moderation_dialog.h"
10
11namespace Column {
12enum {
13 SUBJECT,
14 TYPE,
15 COUNT,
16};
17}
18
19ModerationDialog::ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent)
20 : QDialog(parent), ui(std::make_unique<Ui::ModerationDialog>()), room_network{room_network_} {
21 ui->setupUi(this);
22
23 qRegisterMetaType<Network::Room::BanList>();
24
25 if (auto member = room_network.GetRoomMember().lock()) {
26 callback_handle_status_message = member->BindOnStatusMessageReceived(
27 [this](const Network::StatusMessageEntry& status_message) {
28 emit StatusMessageReceived(status_message);
29 });
30 connect(this, &ModerationDialog::StatusMessageReceived, this,
31 &ModerationDialog::OnStatusMessageReceived);
32 callback_handle_ban_list = member->BindOnBanListReceived(
33 [this](const Network::Room::BanList& ban_list) { emit BanListReceived(ban_list); });
34 connect(this, &ModerationDialog::BanListReceived, this, &ModerationDialog::PopulateBanList);
35 }
36
37 // Initialize the UI
38 model = new QStandardItemModel(ui->ban_list_view);
39 model->insertColumns(0, Column::COUNT);
40 model->setHeaderData(Column::SUBJECT, Qt::Horizontal, tr("Subject"));
41 model->setHeaderData(Column::TYPE, Qt::Horizontal, tr("Type"));
42
43 ui->ban_list_view->setModel(model);
44
45 // Load the ban list in background
46 LoadBanList();
47
48 connect(ui->refresh, &QPushButton::clicked, this, [this] { LoadBanList(); });
49 connect(ui->unban, &QPushButton::clicked, this, [this] {
50 auto index = ui->ban_list_view->currentIndex();
51 SendUnbanRequest(model->item(index.row(), 0)->text());
52 });
53 connect(ui->ban_list_view, &QTreeView::clicked, [this] { ui->unban->setEnabled(true); });
54}
55
56ModerationDialog::~ModerationDialog() {
57 if (callback_handle_status_message) {
58 if (auto room = room_network.GetRoomMember().lock()) {
59 room->Unbind(callback_handle_status_message);
60 }
61 }
62
63 if (callback_handle_ban_list) {
64 if (auto room = room_network.GetRoomMember().lock()) {
65 room->Unbind(callback_handle_ban_list);
66 }
67 }
68}
69
70void ModerationDialog::LoadBanList() {
71 if (auto room = room_network.GetRoomMember().lock()) {
72 ui->refresh->setEnabled(false);
73 ui->refresh->setText(tr("Refreshing"));
74 ui->unban->setEnabled(false);
75 room->RequestBanList();
76 }
77}
78
79void ModerationDialog::PopulateBanList(const Network::Room::BanList& ban_list) {
80 model->removeRows(0, model->rowCount());
81 for (const auto& username : ban_list.first) {
82 QStandardItem* subject_item = new QStandardItem(QString::fromStdString(username));
83 QStandardItem* type_item = new QStandardItem(tr("Forum Username"));
84 model->invisibleRootItem()->appendRow({subject_item, type_item});
85 }
86 for (const auto& ip : ban_list.second) {
87 QStandardItem* subject_item = new QStandardItem(QString::fromStdString(ip));
88 QStandardItem* type_item = new QStandardItem(tr("IP Address"));
89 model->invisibleRootItem()->appendRow({subject_item, type_item});
90 }
91 for (int i = 0; i < Column::COUNT - 1; ++i) {
92 ui->ban_list_view->resizeColumnToContents(i);
93 }
94 ui->refresh->setEnabled(true);
95 ui->refresh->setText(tr("Refresh"));
96 ui->unban->setEnabled(false);
97}
98
99void ModerationDialog::SendUnbanRequest(const QString& subject) {
100 if (auto room = room_network.GetRoomMember().lock()) {
101 room->SendModerationRequest(Network::IdModUnban, subject.toStdString());
102 }
103}
104
105void ModerationDialog::OnStatusMessageReceived(const Network::StatusMessageEntry& status_message) {
106 if (status_message.type != Network::IdMemberBanned &&
107 status_message.type != Network::IdAddressUnbanned)
108 return;
109
110 // Update the ban list for ban/unban
111 LoadBanList();
112}
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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <memory>
7#include <optional>
8#include <QDialog>
9#include "network/room.h"
10#include "network/room_member.h"
11
12namespace Ui {
13class ModerationDialog;
14}
15
16class QStandardItemModel;
17
18class ModerationDialog : public QDialog {
19 Q_OBJECT
20
21public:
22 explicit ModerationDialog(Network::RoomNetwork& room_network_, QWidget* parent = nullptr);
23 ~ModerationDialog();
24
25signals:
26 void StatusMessageReceived(const Network::StatusMessageEntry&);
27 void BanListReceived(const Network::Room::BanList&);
28
29private:
30 void LoadBanList();
31 void PopulateBanList(const Network::Room::BanList& ban_list);
32 void SendUnbanRequest(const QString& subject);
33 void OnStatusMessageReceived(const Network::StatusMessageEntry& status_message);
34
35 std::unique_ptr<Ui::ModerationDialog> ui;
36 QStandardItemModel* model;
37 Network::RoomMember::CallbackHandle<Network::StatusMessageEntry> callback_handle_status_message;
38 Network::RoomMember::CallbackHandle<Network::Room::BanList> callback_handle_ban_list;
39
40 Network::RoomNetwork& room_network;
41};
42
43Q_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 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>ModerationDialog</class>
4 <widget class="QDialog" name="ModerationDialog">
5 <property name="windowTitle">
6 <string>Moderation</string>
7 </property>
8 <property name="geometry">
9 <rect>
10 <x>0</x>
11 <y>0</y>
12 <width>500</width>
13 <height>300</height>
14 </rect>
15 </property>
16 <layout class="QVBoxLayout">
17 <item>
18 <widget class="QGroupBox" name="ban_list_group_box">
19 <property name="title">
20 <string>Ban List</string>
21 </property>
22 <layout class="QVBoxLayout">
23 <item>
24 <layout class="QHBoxLayout">
25 <item>
26 <spacer name="horizontalSpacer">
27 <property name="orientation">
28 <enum>Qt::Horizontal</enum>
29 </property>
30 <property name="sizeHint" stdset="0">
31 <size>
32 <width>40</width>
33 <height>20</height>
34 </size>
35 </property>
36 </spacer>
37 </item>
38 <item>
39 <widget class="QPushButton" name="refresh">
40 <property name="text">
41 <string>Refreshing</string>
42 </property>
43 <property name="enabled">
44 <bool>false</bool>
45 </property>
46 </widget>
47 </item>
48 <item>
49 <widget class="QPushButton" name="unban">
50 <property name="text">
51 <string>Unban</string>
52 </property>
53 <property name="enabled">
54 <bool>false</bool>
55 </property>
56 </widget>
57 </item>
58 </layout>
59 </item>
60 <item>
61 <widget class="QTreeView" name="ban_list_view"/>
62 </item>
63 </layout>
64 </widget>
65 </item>
66 <item>
67 <widget class="QDialogButtonBox" name="buttonBox">
68 <property name="standardButtons">
69 <set>QDialogButtonBox::Ok</set>
70 </property>
71 </widget>
72 </item>
73 </layout>
74 </widget>
75 <connections>
76 <connection>
77 <sender>buttonBox</sender>
78 <signal>accepted()</signal>
79 <receiver>ModerationDialog</receiver>
80 <slot>accept()</slot>
81 </connection>
82 </connections>
83 <resources/>
84</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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <QAction>
5#include <QApplication>
6#include <QIcon>
7#include <QMessageBox>
8#include <QStandardItemModel>
9#include "common/announce_multiplayer_room.h"
10#include "common/logging/log.h"
11#include "yuzu/game_list.h"
12#include "yuzu/multiplayer/client_room.h"
13#include "yuzu/multiplayer/direct_connect.h"
14#include "yuzu/multiplayer/host_room.h"
15#include "yuzu/multiplayer/lobby.h"
16#include "yuzu/multiplayer/message.h"
17#include "yuzu/multiplayer/state.h"
18#include "yuzu/uisettings.h"
19#include "yuzu/util/clickable_label.h"
20
21MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_list_model_,
22 QAction* leave_room_, QAction* show_room_,
23 Network::RoomNetwork& room_network_)
24 : QWidget(parent), game_list_model(game_list_model_), leave_room(leave_room_),
25 show_room(show_room_), room_network{room_network_} {
26 if (auto member = room_network.GetRoomMember().lock()) {
27 // register the network structs to use in slots and signals
28 state_callback_handle = member->BindOnStateChanged(
29 [this](const Network::RoomMember::State& state) { emit NetworkStateChanged(state); });
30 connect(this, &MultiplayerState::NetworkStateChanged, this,
31 &MultiplayerState::OnNetworkStateChanged);
32 error_callback_handle = member->BindOnError(
33 [this](const Network::RoomMember::Error& error) { emit NetworkError(error); });
34 connect(this, &MultiplayerState::NetworkError, this, &MultiplayerState::OnNetworkError);
35 }
36
37 qRegisterMetaType<Network::RoomMember::State>();
38 qRegisterMetaType<Network::RoomMember::Error>();
39 qRegisterMetaType<WebService::WebResult>();
40 announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>(room_network);
41 announce_multiplayer_session->BindErrorCallback(
42 [this](const WebService::WebResult& result) { emit AnnounceFailed(result); });
43 connect(this, &MultiplayerState::AnnounceFailed, this, &MultiplayerState::OnAnnounceFailed);
44
45 status_text = new ClickableLabel(this);
46 status_icon = new ClickableLabel(this);
47 status_text->setToolTip(tr("Current connection status"));
48 status_text->setText(tr("Not Connected. Click here to find a room!"));
49 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
50
51 connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
52 connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
53
54 connect(static_cast<QApplication*>(QApplication::instance()), &QApplication::focusChanged, this,
55 [this](QWidget* /*old*/, QWidget* now) {
56 if (client_room && client_room->isAncestorOf(now)) {
57 HideNotification();
58 }
59 });
60}
61
62MultiplayerState::~MultiplayerState() {
63 if (state_callback_handle) {
64 if (auto member = room_network.GetRoomMember().lock()) {
65 member->Unbind(state_callback_handle);
66 }
67 }
68
69 if (error_callback_handle) {
70 if (auto member = room_network.GetRoomMember().lock()) {
71 member->Unbind(error_callback_handle);
72 }
73 }
74}
75
76void MultiplayerState::Close() {
77 if (host_room) {
78 host_room->close();
79 }
80 if (direct_connect) {
81 direct_connect->close();
82 }
83 if (client_room) {
84 client_room->close();
85 }
86 if (lobby) {
87 lobby->close();
88 }
89}
90
91void MultiplayerState::retranslateUi() {
92 status_text->setToolTip(tr("Current connection status"));
93
94 if (current_state == Network::RoomMember::State::Uninitialized) {
95 status_text->setText(tr("Not Connected. Click here to find a room!"));
96 } else if (current_state == Network::RoomMember::State::Joined ||
97 current_state == Network::RoomMember::State::Moderator) {
98
99 status_text->setText(tr("Connected"));
100 } else {
101 status_text->setText(tr("Not Connected"));
102 }
103
104 if (lobby) {
105 lobby->RetranslateUi();
106 }
107 if (host_room) {
108 host_room->RetranslateUi();
109 }
110 if (client_room) {
111 client_room->RetranslateUi();
112 }
113 if (direct_connect) {
114 direct_connect->RetranslateUi();
115 }
116}
117
118void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
119 LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
120 if (state == Network::RoomMember::State::Joined ||
121 state == Network::RoomMember::State::Moderator) {
122
123 OnOpenNetworkRoom();
124 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
125 status_text->setText(tr("Connected"));
126 leave_room->setEnabled(true);
127 show_room->setEnabled(true);
128 } else {
129 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
130 status_text->setText(tr("Not Connected"));
131 leave_room->setEnabled(false);
132 show_room->setEnabled(false);
133 }
134
135 current_state = state;
136}
137
138void MultiplayerState::OnNetworkError(const Network::RoomMember::Error& error) {
139 LOG_DEBUG(Frontend, "Network Error: {}", Network::GetErrorStr(error));
140 switch (error) {
141 case Network::RoomMember::Error::LostConnection:
142 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::LOST_CONNECTION);
143 break;
144 case Network::RoomMember::Error::HostKicked:
145 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_KICKED);
146 break;
147 case Network::RoomMember::Error::CouldNotConnect:
148 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT);
149 break;
150 case Network::RoomMember::Error::NameCollision:
151 NetworkMessage::ErrorManager::ShowError(
152 NetworkMessage::ErrorManager::USERNAME_NOT_VALID_SERVER);
153 break;
154 case Network::RoomMember::Error::MacCollision:
155 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::MAC_COLLISION);
156 break;
157 case Network::RoomMember::Error::ConsoleIdCollision:
158 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::CONSOLE_ID_COLLISION);
159 break;
160 case Network::RoomMember::Error::RoomIsFull:
161 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::ROOM_IS_FULL);
162 break;
163 case Network::RoomMember::Error::WrongPassword:
164 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_PASSWORD);
165 break;
166 case Network::RoomMember::Error::WrongVersion:
167 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::WRONG_VERSION);
168 break;
169 case Network::RoomMember::Error::HostBanned:
170 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::HOST_BANNED);
171 break;
172 case Network::RoomMember::Error::UnknownError:
173 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::UNABLE_TO_CONNECT);
174 break;
175 case Network::RoomMember::Error::PermissionDenied:
176 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::PERMISSION_DENIED);
177 break;
178 case Network::RoomMember::Error::NoSuchUser:
179 NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::NO_SUCH_USER);
180 break;
181 }
182}
183
184void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) {
185 announce_multiplayer_session->Stop();
186 QMessageBox::warning(this, tr("Error"),
187 tr("Failed to update the room information. Please check your Internet "
188 "connection and try hosting the room again.\nDebug Message: ") +
189 QString::fromStdString(result.result_string),
190 QMessageBox::Ok);
191}
192
193void MultiplayerState::UpdateThemedIcons() {
194 if (show_notification) {
195 status_icon->setPixmap(
196 QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
197 } else if (current_state == Network::RoomMember::State::Joined ||
198 current_state == Network::RoomMember::State::Moderator) {
199
200 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
201 } else {
202 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
203 }
204 if (client_room)
205 client_room->UpdateIconDisplay();
206}
207
208static void BringWidgetToFront(QWidget* widget) {
209 widget->show();
210 widget->activateWindow();
211 widget->raise();
212}
213
214void MultiplayerState::OnViewLobby() {
215 if (lobby == nullptr) {
216 lobby = new Lobby(this, game_list_model, announce_multiplayer_session, room_network);
217 }
218 BringWidgetToFront(lobby);
219}
220
221void MultiplayerState::OnCreateRoom() {
222 if (host_room == nullptr) {
223 host_room =
224 new HostRoomWindow(this, game_list_model, announce_multiplayer_session, room_network);
225 }
226 BringWidgetToFront(host_room);
227}
228
229bool MultiplayerState::OnCloseRoom() {
230 if (!NetworkMessage::WarnCloseRoom())
231 return false;
232 if (auto room = room_network.GetRoom().lock()) {
233 // if you are in a room, leave it
234 if (auto member = room_network.GetRoomMember().lock()) {
235 member->Leave();
236 LOG_DEBUG(Frontend, "Left the room (as a client)");
237 }
238
239 // if you are hosting a room, also stop hosting
240 if (room->GetState() != Network::Room::State::Open) {
241 return true;
242 }
243 // Save ban list
244 UISettings::values.multiplayer_ban_list = std::move(room->GetBanList());
245
246 room->Destroy();
247 announce_multiplayer_session->Stop();
248 LOG_DEBUG(Frontend, "Closed the room (as a server)");
249 }
250 return true;
251}
252
253void MultiplayerState::ShowNotification() {
254 if (client_room && client_room->isAncestorOf(QApplication::focusWidget()))
255 return; // Do not show notification if the chat window currently has focus
256 show_notification = true;
257 QApplication::alert(nullptr);
258 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
259 status_text->setText(tr("New Messages Received"));
260}
261
262void MultiplayerState::HideNotification() {
263 show_notification = false;
264 status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
265 status_text->setText(tr("Connected"));
266}
267
268void MultiplayerState::OnOpenNetworkRoom() {
269 if (auto member = room_network.GetRoomMember().lock()) {
270 if (member->IsConnected()) {
271 if (client_room == nullptr) {
272 client_room = new ClientRoomWindow(this, room_network);
273 connect(client_room, &ClientRoomWindow::ShowNotification, this,
274 &MultiplayerState::ShowNotification);
275 }
276 BringWidgetToFront(client_room);
277 return;
278 }
279 }
280 // If the user is not a member of a room, show the lobby instead.
281 // This is currently only used on the clickable label in the status bar
282 OnViewLobby();
283}
284
285void MultiplayerState::OnDirectConnectToRoom() {
286 if (direct_connect == nullptr) {
287 direct_connect = new DirectConnectWindow(room_network, this);
288 }
289 BringWidgetToFront(direct_connect);
290}
291
292bool MultiplayerState::IsHostingPublicRoom() const {
293 return announce_multiplayer_session->IsRunning();
294}
295
296void MultiplayerState::UpdateCredentials() {
297 announce_multiplayer_session->UpdateCredentials();
298}
299
300void MultiplayerState::UpdateGameList(QStandardItemModel* game_list) {
301 game_list_model = game_list;
302 if (lobby) {
303 lobby->UpdateGameList(game_list);
304 }
305 if (host_room) {
306 host_room->UpdateGameList(game_list);
307 }
308}
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 @@
1// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <QWidget>
7#include "core/announce_multiplayer_session.h"
8#include "network/network.h"
9
10class QStandardItemModel;
11class Lobby;
12class HostRoomWindow;
13class ClientRoomWindow;
14class DirectConnectWindow;
15class ClickableLabel;
16
17class MultiplayerState : public QWidget {
18 Q_OBJECT;
19
20public:
21 explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
22 QAction* show_room, Network::RoomNetwork& room_network_);
23 ~MultiplayerState();
24
25 /**
26 * Close all open multiplayer related dialogs
27 */
28 void Close();
29
30 ClickableLabel* GetStatusText() const {
31 return status_text;
32 }
33
34 ClickableLabel* GetStatusIcon() const {
35 return status_icon;
36 }
37
38 void retranslateUi();
39
40 /**
41 * Whether a public room is being hosted or not.
42 * When this is true, Web Services configuration should be disabled.
43 */
44 bool IsHostingPublicRoom() const;
45
46 void UpdateCredentials();
47
48 /**
49 * Updates the multiplayer dialogs with a new game list model.
50 * This model should be the original model of the game list.
51 */
52 void UpdateGameList(QStandardItemModel* game_list);
53
54public slots:
55 void OnNetworkStateChanged(const Network::RoomMember::State& state);
56 void OnNetworkError(const Network::RoomMember::Error& error);
57 void OnViewLobby();
58 void OnCreateRoom();
59 bool OnCloseRoom();
60 void OnOpenNetworkRoom();
61 void OnDirectConnectToRoom();
62 void OnAnnounceFailed(const WebService::WebResult&);
63 void UpdateThemedIcons();
64 void ShowNotification();
65 void HideNotification();
66
67signals:
68 void NetworkStateChanged(const Network::RoomMember::State&);
69 void NetworkError(const Network::RoomMember::Error&);
70 void AnnounceFailed(const WebService::WebResult&);
71
72private:
73 Lobby* lobby = nullptr;
74 HostRoomWindow* host_room = nullptr;
75 ClientRoomWindow* client_room = nullptr;
76 DirectConnectWindow* direct_connect = nullptr;
77 ClickableLabel* status_icon = nullptr;
78 ClickableLabel* status_text = nullptr;
79 QStandardItemModel* game_list_model = nullptr;
80 QAction* leave_room;
81 QAction* show_room;
82 std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
83 Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized;
84 bool has_mod_perms = false;
85 Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle;
86 Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle;
87
88 bool show_notification = false;
89 Network::RoomNetwork& room_network;
90};
91
92Q_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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <QRegExp>
7#include <QString>
8#include <QValidator>
9
10class Validation {
11public:
12 Validation()
13 : room_name(room_name_regex), nickname(nickname_regex), ip(ip_regex), port(0, 65535) {}
14
15 ~Validation() = default;
16
17 const QValidator* GetRoomName() const {
18 return &room_name;
19 }
20 const QValidator* GetNickname() const {
21 return &nickname;
22 }
23 const QValidator* GetIP() const {
24 return &ip;
25 }
26 const QValidator* GetPort() const {
27 return &port;
28 }
29
30private:
31 /// room name can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20
32 QRegExp room_name_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$"));
33 QRegExpValidator room_name;
34
35 /// nickname can be alphanumeric and " " "_" "." and "-" and must have a size of 4-20
36 QRegExp nickname_regex = QRegExp(QStringLiteral("^[a-zA-Z0-9._- ]{4,20}$"));
37 QRegExpValidator nickname;
38
39 /// ipv4 address only
40 // TODO remove this when we support hostnames in direct connect
41 QRegExp ip_regex = QRegExp(QStringLiteral(
42 "(([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}|"
43 "2[0-4][0-9]|25[0-5])"));
44 QRegExpValidator ip;
45
46 /// port must be between 0 and 65535
47 QIntValidator port;
48};
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h
index 2f6948243..6cd4d6cb2 100644
--- a/src/yuzu/uisettings.h
+++ b/src/yuzu/uisettings.h
@@ -102,6 +102,19 @@ struct Values {
102 102
103 Settings::Setting<uint32_t> callout_flags{0, "calloutFlags"}; 103 Settings::Setting<uint32_t> callout_flags{0, "calloutFlags"};
104 104
105 // multiplayer settings
106 Settings::Setting<QString> multiplayer_nickname{QStringLiteral("yuzu"), "nickname"};
107 Settings::Setting<QString> multiplayer_ip{{}, "ip"};
108 Settings::SwitchableSetting<uint, true> multiplayer_port{24872, 0, 65535, "port"};
109 Settings::Setting<QString> multiplayer_room_nickname{{}, "room_nickname"};
110 Settings::Setting<QString> multiplayer_room_name{{}, "room_name"};
111 Settings::SwitchableSetting<uint, true> multiplayer_max_player{8, 0, 8, "max_player"};
112 Settings::SwitchableSetting<uint, true> multiplayer_room_port{24872, 0, 65535, "room_port"};
113 Settings::SwitchableSetting<uint, true> multiplayer_host_type{0, 0, 1, "host_type"};
114 Settings::Setting<qulonglong> multiplayer_game_id{{}, "game_id"};
115 Settings::Setting<QString> multiplayer_room_description{{}, "room_description"};
116 std::pair<std::vector<std::string>, std::vector<std::string>> multiplayer_ban_list;
117
105 // logging 118 // logging
106 Settings::Setting<bool> show_console{false, "showConsole"}; 119 Settings::Setting<bool> show_console{false, "showConsole"};
107 120
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "yuzu/util/clickable_label.h"
5
6ClickableLabel::ClickableLabel(QWidget* parent, [[maybe_unused]] Qt::WindowFlags f)
7 : QLabel(parent) {}
8
9void ClickableLabel::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) {
10 emit clicked();
11}
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 @@
1// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <QLabel>
7#include <QWidget>
8
9class ClickableLabel : public QLabel {
10 Q_OBJECT
11
12public:
13 explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
14 ~ClickableLabel() = default;
15
16signals:
17 void clicked();
18
19protected:
20 void mouseReleaseEvent(QMouseEvent* event);
21};
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index cb301e78b..e10d3f5b4 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -5,6 +5,7 @@
5#include <chrono> 5#include <chrono>
6#include <iostream> 6#include <iostream>
7#include <memory> 7#include <memory>
8#include <regex>
8#include <string> 9#include <string>
9#include <thread> 10#include <thread>
10 11
@@ -29,6 +30,7 @@
29#include "core/loader/loader.h" 30#include "core/loader/loader.h"
30#include "core/telemetry_session.h" 31#include "core/telemetry_session.h"
31#include "input_common/main.h" 32#include "input_common/main.h"
33#include "network/network.h"
32#include "video_core/renderer_base.h" 34#include "video_core/renderer_base.h"
33#include "yuzu_cmd/config.h" 35#include "yuzu_cmd/config.h"
34#include "yuzu_cmd/emu_window/emu_window_sdl2.h" 36#include "yuzu_cmd/emu_window/emu_window_sdl2.h"
@@ -60,6 +62,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
60static void PrintHelp(const char* argv0) { 62static void PrintHelp(const char* argv0) {
61 std::cout << "Usage: " << argv0 63 std::cout << "Usage: " << argv0
62 << " [options] <filename>\n" 64 << " [options] <filename>\n"
65 "-m, --multiplayer=nick:password@address:port"
66 " Nickname, password, address and port for multiplayer\n"
63 "-f, --fullscreen Start in fullscreen mode\n" 67 "-f, --fullscreen Start in fullscreen mode\n"
64 "-h, --help Display this help and exit\n" 68 "-h, --help Display this help and exit\n"
65 "-v, --version Output version information and exit\n" 69 "-v, --version Output version information and exit\n"
@@ -71,6 +75,107 @@ static void PrintVersion() {
71 std::cout << "yuzu " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl; 75 std::cout << "yuzu " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl;
72} 76}
73 77
78static void OnStateChanged(const Network::RoomMember::State& state) {
79 switch (state) {
80 case Network::RoomMember::State::Idle:
81 LOG_DEBUG(Network, "Network is idle");
82 break;
83 case Network::RoomMember::State::Joining:
84 LOG_DEBUG(Network, "Connection sequence to room started");
85 break;
86 case Network::RoomMember::State::Joined:
87 LOG_DEBUG(Network, "Successfully joined to the room");
88 break;
89 case Network::RoomMember::State::Moderator:
90 LOG_DEBUG(Network, "Successfully joined the room as a moderator");
91 break;
92 default:
93 break;
94 }
95}
96
97static void OnNetworkError(const Network::RoomMember::Error& error) {
98 switch (error) {
99 case Network::RoomMember::Error::LostConnection:
100 LOG_DEBUG(Network, "Lost connection to the room");
101 break;
102 case Network::RoomMember::Error::CouldNotConnect:
103 LOG_ERROR(Network, "Error: Could not connect");
104 exit(1);
105 break;
106 case Network::RoomMember::Error::NameCollision:
107 LOG_ERROR(
108 Network,
109 "You tried to use the same nickname as another user that is connected to the Room");
110 exit(1);
111 break;
112 case Network::RoomMember::Error::MacCollision:
113 LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is "
114 "connected to the Room");
115 exit(1);
116 break;
117 case Network::RoomMember::Error::ConsoleIdCollision:
118 LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room");
119 exit(1);
120 break;
121 case Network::RoomMember::Error::WrongPassword:
122 LOG_ERROR(Network, "Room replied with: Wrong password");
123 exit(1);
124 break;
125 case Network::RoomMember::Error::WrongVersion:
126 LOG_ERROR(Network,
127 "You are using a different version than the room you are trying to connect to");
128 exit(1);
129 break;
130 case Network::RoomMember::Error::RoomIsFull:
131 LOG_ERROR(Network, "The room is full");
132 exit(1);
133 break;
134 case Network::RoomMember::Error::HostKicked:
135 LOG_ERROR(Network, "You have been kicked by the host");
136 break;
137 case Network::RoomMember::Error::HostBanned:
138 LOG_ERROR(Network, "You have been banned by the host");
139 break;
140 case Network::RoomMember::Error::UnknownError:
141 LOG_ERROR(Network, "UnknownError");
142 break;
143 case Network::RoomMember::Error::PermissionDenied:
144 LOG_ERROR(Network, "PermissionDenied");
145 break;
146 case Network::RoomMember::Error::NoSuchUser:
147 LOG_ERROR(Network, "NoSuchUser");
148 break;
149 }
150}
151
152static void OnMessageReceived(const Network::ChatEntry& msg) {
153 std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl;
154}
155
156static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) {
157 std::string message;
158 switch (msg.type) {
159 case Network::IdMemberJoin:
160 message = fmt::format("{} has joined", msg.nickname);
161 break;
162 case Network::IdMemberLeave:
163 message = fmt::format("{} has left", msg.nickname);
164 break;
165 case Network::IdMemberKicked:
166 message = fmt::format("{} has been kicked", msg.nickname);
167 break;
168 case Network::IdMemberBanned:
169 message = fmt::format("{} has been banned", msg.nickname);
170 break;
171 case Network::IdAddressUnbanned:
172 message = fmt::format("{} has been unbanned", msg.nickname);
173 break;
174 }
175 if (!message.empty())
176 std::cout << std::endl << "* " << message << std::endl << std::endl;
177}
178
74/// Application entry point 179/// Application entry point
75int main(int argc, char** argv) { 180int main(int argc, char** argv) {
76 Common::Log::Initialize(); 181 Common::Log::Initialize();
@@ -92,10 +197,16 @@ int main(int argc, char** argv) {
92 std::optional<std::string> config_path; 197 std::optional<std::string> config_path;
93 std::string program_args; 198 std::string program_args;
94 199
200 bool use_multiplayer = false;
95 bool fullscreen = false; 201 bool fullscreen = false;
202 std::string nickname{};
203 std::string password{};
204 std::string address{};
205 u16 port = Network::DefaultRoomPort;
96 206
97 static struct option long_options[] = { 207 static struct option long_options[] = {
98 // clang-format off 208 // clang-format off
209 {"multiplayer", required_argument, 0, 'm'},
99 {"fullscreen", no_argument, 0, 'f'}, 210 {"fullscreen", no_argument, 0, 'f'},
100 {"help", no_argument, 0, 'h'}, 211 {"help", no_argument, 0, 'h'},
101 {"version", no_argument, 0, 'v'}, 212 {"version", no_argument, 0, 'v'},
@@ -109,6 +220,38 @@ int main(int argc, char** argv) {
109 int arg = getopt_long(argc, argv, "g:fhvp::c:", long_options, &option_index); 220 int arg = getopt_long(argc, argv, "g:fhvp::c:", long_options, &option_index);
110 if (arg != -1) { 221 if (arg != -1) {
111 switch (static_cast<char>(arg)) { 222 switch (static_cast<char>(arg)) {
223 case 'm': {
224 use_multiplayer = true;
225 const std::string str_arg(optarg);
226 // regex to check if the format is nickname:password@ip:port
227 // with optional :password
228 const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$");
229 if (!std::regex_match(str_arg, re)) {
230 std::cout << "Wrong format for option --multiplayer\n";
231 PrintHelp(argv[0]);
232 return 0;
233 }
234
235 std::smatch match;
236 std::regex_search(str_arg, match, re);
237 ASSERT(match.size() == 5);
238 nickname = match[1];
239 password = match[2];
240 address = match[3];
241 if (!match[4].str().empty())
242 port = std::stoi(match[4]);
243 std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$");
244 if (!std::regex_match(nickname, nickname_re)) {
245 std::cout
246 << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n";
247 return 0;
248 }
249 if (address.empty()) {
250 std::cout << "Address to room must not be empty.\n";
251 return 0;
252 }
253 break;
254 }
112 case 'f': 255 case 'f':
113 fullscreen = true; 256 fullscreen = true;
114 LOG_INFO(Frontend, "Starting in fullscreen mode..."); 257 LOG_INFO(Frontend, "Starting in fullscreen mode...");
@@ -215,6 +358,21 @@ int main(int argc, char** argv) {
215 358
216 system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); 359 system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL");
217 360
361 if (use_multiplayer) {
362 if (auto member = system.GetRoomNetwork().GetRoomMember().lock()) {
363 member->BindOnChatMessageRecieved(OnMessageReceived);
364 member->BindOnStatusMessageReceived(OnStatusMessageReceived);
365 member->BindOnStateChanged(OnStateChanged);
366 member->BindOnError(OnNetworkError);
367 LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port,
368 nickname);
369 member->Join(nickname, "", address.c_str(), port, 0, Network::NoPreferredMac, password);
370 } else {
371 LOG_ERROR(Network, "Could not access RoomMember");
372 return 0;
373 }
374 }
375
218 // Core is loaded, start the GPU (makes the GPU contexts current to this thread) 376 // Core is loaded, start the GPU (makes the GPU contexts current to this thread)
219 system.GPU().Start(); 377 system.GPU().Start();
220 system.GetCpuManager().OnGpuReady(); 378 system.GetCpuManager().OnGpuReady();