yuzu-emu
/
yuzu
Archived
1
0
Fork 0

Merge pull request #8876 from FearlessTobi/multiplayer-part3

ldn: Implement "local wireless" networked multiplayer
This commit is contained in:
bunnei 2022-10-01 14:53:36 -07:00 committed by GitHub
commit 2a752bbd64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1310 additions and 187 deletions

View File

@ -496,6 +496,8 @@ add_library(core STATIC
hle/service/jit/jit.h
hle/service/lbl/lbl.cpp
hle/service/lbl/lbl.h
hle/service/ldn/lan_discovery.cpp
hle/service/ldn/lan_discovery.h
hle/service/ldn/ldn_results.h
hle/service/ldn/ldn.cpp
hle/service/ldn/ldn.h

View File

@ -36,7 +36,8 @@ namespace Service::HID {
// Updating period for each HID device.
// Period time is obtained by measuring the number of samples in a second on HW using a homebrew
constexpr auto pad_update_ns = std::chrono::nanoseconds{4 * 1000 * 1000}; // (4ms, 250Hz)
// Correct pad_update_ns is 4ms this is overclocked to lower input lag
constexpr auto pad_update_ns = std::chrono::nanoseconds{1 * 1000 * 1000}; // (1ms, 1000Hz)
constexpr auto mouse_keyboard_update_ns = std::chrono::nanoseconds{8 * 1000 * 1000}; // (8ms, 125Hz)
constexpr auto motion_update_ns = std::chrono::nanoseconds{5 * 1000 * 1000}; // (5ms, 200Hz)

View File

@ -0,0 +1,633 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ldn/lan_discovery.h"
#include "core/internal_network/network.h"
#include "core/internal_network/network_interface.h"
namespace Service::LDN {
LanStation::LanStation(s8 node_id_, LANDiscovery* discovery_)
: node_info(nullptr), status(NodeStatus::Disconnected), node_id(node_id_),
discovery(discovery_) {}
LanStation::~LanStation() = default;
NodeStatus LanStation::GetStatus() const {
return status;
}
void LanStation::OnClose() {
LOG_INFO(Service_LDN, "OnClose {}", node_id);
Reset();
discovery->UpdateNodes();
}
void LanStation::Reset() {
status = NodeStatus::Disconnected;
};
void LanStation::OverrideInfo() {
bool connected = GetStatus() == NodeStatus::Connected;
node_info->node_id = node_id;
node_info->is_connected = connected ? 1 : 0;
}
LANDiscovery::LANDiscovery(Network::RoomNetwork& room_network_)
: stations({{{1, this}, {2, this}, {3, this}, {4, this}, {5, this}, {6, this}, {7, this}}}),
room_network{room_network_} {}
LANDiscovery::~LANDiscovery() {
if (inited) {
Result rc = Finalize();
LOG_INFO(Service_LDN, "Finalize: {}", rc.raw);
}
}
void LANDiscovery::InitNetworkInfo() {
network_info.common.bssid = GetFakeMac();
network_info.common.channel = WifiChannel::Wifi24_6;
network_info.common.link_level = LinkLevel::Good;
network_info.common.network_type = PackedNetworkType::Ldn;
network_info.common.ssid = fake_ssid;
auto& nodes = network_info.ldn.nodes;
for (std::size_t i = 0; i < NodeCountMax; i++) {
nodes[i].node_id = static_cast<s8>(i);
nodes[i].is_connected = 0;
}
}
void LANDiscovery::InitNodeStateChange() {
for (auto& node_update : node_changes) {
node_update.state_change = NodeStateChange::None;
}
for (auto& node_state : node_last_states) {
node_state = 0;
}
}
State LANDiscovery::GetState() const {
return state;
}
void LANDiscovery::SetState(State new_state) {
state = new_state;
}
Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network) const {
if (state == State::AccessPointCreated || state == State::StationConnected) {
std::memcpy(&out_network, &network_info, sizeof(network_info));
return ResultSuccess;
}
return ResultBadState;
}
Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network,
std::vector<NodeLatestUpdate>& out_updates,
std::size_t buffer_count) {
if (buffer_count > NodeCountMax) {
return ResultInvalidBufferCount;
}
if (state == State::AccessPointCreated || state == State::StationConnected) {
std::memcpy(&out_network, &network_info, sizeof(network_info));
for (std::size_t i = 0; i < buffer_count; i++) {
out_updates[i].state_change = node_changes[i].state_change;
node_changes[i].state_change = NodeStateChange::None;
}
return ResultSuccess;
}
return ResultBadState;
}
DisconnectReason LANDiscovery::GetDisconnectReason() const {
return disconnect_reason;
}
Result LANDiscovery::Scan(std::vector<NetworkInfo>& networks, u16& count,
const ScanFilter& filter) {
if (!IsFlagSet(filter.flag, ScanFilterFlag::NetworkType) ||
filter.network_type <= NetworkType::All) {
if (!IsFlagSet(filter.flag, ScanFilterFlag::Ssid) && filter.ssid.length >= SsidLengthMax) {
return ResultBadInput;
}
}
{
std::scoped_lock lock{packet_mutex};
scan_results.clear();
SendBroadcast(Network::LDNPacketType::Scan);
}
LOG_INFO(Service_LDN, "Waiting for scan replies");
std::this_thread::sleep_for(std::chrono::seconds(1));
std::scoped_lock lock{packet_mutex};
for (const auto& [key, info] : scan_results) {
if (count >= networks.size()) {
break;
}
if (IsFlagSet(filter.flag, ScanFilterFlag::LocalCommunicationId)) {
if (filter.network_id.intent_id.local_communication_id !=
info.network_id.intent_id.local_communication_id) {
continue;
}
}
if (IsFlagSet(filter.flag, ScanFilterFlag::SessionId)) {
if (filter.network_id.session_id != info.network_id.session_id) {
continue;
}
}
if (IsFlagSet(filter.flag, ScanFilterFlag::NetworkType)) {
if (filter.network_type != static_cast<NetworkType>(info.common.network_type)) {
continue;
}
}
if (IsFlagSet(filter.flag, ScanFilterFlag::Ssid)) {
if (filter.ssid != info.common.ssid) {
continue;
}
}
if (IsFlagSet(filter.flag, ScanFilterFlag::SceneId)) {
if (filter.network_id.intent_id.scene_id != info.network_id.intent_id.scene_id) {
continue;
}
}
networks[count++] = info;
}
return ResultSuccess;
}
Result LANDiscovery::SetAdvertiseData(std::span<const u8> data) {
std::scoped_lock lock{packet_mutex};
const std::size_t size = data.size();
if (size > AdvertiseDataSizeMax) {
return ResultAdvertiseDataTooLarge;
}
std::memcpy(network_info.ldn.advertise_data.data(), data.data(), size);
network_info.ldn.advertise_data_size = static_cast<u16>(size);
UpdateNodes();
return ResultSuccess;
}
Result LANDiscovery::OpenAccessPoint() {
std::scoped_lock lock{packet_mutex};
disconnect_reason = DisconnectReason::None;
if (state == State::None) {
return ResultBadState;
}
ResetStations();
SetState(State::AccessPointOpened);
return ResultSuccess;
}
Result LANDiscovery::CloseAccessPoint() {
std::scoped_lock lock{packet_mutex};
if (state == State::None) {
return ResultBadState;
}
if (state == State::AccessPointCreated) {
DestroyNetwork();
}
ResetStations();
SetState(State::Initialized);
return ResultSuccess;
}
Result LANDiscovery::OpenStation() {
std::scoped_lock lock{packet_mutex};
disconnect_reason = DisconnectReason::None;
if (state == State::None) {
return ResultBadState;
}
ResetStations();
SetState(State::StationOpened);
return ResultSuccess;
}
Result LANDiscovery::CloseStation() {
std::scoped_lock lock{packet_mutex};
if (state == State::None) {
return ResultBadState;
}
if (state == State::StationConnected) {
Disconnect();
}
ResetStations();
SetState(State::Initialized);
return ResultSuccess;
}
Result LANDiscovery::CreateNetwork(const SecurityConfig& security_config,
const UserConfig& user_config,
const NetworkConfig& network_config) {
std::scoped_lock lock{packet_mutex};
if (state != State::AccessPointOpened) {
return ResultBadState;
}
InitNetworkInfo();
network_info.ldn.node_count_max = network_config.node_count_max;
network_info.ldn.security_mode = security_config.security_mode;
if (network_config.channel == WifiChannel::Default) {
network_info.common.channel = WifiChannel::Wifi24_6;
} else {
network_info.common.channel = network_config.channel;
}
std::independent_bits_engine<std::mt19937, 64, u64> bits_engine;
network_info.network_id.session_id.high = bits_engine();
network_info.network_id.session_id.low = bits_engine();
network_info.network_id.intent_id = network_config.intent_id;
NodeInfo& node0 = network_info.ldn.nodes[0];
const Result rc2 = GetNodeInfo(node0, user_config, network_config.local_communication_version);
if (rc2.IsError()) {
return ResultAccessPointConnectionFailed;
}
SetState(State::AccessPointCreated);
InitNodeStateChange();
node0.is_connected = 1;
UpdateNodes();
return rc2;
}
Result LANDiscovery::DestroyNetwork() {
for (auto local_ip : connected_clients) {
SendPacket(Network::LDNPacketType::DestroyNetwork, local_ip);
}
ResetStations();
SetState(State::AccessPointOpened);
lan_event();
return ResultSuccess;
}
Result LANDiscovery::Connect(const NetworkInfo& network_info_, const UserConfig& user_config,
u16 local_communication_version) {
std::scoped_lock lock{packet_mutex};
if (network_info_.ldn.node_count == 0) {
return ResultInvalidNodeCount;
}
Result rc = GetNodeInfo(node_info, user_config, local_communication_version);
if (rc.IsError()) {
return ResultConnectionFailed;
}
Ipv4Address node_host = network_info_.ldn.nodes[0].ipv4_address;
std::reverse(std::begin(node_host), std::end(node_host)); // htonl
host_ip = node_host;
SendPacket(Network::LDNPacketType::Connect, node_info, *host_ip);
InitNodeStateChange();
std::this_thread::sleep_for(std::chrono::seconds(1));
return ResultSuccess;
}
Result LANDiscovery::Disconnect() {
if (host_ip) {
SendPacket(Network::LDNPacketType::Disconnect, node_info, *host_ip);
}
SetState(State::StationOpened);
lan_event();
return ResultSuccess;
}
Result LANDiscovery::Initialize(LanEventFunc lan_event_, bool listening) {
std::scoped_lock lock{packet_mutex};
if (inited) {
return ResultSuccess;
}
for (auto& station : stations) {
station.discovery = this;
station.node_info = &network_info.ldn.nodes[station.node_id];
station.Reset();
}
connected_clients.clear();
lan_event = lan_event_;
SetState(State::Initialized);
inited = true;
return ResultSuccess;
}
Result LANDiscovery::Finalize() {
std::scoped_lock lock{packet_mutex};
Result rc = ResultSuccess;
if (inited) {
if (state == State::AccessPointCreated) {
DestroyNetwork();
}
if (state == State::StationConnected) {
Disconnect();
}
ResetStations();
inited = false;
}
SetState(State::None);
return rc;
}
void LANDiscovery::ResetStations() {
for (auto& station : stations) {
station.Reset();
}
connected_clients.clear();
}
void LANDiscovery::UpdateNodes() {
u8 count = 0;
for (auto& station : stations) {
bool connected = station.GetStatus() == NodeStatus::Connected;
if (connected) {
count++;
}
station.OverrideInfo();
}
network_info.ldn.node_count = count + 1;
for (auto local_ip : connected_clients) {
SendPacket(Network::LDNPacketType::SyncNetwork, network_info, local_ip);
}
OnNetworkInfoChanged();
}
void LANDiscovery::OnSyncNetwork(const NetworkInfo& info) {
network_info = info;
if (state == State::StationOpened) {
SetState(State::StationConnected);
}
OnNetworkInfoChanged();
}
void LANDiscovery::OnDisconnectFromHost() {
LOG_INFO(Service_LDN, "OnDisconnectFromHost state: {}", static_cast<int>(state));
host_ip = std::nullopt;
if (state == State::StationConnected) {
SetState(State::StationOpened);
lan_event();
}
}
void LANDiscovery::OnNetworkInfoChanged() {
if (IsNodeStateChanged()) {
lan_event();
}
return;
}
Network::IPv4Address LANDiscovery::GetLocalIp() const {
Network::IPv4Address local_ip{0xFF, 0xFF, 0xFF, 0xFF};
if (auto room_member = room_network.GetRoomMember().lock()) {
if (room_member->IsConnected()) {
local_ip = room_member->GetFakeIpAddress();
}
}
return local_ip;
}
template <typename Data>
void LANDiscovery::SendPacket(Network::LDNPacketType type, const Data& data,
Ipv4Address remote_ip) {
Network::LDNPacket packet;
packet.type = type;
packet.broadcast = false;
packet.local_ip = GetLocalIp();
packet.remote_ip = remote_ip;
packet.data.resize(sizeof(data));
std::memcpy(packet.data.data(), &data, sizeof(data));
SendPacket(packet);
}
void LANDiscovery::SendPacket(Network::LDNPacketType type, Ipv4Address remote_ip) {
Network::LDNPacket packet;
packet.type = type;
packet.broadcast = false;
packet.local_ip = GetLocalIp();
packet.remote_ip = remote_ip;
SendPacket(packet);
}
template <typename Data>
void LANDiscovery::SendBroadcast(Network::LDNPacketType type, const Data& data) {
Network::LDNPacket packet;
packet.type = type;
packet.broadcast = true;
packet.local_ip = GetLocalIp();
packet.data.resize(sizeof(data));
std::memcpy(packet.data.data(), &data, sizeof(data));
SendPacket(packet);
}
void LANDiscovery::SendBroadcast(Network::LDNPacketType type) {
Network::LDNPacket packet;
packet.type = type;
packet.broadcast = true;
packet.local_ip = GetLocalIp();
SendPacket(packet);
}
void LANDiscovery::SendPacket(const Network::LDNPacket& packet) {
if (auto room_member = room_network.GetRoomMember().lock()) {
if (room_member->IsConnected()) {
room_member->SendLdnPacket(packet);
}
}
}
void LANDiscovery::ReceivePacket(const Network::LDNPacket& packet) {
std::scoped_lock lock{packet_mutex};
switch (packet.type) {
case Network::LDNPacketType::Scan: {
LOG_INFO(Frontend, "Scan packet received!");
if (state == State::AccessPointCreated) {
// Reply to the sender
SendPacket(Network::LDNPacketType::ScanResp, network_info, packet.local_ip);
}
break;
}
case Network::LDNPacketType::ScanResp: {
LOG_INFO(Frontend, "ScanResp packet received!");
NetworkInfo info{};
std::memcpy(&info, packet.data.data(), sizeof(NetworkInfo));
scan_results.insert({info.common.bssid, info});
break;
}
case Network::LDNPacketType::Connect: {
LOG_INFO(Frontend, "Connect packet received!");
NodeInfo info{};
std::memcpy(&info, packet.data.data(), sizeof(NodeInfo));
connected_clients.push_back(packet.local_ip);
for (LanStation& station : stations) {
if (station.status != NodeStatus::Connected) {
*station.node_info = info;
station.status = NodeStatus::Connected;
break;
}
}
UpdateNodes();
break;
}
case Network::LDNPacketType::Disconnect: {
LOG_INFO(Frontend, "Disconnect packet received!");
connected_clients.erase(
std::remove(connected_clients.begin(), connected_clients.end(), packet.local_ip),
connected_clients.end());
NodeInfo info{};
std::memcpy(&info, packet.data.data(), sizeof(NodeInfo));
for (LanStation& station : stations) {
if (station.status == NodeStatus::Connected &&
station.node_info->mac_address == info.mac_address) {
station.OnClose();
break;
}
}
break;
}
case Network::LDNPacketType::DestroyNetwork: {
ResetStations();
OnDisconnectFromHost();
break;
}
case Network::LDNPacketType::SyncNetwork: {
if (state == State::StationOpened || state == State::StationConnected) {
LOG_INFO(Frontend, "SyncNetwork packet received!");
NetworkInfo info{};
std::memcpy(&info, packet.data.data(), sizeof(NetworkInfo));
OnSyncNetwork(info);
} else {
LOG_INFO(Frontend, "SyncNetwork packet received but in wrong State!");
}
break;
}
default: {
LOG_INFO(Frontend, "ReceivePacket unhandled type {}", static_cast<int>(packet.type));
break;
}
}
}
bool LANDiscovery::IsNodeStateChanged() {
bool changed = false;
const auto& nodes = network_info.ldn.nodes;
for (int i = 0; i < NodeCountMax; i++) {
if (nodes[i].is_connected != node_last_states[i]) {
if (nodes[i].is_connected) {
node_changes[i].state_change |= NodeStateChange::Connect;
} else {
node_changes[i].state_change |= NodeStateChange::Disconnect;
}
node_last_states[i] = nodes[i].is_connected;
changed = true;
}
}
return changed;
}
bool LANDiscovery::IsFlagSet(ScanFilterFlag flag, ScanFilterFlag search_flag) const {
const auto flag_value = static_cast<u32>(flag);
const auto search_flag_value = static_cast<u32>(search_flag);
return (flag_value & search_flag_value) == search_flag_value;
}
int LANDiscovery::GetStationCount() const {
return static_cast<int>(
std::count_if(stations.begin(), stations.end(), [](const auto& station) {
return station.GetStatus() != NodeStatus::Disconnected;
}));
}
MacAddress LANDiscovery::GetFakeMac() const {
MacAddress mac{};
mac.raw[0] = 0x02;
mac.raw[1] = 0x00;
const auto ip = GetLocalIp();
memcpy(mac.raw.data() + 2, &ip, sizeof(ip));
return mac;
}
Result LANDiscovery::GetNodeInfo(NodeInfo& node, const UserConfig& userConfig,
u16 localCommunicationVersion) {
const auto network_interface = Network::GetSelectedNetworkInterface();
if (!network_interface) {
LOG_ERROR(Service_LDN, "No network interface available");
return ResultNoIpAddress;
}
node.mac_address = GetFakeMac();
node.is_connected = 1;
std::memcpy(node.user_name.data(), userConfig.user_name.data(), UserNameBytesMax + 1);
node.local_communication_version = localCommunicationVersion;
Ipv4Address current_address = GetLocalIp();
std::reverse(std::begin(current_address), std::end(current_address)); // ntohl
node.ipv4_address = current_address;
return ResultSuccess;
}
} // namespace Service::LDN

View File

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstring>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <random>
#include <span>
#include <thread>
#include <unordered_map>
#include "common/logging/log.h"
#include "common/socket_types.h"
#include "core/hle/result.h"
#include "core/hle/service/ldn/ldn_results.h"
#include "core/hle/service/ldn/ldn_types.h"
#include "network/network.h"
namespace Service::LDN {
class LANDiscovery;
class LanStation {
public:
LanStation(s8 node_id_, LANDiscovery* discovery_);
~LanStation();
void OnClose();
NodeStatus GetStatus() const;
void Reset();
void OverrideInfo();
protected:
friend class LANDiscovery;
NodeInfo* node_info;
NodeStatus status;
s8 node_id;
LANDiscovery* discovery;
};
class LANDiscovery {
public:
using LanEventFunc = std::function<void()>;
LANDiscovery(Network::RoomNetwork& room_network_);
~LANDiscovery();
State GetState() const;
void SetState(State new_state);
Result GetNetworkInfo(NetworkInfo& out_network) const;
Result GetNetworkInfo(NetworkInfo& out_network, std::vector<NodeLatestUpdate>& out_updates,
std::size_t buffer_count);
DisconnectReason GetDisconnectReason() const;
Result Scan(std::vector<NetworkInfo>& networks, u16& count, const ScanFilter& filter);
Result SetAdvertiseData(std::span<const u8> data);
Result OpenAccessPoint();
Result CloseAccessPoint();
Result OpenStation();
Result CloseStation();
Result CreateNetwork(const SecurityConfig& security_config, const UserConfig& user_config,
const NetworkConfig& network_config);
Result DestroyNetwork();
Result Connect(const NetworkInfo& network_info_, const UserConfig& user_config,
u16 local_communication_version);
Result Disconnect();
Result Initialize(LanEventFunc lan_event_ = empty_func, bool listening = true);
Result Finalize();
void ReceivePacket(const Network::LDNPacket& packet);
protected:
friend class LanStation;
void InitNetworkInfo();
void InitNodeStateChange();
void ResetStations();
void UpdateNodes();
void OnSyncNetwork(const NetworkInfo& info);
void OnDisconnectFromHost();
void OnNetworkInfoChanged();
bool IsNodeStateChanged();
bool IsFlagSet(ScanFilterFlag flag, ScanFilterFlag search_flag) const;
int GetStationCount() const;
MacAddress GetFakeMac() const;
Result GetNodeInfo(NodeInfo& node, const UserConfig& user_config,
u16 local_communication_version);
Network::IPv4Address GetLocalIp() const;
template <typename Data>
void SendPacket(Network::LDNPacketType type, const Data& data, Ipv4Address remote_ip);
void SendPacket(Network::LDNPacketType type, Ipv4Address remote_ip);
template <typename Data>
void SendBroadcast(Network::LDNPacketType type, const Data& data);
void SendBroadcast(Network::LDNPacketType type);
void SendPacket(const Network::LDNPacket& packet);
static const LanEventFunc empty_func;
static constexpr Ssid fake_ssid{"YuzuFakeSsidForLdn"};
bool inited{};
std::mutex packet_mutex;
std::array<LanStation, StationCountMax> stations;
std::array<NodeLatestUpdate, NodeCountMax> node_changes{};
std::array<u8, NodeCountMax> node_last_states{};
std::unordered_map<MacAddress, NetworkInfo, MACAddressHash> scan_results{};
NodeInfo node_info{};
NetworkInfo network_info{};
State state{State::None};
DisconnectReason disconnect_reason{DisconnectReason::None};
// TODO (flTobi): Should this be an std::set?
std::vector<Ipv4Address> connected_clients;
std::optional<Ipv4Address> host_ip;
LanEventFunc lan_event;
Network::RoomNetwork& room_network;
};
} // namespace Service::LDN

View File

@ -4,11 +4,13 @@
#include <memory>
#include "core/core.h"
#include "core/hle/service/ldn/lan_discovery.h"
#include "core/hle/service/ldn/ldn.h"
#include "core/hle/service/ldn/ldn_results.h"
#include "core/hle/service/ldn/ldn_types.h"
#include "core/internal_network/network.h"
#include "core/internal_network/network_interface.h"
#include "network/network.h"
// This is defined by synchapi.h and conflicts with ServiceContext::CreateEvent
#undef CreateEvent
@ -105,13 +107,13 @@ class IUserLocalCommunicationService final
public:
explicit IUserLocalCommunicationService(Core::System& system_)
: ServiceFramework{system_, "IUserLocalCommunicationService", ServiceThreadType::CreateNew},
service_context{system, "IUserLocalCommunicationService"}, room_network{
system_.GetRoomNetwork()} {
service_context{system, "IUserLocalCommunicationService"},
room_network{system_.GetRoomNetwork()}, lan_discovery{room_network} {
// clang-format off
static const FunctionInfo functions[] = {
{0, &IUserLocalCommunicationService::GetState, "GetState"},
{1, &IUserLocalCommunicationService::GetNetworkInfo, "GetNetworkInfo"},
{2, nullptr, "GetIpv4Address"},
{2, &IUserLocalCommunicationService::GetIpv4Address, "GetIpv4Address"},
{3, &IUserLocalCommunicationService::GetDisconnectReason, "GetDisconnectReason"},
{4, &IUserLocalCommunicationService::GetSecurityParameter, "GetSecurityParameter"},
{5, &IUserLocalCommunicationService::GetNetworkConfig, "GetNetworkConfig"},
@ -119,7 +121,7 @@ public:
{101, &IUserLocalCommunicationService::GetNetworkInfoLatestUpdate, "GetNetworkInfoLatestUpdate"},
{102, &IUserLocalCommunicationService::Scan, "Scan"},
{103, &IUserLocalCommunicationService::ScanPrivate, "ScanPrivate"},
{104, nullptr, "SetWirelessControllerRestriction"},
{104, &IUserLocalCommunicationService::SetWirelessControllerRestriction, "SetWirelessControllerRestriction"},
{200, &IUserLocalCommunicationService::OpenAccessPoint, "OpenAccessPoint"},
{201, &IUserLocalCommunicationService::CloseAccessPoint, "CloseAccessPoint"},
{202, &IUserLocalCommunicationService::CreateNetwork, "CreateNetwork"},
@ -148,16 +150,30 @@ public:
}
~IUserLocalCommunicationService() {
if (is_initialized) {
if (auto room_member = room_network.GetRoomMember().lock()) {
room_member->Unbind(ldn_packet_received);
}
}
service_context.CloseEvent(state_change_event);
}
/// Callback to parse and handle a received LDN packet.
void OnLDNPacketReceived(const Network::LDNPacket& packet) {
lan_discovery.ReceivePacket(packet);
}
void OnEventFired() {
state_change_event->GetWritableEvent().Signal();
}
void GetState(Kernel::HLERequestContext& ctx) {
State state = State::Error;
LOG_WARNING(Service_LDN, "(STUBBED) called, state = {}", state);
if (is_initialized) {
state = lan_discovery.GetState();
}
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
@ -175,7 +191,7 @@ public:
}
NetworkInfo network_info{};
const auto rc = ResultSuccess;
const auto rc = lan_discovery.GetNetworkInfo(network_info);
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "NetworkInfo is not valid {}", rc.raw);
IPC::ResponseBuilder rb{ctx, 2};
@ -183,28 +199,50 @@ public:
return;
}
LOG_WARNING(Service_LDN, "(STUBBED) called, ssid='{}', nodes={}",
network_info.common.ssid.GetStringValue(), network_info.ldn.node_count);
ctx.WriteBuffer<NetworkInfo>(network_info);
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(rc);
rb.Push(ResultSuccess);
}
void GetIpv4Address(Kernel::HLERequestContext& ctx) {
const auto network_interface = Network::GetSelectedNetworkInterface();
if (!network_interface) {
LOG_ERROR(Service_LDN, "No network interface available");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultNoIpAddress);
return;
}
Ipv4Address current_address{Network::TranslateIPv4(network_interface->ip_address)};
Ipv4Address subnet_mask{Network::TranslateIPv4(network_interface->subnet_mask)};
// When we're connected to a room, spoof the hosts IP address
if (auto room_member = room_network.GetRoomMember().lock()) {
if (room_member->IsConnected()) {
current_address = room_member->GetFakeIpAddress();
}
}
std::reverse(std::begin(current_address), std::end(current_address)); // ntohl
std::reverse(std::begin(subnet_mask), std::end(subnet_mask)); // ntohl
IPC::ResponseBuilder rb{ctx, 4};
rb.Push(ResultSuccess);
rb.PushRaw(current_address);
rb.PushRaw(subnet_mask);
}
void GetDisconnectReason(Kernel::HLERequestContext& ctx) {
const auto disconnect_reason = DisconnectReason::None;
LOG_WARNING(Service_LDN, "(STUBBED) called, disconnect_reason={}", disconnect_reason);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.PushEnum(disconnect_reason);
rb.PushEnum(lan_discovery.GetDisconnectReason());
}
void GetSecurityParameter(Kernel::HLERequestContext& ctx) {
SecurityParameter security_parameter{};
NetworkInfo info{};
const Result rc = ResultSuccess;
const Result rc = lan_discovery.GetNetworkInfo(info);
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "NetworkInfo is not valid {}", rc.raw);
@ -217,8 +255,6 @@ public:
std::memcpy(security_parameter.data.data(), info.ldn.security_parameter.data(),
sizeof(SecurityParameter::data));
LOG_WARNING(Service_LDN, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 10};
rb.Push(rc);
rb.PushRaw<SecurityParameter>(security_parameter);
@ -227,7 +263,7 @@ public:
void GetNetworkConfig(Kernel::HLERequestContext& ctx) {
NetworkConfig config{};
NetworkInfo info{};
const Result rc = ResultSuccess;
const Result rc = lan_discovery.GetNetworkInfo(info);
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "NetworkConfig is not valid {}", rc.raw);
@ -241,12 +277,6 @@ public:
config.node_count_max = info.ldn.node_count_max;
config.local_communication_version = info.ldn.nodes[0].local_communication_version;
LOG_WARNING(Service_LDN,
"(STUBBED) called, intent_id={}/{}, channel={}, node_count_max={}, "
"local_communication_version={}",
config.intent_id.local_communication_id, config.intent_id.scene_id,
config.channel, config.node_count_max, config.local_communication_version);
IPC::ResponseBuilder rb{ctx, 10};
rb.Push(rc);
rb.PushRaw<NetworkConfig>(config);
@ -265,17 +295,17 @@ public:
const std::size_t node_buffer_count = ctx.GetWriteBufferSize(1) / sizeof(NodeLatestUpdate);
if (node_buffer_count == 0 || network_buffer_size != sizeof(NetworkInfo)) {
LOG_ERROR(Service_LDN, "Invalid buffer size {}, {}", network_buffer_size,
LOG_ERROR(Service_LDN, "Invalid buffer, size = {}, count = {}", network_buffer_size,
node_buffer_count);
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultBadInput);
return;
}
NetworkInfo info;
NetworkInfo info{};
std::vector<NodeLatestUpdate> latest_update(node_buffer_count);
const auto rc = ResultSuccess;
const auto rc = lan_discovery.GetNetworkInfo(info, latest_update, latest_update.size());
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "NetworkInfo is not valid {}", rc.raw);
IPC::ResponseBuilder rb{ctx, 2};
@ -283,9 +313,6 @@ public:
return;
}
LOG_WARNING(Service_LDN, "(STUBBED) called, ssid='{}', nodes={}",
info.common.ssid.GetStringValue(), info.ldn.node_count);
ctx.WriteBuffer(info, 0);
ctx.WriteBuffer(latest_update, 1);
@ -317,92 +344,78 @@ public:
u16 count = 0;
std::vector<NetworkInfo> network_infos(network_info_size);
Result rc = lan_discovery.Scan(network_infos, count, scan_filter);
LOG_WARNING(Service_LDN,
"(STUBBED) called, channel={}, filter_scan_flag={}, filter_network_type={}",
channel, scan_filter.flag, scan_filter.network_type);
LOG_INFO(Service_LDN,
"called, channel={}, filter_scan_flag={}, filter_network_type={}, is_private={}",
channel, scan_filter.flag, scan_filter.network_type, is_private);
ctx.WriteBuffer(network_infos);
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(rc);
rb.Push<u32>(count);
}
void OpenAccessPoint(Kernel::HLERequestContext& ctx) {
void SetWirelessControllerRestriction(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
void OpenAccessPoint(Kernel::HLERequestContext& ctx) {
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(lan_discovery.OpenAccessPoint());
}
void CloseAccessPoint(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.CloseAccessPoint());
}
void CreateNetwork(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
struct Parameters {
SecurityConfig security_config;
UserConfig user_config;
INSERT_PADDING_WORDS_NOINIT(1);
NetworkConfig network_config;
};
static_assert(sizeof(Parameters) == 0x98, "Parameters has incorrect size.");
LOG_INFO(Service_LDN, "called");
const auto parameters{rp.PopRaw<Parameters>()};
LOG_WARNING(Service_LDN,
"(STUBBED) called, passphrase_size={}, security_mode={}, "
"local_communication_version={}",
parameters.security_config.passphrase_size,
parameters.security_config.security_mode,
parameters.network_config.local_communication_version);
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
CreateNetworkImpl(ctx);
}
void CreateNetworkPrivate(Kernel::HLERequestContext& ctx) {
LOG_INFO(Service_LDN, "called");
CreateNetworkImpl(ctx, true);
}
void CreateNetworkImpl(Kernel::HLERequestContext& ctx, bool is_private = false) {
IPC::RequestParser rp{ctx};
struct Parameters {
SecurityConfig security_config;
SecurityParameter security_parameter;
UserConfig user_config;
NetworkConfig network_config;
};
static_assert(sizeof(Parameters) == 0xB8, "Parameters has incorrect size.");
const auto parameters{rp.PopRaw<Parameters>()};
LOG_WARNING(Service_LDN,
"(STUBBED) called, passphrase_size={}, security_mode={}, "
"local_communication_version={}",
parameters.security_config.passphrase_size,
parameters.security_config.security_mode,
parameters.network_config.local_communication_version);
const auto security_config{rp.PopRaw<SecurityConfig>()};
[[maybe_unused]] const auto security_parameter{is_private ? rp.PopRaw<SecurityParameter>()
: SecurityParameter{}};
const auto user_config{rp.PopRaw<UserConfig>()};
rp.Pop<u32>(); // Padding
const auto network_Config{rp.PopRaw<NetworkConfig>()};
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.CreateNetwork(security_config, user_config, network_Config));
}
void DestroyNetwork(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.DestroyNetwork());
}
void SetAdvertiseData(Kernel::HLERequestContext& ctx) {
std::vector<u8> read_buffer = ctx.ReadBuffer();
LOG_WARNING(Service_LDN, "(STUBBED) called, size {}", read_buffer.size());
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.SetAdvertiseData(read_buffer));
}
void SetStationAcceptPolicy(Kernel::HLERequestContext& ctx) {
@ -420,17 +433,17 @@ public:
}
void OpenStation(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.OpenStation());
}
void CloseStation(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.CloseStation());
}
void Connect(Kernel::HLERequestContext& ctx) {
@ -445,16 +458,13 @@ public:
const auto parameters{rp.PopRaw<Parameters>()};
LOG_WARNING(Service_LDN,
"(STUBBED) called, passphrase_size={}, security_mode={}, "
LOG_INFO(Service_LDN,
"called, passphrase_size={}, security_mode={}, "
"local_communication_version={}",
parameters.security_config.passphrase_size,
parameters.security_config.security_mode,
parameters.local_communication_version);
parameters.security_config.security_mode, parameters.local_communication_version);
const std::vector<u8> read_buffer = ctx.ReadBuffer();
NetworkInfo network_info{};
if (read_buffer.size() != sizeof(NetworkInfo)) {
LOG_ERROR(Frontend, "NetworkInfo doesn't match read_buffer size!");
IPC::ResponseBuilder rb{ctx, 2};
@ -462,40 +472,47 @@ public:
return;
}
NetworkInfo network_info{};
std::memcpy(&network_info, read_buffer.data(), read_buffer.size());
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.Connect(network_info, parameters.user_config,
static_cast<u16>(parameters.local_communication_version)));
}
void Disconnect(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
LOG_INFO(Service_LDN, "called");
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.Disconnect());
}
void Initialize(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
void Initialize(Kernel::HLERequestContext& ctx) {
const auto rc = InitializeImpl(ctx);
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "Network isn't initialized, rc={}", rc.raw);
}
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(rc);
}
void Finalize(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
if (auto room_member = room_network.GetRoomMember().lock()) {
room_member->Unbind(ldn_packet_received);
}
is_initialized = false;
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
rb.Push(lan_discovery.Finalize());
}
void Initialize2(Kernel::HLERequestContext& ctx) {
LOG_WARNING(Service_LDN, "(STUBBED) called");
const auto rc = InitializeImpl(ctx);
if (rc.IsError()) {
LOG_ERROR(Service_LDN, "Network isn't initialized, rc={}", rc.raw);
}
IPC::ResponseBuilder rb{ctx, 2};
rb.Push(rc);
@ -508,14 +525,26 @@ public:
return ResultAirplaneModeEnabled;
}
is_initialized = true;
// TODO (flTobi): Change this to ResultSuccess when LDN is fully implemented
if (auto room_member = room_network.GetRoomMember().lock()) {
ldn_packet_received = room_member->BindOnLdnPacketReceived(
[this](const Network::LDNPacket& packet) { OnLDNPacketReceived(packet); });
} else {
LOG_ERROR(Service_LDN, "Couldn't bind callback!");
return ResultAirplaneModeEnabled;
}
lan_discovery.Initialize([&]() { OnEventFired(); });
is_initialized = true;
return ResultSuccess;
}
KernelHelpers::ServiceContext service_context;
Kernel::KEvent* state_change_event;
Network::RoomNetwork& room_network;
LANDiscovery lan_discovery;
// Callback identifier for the OnLDNPacketReceived event.
Network::RoomMember::CallbackHandle<Network::LDNPacket> ldn_packet_received;
bool is_initialized{};
};

View File

@ -31,6 +31,8 @@ enum class NodeStateChange : u8 {
DisconnectAndConnect,
};
DECLARE_ENUM_FLAG_OPERATORS(NodeStateChange)
enum class ScanFilterFlag : u32 {
None = 0,
LocalCommunicationId = 1 << 0,
@ -100,13 +102,13 @@ enum class AcceptPolicy : u8 {
enum class WifiChannel : s16 {
Default = 0,
wifi24_1 = 1,
wifi24_6 = 6,
wifi24_11 = 11,
wifi50_36 = 36,
wifi50_40 = 40,
wifi50_44 = 44,
wifi50_48 = 48,
Wifi24_1 = 1,
Wifi24_6 = 6,
Wifi24_11 = 11,
Wifi50_36 = 36,
Wifi50_40 = 40,
Wifi50_44 = 44,
Wifi50_48 = 48,
};
enum class LinkLevel : s8 {
@ -116,6 +118,11 @@ enum class LinkLevel : s8 {
Excellent,
};
enum class NodeStatus : u8 {
Disconnected,
Connected,
};
struct NodeLatestUpdate {
NodeStateChange state_change;
INSERT_PADDING_BYTES(0x7); // Unknown
@ -150,7 +157,7 @@ struct Ssid {
Ssid() = default;
explicit Ssid(std::string_view data) {
constexpr explicit Ssid(std::string_view data) {
length = static_cast<u8>(std::min(data.size(), SsidLengthMax));
data.copy(raw.data(), length);
raw[length] = 0;
@ -159,19 +166,18 @@ struct Ssid {
std::string GetStringValue() const {
return std::string(raw.data());
}
bool operator==(const Ssid& b) const {
return (length == b.length) && (std::memcmp(raw.data(), b.raw.data(), length) == 0);
}
bool operator!=(const Ssid& b) const {
return !operator==(b);
}
};
static_assert(sizeof(Ssid) == 0x22, "Ssid is an invalid size");
struct Ipv4Address {
union {
u32 raw{};
std::array<u8, 4> bytes;
};
std::string GetStringValue() const {
return fmt::format("{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
}
};
using Ipv4Address = std::array<u8, 4>;
static_assert(sizeof(Ipv4Address) == 0x4, "Ipv4Address is an invalid size");
struct MacAddress {
@ -181,6 +187,14 @@ struct MacAddress {
};
static_assert(sizeof(MacAddress) == 0x6, "MacAddress is an invalid size");
struct MACAddressHash {
size_t operator()(const MacAddress& address) const {
u64 value{};
std::memcpy(&value, address.raw.data(), sizeof(address.raw));
return value;
}
};
struct ScanFilter {
NetworkId network_id;
NetworkType network_type;

View File

@ -188,7 +188,7 @@ std::vector<NetworkInterface> GetAvailableNetworkInterfaces() {
std::optional<NetworkInterface> GetSelectedNetworkInterface() {
const auto& selected_network_interface = Settings::values.network_interface.GetValue();
const auto network_interfaces = Network::GetAvailableNetworkInterfaces();
if (network_interfaces.size() == 0) {
if (network_interfaces.empty()) {
LOG_ERROR(Network, "GetAvailableNetworkInterfaces returned no interfaces");
return std::nullopt;
}
@ -206,4 +206,14 @@ std::optional<NetworkInterface> GetSelectedNetworkInterface() {
return *res;
}
void SelectFirstNetworkInterface() {
const auto network_interfaces = Network::GetAvailableNetworkInterfaces();
if (network_interfaces.empty()) {
return;
}
Settings::values.network_interface.SetValue(network_interfaces[0].name);
}
} // namespace Network

View File

@ -24,5 +24,6 @@ struct NetworkInterface {
std::vector<NetworkInterface> GetAvailableNetworkInterfaces();
std::optional<NetworkInterface> GetSelectedNetworkInterface();
void SelectFirstNetworkInterface();
} // namespace Network

View File

@ -6,6 +6,7 @@
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/zstd_compression.h"
#include "core/internal_network/network.h"
#include "core/internal_network/network_interface.h"
#include "core/internal_network/socket_proxy.h"
@ -32,8 +33,11 @@ void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
return;
}
auto decompressed = packet;
decompressed.data = Common::Compression::DecompressDataZSTD(packet.data);
std::lock_guard guard(packets_mutex);
received_packets.push(packet);
received_packets.push(decompressed);
}
template <typename T>
@ -185,6 +189,8 @@ std::pair<s32, Errno> ProxySocket::Send(const std::vector<u8>& message, int flag
void ProxySocket::SendPacket(ProxyPacket& packet) {
if (auto room_member = room_network.GetRoomMember().lock()) {
if (room_member->IsConnected()) {
packet.data = Common::Compression::CompressDataZSTDDefault(packet.data.data(),
packet.data.size());
room_member->SendProxyPacket(packet);
}
}

View File

@ -76,7 +76,18 @@ static constexpr char BanListMagic[] = "YuzuRoom-BanList-1";
static constexpr char token_delimiter{':'};
static void PadToken(std::string& token) {
while (token.size() % 4 != 0) {
std::size_t outlen = 0;
std::array<unsigned char, 512> output{};
std::array<unsigned char, 2048> roundtrip{};
for (size_t i = 0; i < 3; i++) {
mbedtls_base64_decode(output.data(), output.size(), &outlen,
reinterpret_cast<const unsigned char*>(token.c_str()),
token.length());
mbedtls_base64_encode(roundtrip.data(), roundtrip.size(), &outlen, output.data(), outlen);
if (memcmp(roundtrip.data(), token.data(), token.size()) == 0) {
break;
}
token.push_back('=');
}
}

View File

@ -211,6 +211,12 @@ public:
*/
void HandleProxyPacket(const ENetEvent* event);
/**
* Broadcasts this packet to all members except the sender.
* @param event The ENet event containing the data
*/
void HandleLdnPacket(const ENetEvent* event);
/**
* Extracts a chat entry from a received ENet packet and adds it to the chat queue.
* @param event The ENet event that was received.
@ -247,6 +253,9 @@ void Room::RoomImpl::ServerLoop() {
case IdProxyPacket:
HandleProxyPacket(&event);
break;
case IdLdnPacket:
HandleLdnPacket(&event);
break;
case IdChatMessage:
HandleChatPacket(&event);
break;
@ -861,6 +870,60 @@ void Room::RoomImpl::HandleProxyPacket(const ENetEvent* event) {
enet_host_flush(server);
}
void Room::RoomImpl::HandleLdnPacket(const ENetEvent* event) {
Packet in_packet;
in_packet.Append(event->packet->data, event->packet->dataLength);
in_packet.IgnoreBytes(sizeof(u8)); // Message type
in_packet.IgnoreBytes(sizeof(u8)); // LAN packet type
in_packet.IgnoreBytes(sizeof(IPv4Address)); // Local IP
IPv4Address remote_ip;
in_packet.Read(remote_ip); // Remote IP
bool broadcast;
in_packet.Read(broadcast); // Broadcast
Packet out_packet;
out_packet.Append(event->packet->data, event->packet->dataLength);
ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(),
ENET_PACKET_FLAG_RELIABLE);
const auto& destination_address = remote_ip;
if (broadcast) { // Send the data to everyone except the sender
std::lock_guard lock(member_mutex);
bool sent_packet = false;
for (const auto& member : members) {
if (member.peer != event->peer) {
sent_packet = true;
enet_peer_send(member.peer, 0, enet_packet);
}
}
if (!sent_packet) {
enet_packet_destroy(enet_packet);
}
} else {
std::lock_guard lock(member_mutex);
auto member = std::find_if(members.begin(), members.end(),
[destination_address](const Member& member_entry) -> bool {
return member_entry.fake_ip == destination_address;
});
if (member != members.end()) {
enet_peer_send(member->peer, 0, enet_packet);
} else {
LOG_ERROR(Network,
"Attempting to send to unknown IP address: "
"{}.{}.{}.{}",
destination_address[0], destination_address[1], destination_address[2],
destination_address[3]);
enet_packet_destroy(enet_packet);
}
}
enet_host_flush(server);
}
void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) {
Packet in_packet;
in_packet.Append(event->packet->data, event->packet->dataLength);

View File

@ -40,6 +40,7 @@ enum RoomMessageTypes : u8 {
IdRoomInformation,
IdSetGameInfo,
IdProxyPacket,
IdLdnPacket,
IdChatMessage,
IdNameCollision,
IdIpCollision,

View File

@ -58,6 +58,7 @@ public:
private:
CallbackSet<ProxyPacket> callback_set_proxy_packet;
CallbackSet<LDNPacket> callback_set_ldn_packet;
CallbackSet<ChatEntry> callback_set_chat_messages;
CallbackSet<StatusMessageEntry> callback_set_status_messages;
CallbackSet<RoomInformation> callback_set_room_information;
@ -107,6 +108,12 @@ public:
*/
void HandleProxyPackets(const ENetEvent* event);
/**
* Extracts an LdnPacket from a received ENet packet.
* @param event The ENet event that was received.
*/
void HandleLdnPackets(const ENetEvent* event);
/**
* Extracts a chat entry from a received ENet packet and adds it to the chat queue.
* @param event The ENet event that was received.
@ -166,6 +173,9 @@ void RoomMember::RoomMemberImpl::MemberLoop() {
case IdProxyPacket:
HandleProxyPackets(&event);
break;
case IdLdnPacket:
HandleLdnPackets(&event);
break;
case IdChatMessage:
HandleChatPacket(&event);
break;
@ -372,6 +382,27 @@ void RoomMember::RoomMemberImpl::HandleProxyPackets(const ENetEvent* event) {
Invoke<ProxyPacket>(proxy_packet);
}
void RoomMember::RoomMemberImpl::HandleLdnPackets(const ENetEvent* event) {
LDNPacket ldn_packet{};
Packet packet;
packet.Append(event->packet->data, event->packet->dataLength);
// Ignore the first byte, which is the message id.
packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
u8 packet_type;
packet.Read(packet_type);
ldn_packet.type = static_cast<LDNPacketType>(packet_type);
packet.Read(ldn_packet.local_ip);
packet.Read(ldn_packet.remote_ip);
packet.Read(ldn_packet.broadcast);
packet.Read(ldn_packet.data);
Invoke<LDNPacket>(ldn_packet);
}
void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
Packet packet;
packet.Append(event->packet->data, event->packet->dataLength);
@ -449,6 +480,11 @@ RoomMember::RoomMemberImpl::CallbackSet<ProxyPacket>& RoomMember::RoomMemberImpl
return callback_set_proxy_packet;
}
template <>
RoomMember::RoomMemberImpl::CallbackSet<LDNPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
return callback_set_ldn_packet;
}
template <>
RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
RoomMember::RoomMemberImpl::Callbacks::Get() {
@ -607,6 +643,21 @@ void RoomMember::SendProxyPacket(const ProxyPacket& proxy_packet) {
room_member_impl->Send(std::move(packet));
}
void RoomMember::SendLdnPacket(const LDNPacket& ldn_packet) {
Packet packet;
packet.Write(static_cast<u8>(IdLdnPacket));
packet.Write(static_cast<u8>(ldn_packet.type));
packet.Write(ldn_packet.local_ip);
packet.Write(ldn_packet.remote_ip);
packet.Write(ldn_packet.broadcast);
packet.Write(ldn_packet.data);
room_member_impl->Send(std::move(packet));
}
void RoomMember::SendChatMessage(const std::string& message) {
Packet packet;
packet.Write(static_cast<u8>(IdChatMessage));
@ -663,6 +714,11 @@ RoomMember::CallbackHandle<ProxyPacket> RoomMember::BindOnProxyPacketReceived(
return room_member_impl->Bind(callback);
}
RoomMember::CallbackHandle<LDNPacket> RoomMember::BindOnLdnPacketReceived(
std::function<void(const LDNPacket&)> callback) {
return room_member_impl->Bind(std::move(callback));
}
RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
std::function<void(const RoomInformation&)> callback) {
return room_member_impl->Bind(callback);
@ -699,6 +755,7 @@ void RoomMember::Leave() {
}
template void RoomMember::Unbind(CallbackHandle<ProxyPacket>);
template void RoomMember::Unbind(CallbackHandle<LDNPacket>);
template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
template void RoomMember::Unbind(CallbackHandle<RoomInformation>);

View File

@ -17,7 +17,24 @@ namespace Network {
using AnnounceMultiplayerRoom::GameInfo;
using AnnounceMultiplayerRoom::RoomInformation;
/// Information about the received WiFi packets.
enum class LDNPacketType : u8 {
Scan,
ScanResp,
Connect,
SyncNetwork,
Disconnect,
DestroyNetwork,
};
struct LDNPacket {
LDNPacketType type;
IPv4Address local_ip;
IPv4Address remote_ip;
bool broadcast;
std::vector<u8> data;
};
/// Information about the received proxy packets.
struct ProxyPacket {
SockAddrIn local_endpoint;
SockAddrIn remote_endpoint;
@ -151,6 +168,12 @@ public:
*/
void SendProxyPacket(const ProxyPacket& packet);
/**
* Sends an LDN packet to the room.
* @param packet The WiFi packet to send.
*/
void SendLdnPacket(const LDNPacket& packet);
/**
* Sends a chat message to the room.
* @param message The contents of the message.
@ -204,6 +227,16 @@ public:
CallbackHandle<ProxyPacket> BindOnProxyPacketReceived(
std::function<void(const ProxyPacket&)> callback);
/**
* Binds a function to an event that will be triggered every time an LDNPacket is received.
* The function wil be called everytime the event is triggered.
* The callback function must not bind or unbind a function. Doing so will cause a deadlock
* @param callback The function to call
* @return A handle used for removing the function from the registered list
*/
CallbackHandle<LDNPacket> BindOnLdnPacketReceived(
std::function<void(const LDNPacket&)> callback);
/**
* Binds a function to an event that will be triggered every time the RoomInformation changes.
* The function wil be called every time the event is triggered.

View File

@ -899,8 +899,8 @@ void GMainWindow::InitializeWidgets() {
}
// TODO (flTobi): Add the widget when multiplayer is fully implemented
// statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
// statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
tas_label = new QLabel();
tas_label->setObjectName(QStringLiteral("TASlabel"));
@ -1299,6 +1299,7 @@ void GMainWindow::ConnectMenuEvents() {
&MultiplayerState::OnDirectConnectToRoom);
connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state,
&MultiplayerState::OnOpenNetworkRoom);
connect(multiplayer_state, &MultiplayerState::SaveConfig, this, &GMainWindow::OnSaveConfig);
// Tools
connect_menu(ui->action_Rederive, std::bind(&GMainWindow::OnReinitializeKeys, this,
@ -1339,6 +1340,8 @@ void GMainWindow::UpdateMenuState() {
} else {
ui->action_Pause->setText(tr("&Pause"));
}
multiplayer_state->UpdateNotificationStatus();
}
void GMainWindow::OnDisplayTitleBars(bool show) {
@ -2770,6 +2773,11 @@ void GMainWindow::OnExit() {
OnStopGame();
}
void GMainWindow::OnSaveConfig() {
system->ApplySettings();
config->Save();
}
void GMainWindow::ErrorDisplayDisplayError(QString error_code, QString error_text) {
OverlayDialog dialog(render_window, *system, error_code, error_text, QString{}, tr("OK"),
Qt::AlignLeft | Qt::AlignVCenter);

View File

@ -169,6 +169,7 @@ public slots:
void OnLoadComplete();
void OnExecuteProgram(std::size_t program_index);
void OnExit();
void OnSaveConfig();
void ControllerSelectorReconfigureControllers(
const Core::Frontend::ControllerParameters& parameters);
void SoftwareKeyboardInitialize(

View File

@ -120,6 +120,20 @@
<addaction name="menu_Reset_Window_Size"/>
<addaction name="menu_View_Debugging"/>
</widget>
<widget class="QMenu" name="menu_Multiplayer">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>&amp;Multiplayer</string>
</property>
<addaction name="action_View_Lobby"/>
<addaction name="action_Start_Room"/>
<addaction name="action_Connect_To_Room"/>
<addaction name="separator"/>
<addaction name="action_Show_Room"/>
<addaction name="action_Leave_Room"/>
</widget>
<widget class="QMenu" name="menu_Tools">
<property name="title">
<string>&amp;Tools</string>
@ -251,7 +265,7 @@
<bool>true</bool>
</property>
<property name="text">
<string>Browse Public Game Lobby</string>
<string>&amp;Browse Public Game Lobby</string>
</property>
</action>
<action name="action_Start_Room">
@ -259,7 +273,7 @@
<bool>true</bool>
</property>
<property name="text">
<string>Create Room</string>
<string>&amp;Create Room</string>
</property>
</action>
<action name="action_Leave_Room">
@ -267,12 +281,12 @@
<bool>false</bool>
</property>
<property name="text">
<string>Leave Room</string>
<string>&amp;Leave Room</string>
</property>
</action>
<action name="action_Connect_To_Room">
<property name="text">
<string>Direct Connect to Room</string>
<string>&amp;Direct Connect to Room</string>
</property>
</action>
<action name="action_Show_Room">
@ -280,7 +294,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Show Current Room</string>
<string>&amp;Show Current Room</string>
</property>
</action>
<action name="action_Fullscreen">

View File

@ -61,7 +61,10 @@ public:
/// Format the message using the players color
QString GetPlayerChatMessage(u16 player) const {
auto color = player_color[player % 16];
const bool is_dark_theme = QIcon::themeName().contains(QStringLiteral("dark")) ||
QIcon::themeName().contains(QStringLiteral("midnight"));
auto color =
is_dark_theme ? player_color_dark[player % 16] : player_color_default[player % 16];
QString name;
if (username.isEmpty() || username == nickname) {
name = nickname;
@ -84,9 +87,12 @@ public:
}
private:
static constexpr std::array<const char*, 16> player_color = {
static constexpr std::array<const char*, 16> player_color_default = {
{"#0000FF", "#FF0000", "#8A2BE2", "#FF69B4", "#1E90FF", "#008000", "#00FF7F", "#B22222",
"#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "FFFF00"}};
"#DAA520", "#FF4500", "#2E8B57", "#5F9EA0", "#D2691E", "#9ACD32", "#FF7F50", "#FFFF00"}};
static constexpr std::array<const char*, 16> player_color_dark = {
{"#559AD1", "#4EC9A8", "#D69D85", "#C6C923", "#B975B5", "#D81F1F", "#7EAE39", "#4F8733",
"#F7CD8A", "#6FCACF", "#CE4897", "#8A2BE2", "#D2691E", "#9ACD32", "#FF7F50", "#152ccd"}};
static constexpr char ping_color[] = "#FFFF00";
QString timestamp;

View File

@ -97,8 +97,9 @@ void ClientRoomWindow::UpdateView() {
auto memberlist = member->GetMemberInformation();
ui->chat->SetPlayerList(memberlist);
const auto information = member->GetRoomInformation();
setWindowTitle(QString(tr("%1 (%2/%3 members) - connected"))
setWindowTitle(QString(tr("%1 - %2 (%3/%4 members) - connected"))
.arg(QString::fromStdString(information.name))
.arg(QString::fromStdString(information.preferred_game.name))
.arg(memberlist.size())
.arg(information.member_slots));
ui->description->setText(QString::fromStdString(information.description));

View File

@ -106,6 +106,8 @@ void DirectConnectWindow::Connect() {
UISettings::values.multiplayer_port = UISettings::values.multiplayer_port.GetDefault();
}
emit SaveConfig();
// attempt to connect in a different thread
QFuture<void> f = QtConcurrent::run([&] {
if (auto room_member = room_network.GetRoomMember().lock()) {

View File

@ -31,6 +31,7 @@ signals:
* connections that it might have.
*/
void Closed();
void SaveConfig();
private slots:
void OnConnection();

View File

@ -232,6 +232,7 @@ void HostRoomWindow::Host() {
}
UISettings::values.multiplayer_room_description = ui->room_description->toPlainText();
ui->host->setEnabled(true);
emit SaveConfig();
close();
}
}

View File

@ -46,6 +46,9 @@ public:
void UpdateGameList(QStandardItemModel* list);
void RetranslateUi();
signals:
void SaveConfig();
private:
void Host();
std::unique_ptr<Network::VerifyUser::Backend> CreateVerifyBackend(bool use_validation) const;

View File

@ -7,6 +7,7 @@
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/internal_network/network_interface.h"
#include "network/network.h"
#include "ui_lobby.h"
@ -26,9 +27,9 @@
Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
std::shared_ptr<Core::AnnounceMultiplayerSession> session, Core::System& system_)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
ui(std::make_unique<Ui::Lobby>()),
announce_multiplayer_session(session), system{system_}, room_network{
system.GetRoomNetwork()} {
ui(std::make_unique<Ui::Lobby>()), announce_multiplayer_session(session),
profile_manager(std::make_unique<Service::Account::ProfileManager>()), system{system_},
room_network{system.GetRoomNetwork()} {
ui->setupUi(this);
// setup the watcher for background connections
@ -60,9 +61,17 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
ui->nickname->setValidator(validation.GetNickname());
ui->nickname->setText(UISettings::values.multiplayer_nickname.GetValue());
if (ui->nickname->text().isEmpty() && !Settings::values.yuzu_username.GetValue().empty()) {
// Use yuzu Web Service user name as nickname by default
ui->nickname->setText(QString::fromStdString(Settings::values.yuzu_username.GetValue()));
// Try find the best nickname by default
if (ui->nickname->text().isEmpty() || ui->nickname->text() == QStringLiteral("yuzu")) {
if (!Settings::values.yuzu_username.GetValue().empty()) {
ui->nickname->setText(
QString::fromStdString(Settings::values.yuzu_username.GetValue()));
} else if (!GetProfileUsername().empty()) {
ui->nickname->setText(QString::fromStdString(GetProfileUsername()));
} else {
ui->nickname->setText(QStringLiteral("yuzu"));
}
}
// UI Buttons
@ -76,12 +85,6 @@ Lobby::Lobby(QWidget* parent, QStandardItemModel* list,
// Actions
connect(&room_list_watcher, &QFutureWatcher<AnnounceMultiplayerRoom::RoomList>::finished, this,
&Lobby::OnRefreshLobby);
// manually start a refresh when the window is opening
// TODO(jroweboy): if this refresh is slow for people with bad internet, then don't do it as
// part of the constructor, but offload the refresh until after the window shown. perhaps emit a
// refreshroomlist signal from places that open the lobby
RefreshLobby();
}
Lobby::~Lobby() = default;
@ -96,6 +99,7 @@ void Lobby::UpdateGameList(QStandardItemModel* list) {
}
if (proxy)
proxy->UpdateGameList(game_list);
ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
}
void Lobby::RetranslateUi() {
@ -116,6 +120,11 @@ void Lobby::OnExpandRoom(const QModelIndex& index) {
}
void Lobby::OnJoinRoom(const QModelIndex& source) {
if (!Network::GetSelectedNetworkInterface()) {
LOG_INFO(WebService, "Automatically selected network interface for room network.");
Network::SelectFirstNetworkInterface();
}
if (!Network::GetSelectedNetworkInterface()) {
NetworkMessage::ErrorManager::ShowError(
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
@ -197,16 +206,16 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
proxy->data(connection_index, LobbyItemHost::HostIPRole).toString();
UISettings::values.multiplayer_port =
proxy->data(connection_index, LobbyItemHost::HostPortRole).toInt();
emit SaveConfig();
}
void Lobby::ResetModel() {
model->clear();
model->insertColumns(0, Column::TOTAL);
model->setHeaderData(Column::EXPAND, Qt::Horizontal, QString(), Qt::DisplayRole);
model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
model->setHeaderData(Column::ROOM_NAME, Qt::Horizontal, tr("Room Name"), Qt::DisplayRole);
model->setHeaderData(Column::GAME_NAME, Qt::Horizontal, tr("Preferred Game"), Qt::DisplayRole);
model->setHeaderData(Column::HOST, Qt::Horizontal, tr("Host"), Qt::DisplayRole);
model->setHeaderData(Column::MEMBER, Qt::Horizontal, tr("Players"), Qt::DisplayRole);
}
void Lobby::RefreshLobby() {
@ -229,6 +238,7 @@ void Lobby::OnRefreshLobby() {
for (int r = 0; r < game_list->rowCount(); ++r) {
auto index = game_list->index(r, 0);
auto game_id = game_list->data(index, GameListItemPath::ProgramIdRole).toULongLong();
if (game_id != 0 && room.information.preferred_game.id == game_id) {
smdh_icon = game_list->data(index, Qt::DecorationRole).value<QPixmap>();
}
@ -243,17 +253,16 @@ void Lobby::OnRefreshLobby() {
members.append(var);
}
auto first_item = new LobbyItem();
auto first_item = new LobbyItemGame(
room.information.preferred_game.id,
QString::fromStdString(room.information.preferred_game.name), smdh_icon);
auto row = QList<QStandardItem*>({
first_item,
new LobbyItemName(room.has_password, QString::fromStdString(room.information.name)),
new LobbyItemGame(room.information.preferred_game.id,
QString::fromStdString(room.information.preferred_game.name),
smdh_icon),
new LobbyItemMemberList(members, room.information.member_slots),
new LobbyItemHost(QString::fromStdString(room.information.host_username),
QString::fromStdString(room.ip), room.information.port,
QString::fromStdString(room.verify_uid)),
new LobbyItemMemberList(members, room.information.member_slots),
});
model->appendRow(row);
// To make the rows expandable, add the member data as a child of the first column of the
@ -283,6 +292,26 @@ void Lobby::OnRefreshLobby() {
ui->room_list->setFirstColumnSpanned(j, proxy->index(i, 0), true);
}
}
ui->room_list->sortByColumn(Column::GAME_NAME, Qt::AscendingOrder);
}
std::string Lobby::GetProfileUsername() {
const auto& current_user = profile_manager->GetUser(Settings::values.current_user.GetValue());
Service::Account::ProfileBase profile{};
if (!current_user.has_value()) {
return "";
}
if (!profile_manager->GetProfileBase(*current_user, profile)) {
return "";
}
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
return text;
}
LobbyFilterProxyModel::LobbyFilterProxyModel(QWidget* parent, QStandardItemModel* list)

View File

@ -24,6 +24,10 @@ namespace Core {
class System;
}
namespace Service::Account {
class ProfileManager;
}
/**
* Listing of all public games pulled from services. The lobby should be simple enough for users to
* find the game they want to play, and join it.
@ -75,8 +79,11 @@ private slots:
signals:
void StateChanged(const Network::RoomMember::State&);
void SaveConfig();
private:
std::string GetProfileUsername();
/**
* Removes all entries in the Lobby before refreshing.
*/
@ -96,6 +103,7 @@ private:
QFutureWatcher<AnnounceMultiplayerRoom::RoomList> room_list_watcher;
std::weak_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
std::unique_ptr<Service::Account::ProfileManager> profile_manager;
QFutureWatcher<void>* watcher;
Validation validation;
Core::System& system;

View File

@ -11,11 +11,10 @@
namespace Column {
enum List {
EXPAND,
ROOM_NAME,
GAME_NAME,
HOST,
ROOM_NAME,
MEMBER,
HOST,
TOTAL,
};
}
@ -91,6 +90,8 @@ public:
setData(game_name, GameNameRole);
if (!smdh_icon.isNull()) {
setData(smdh_icon, GameIconRole);
} else {
setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(32), GameIconRole);
}
}
@ -98,7 +99,12 @@ public:
if (role == Qt::DecorationRole) {
auto val = data(GameIconRole);
if (val.isValid()) {
val = val.value<QPixmap>().scaled(16, 16, Qt::KeepAspectRatio);
val = val.value<QPixmap>().scaled(32, 32, Qt::KeepAspectRatio,
Qt::TransformationMode::SmoothTransformation);
} else {
auto blank_image = QPixmap(32, 32);
blank_image.fill(Qt::black);
val = blank_image;
}
return val;
} else if (role != Qt::DisplayRole) {
@ -191,8 +197,8 @@ public:
return LobbyItem::data(role);
}
auto members = data(MemberListRole).toList();
return QStringLiteral("%1 / %2").arg(QString::number(members.size()),
data(MaxPlayerRole).toString());
return QStringLiteral("%1 / %2 ")
.arg(QString::number(members.size()), data(MaxPlayerRole).toString());
}
bool operator<(const QStandardItem& other) const override {

View File

@ -49,8 +49,8 @@ const ConnectionError ErrorManager::PERMISSION_DENIED(
QT_TR_NOOP("You do not have enough permission to perform this action."));
const ConnectionError ErrorManager::NO_SUCH_USER(QT_TR_NOOP(
"The user you are trying to kick/ban could not be found.\nThey may have left the room."));
const ConnectionError ErrorManager::NO_INTERFACE_SELECTED(
QT_TR_NOOP("No network interface is selected.\nPlease go to Configure -> System -> Network and "
const ConnectionError ErrorManager::NO_INTERFACE_SELECTED(QT_TR_NOOP(
"No valid network interface is selected.\nPlease go to Configure -> System -> Network and "
"make a selection."));
static bool WarnMessage(const std::string& title, const std::string& text) {

View File

@ -44,9 +44,6 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis
status_text = new ClickableLabel(this);
status_icon = new ClickableLabel(this);
status_text->setToolTip(tr("Current connection status"));
status_text->setText(tr("Not Connected. Click here to find a room!"));
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
connect(status_text, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
connect(status_icon, &ClickableLabel::clicked, this, &MultiplayerState::OnOpenNetworkRoom);
@ -57,6 +54,8 @@ MultiplayerState::MultiplayerState(QWidget* parent, QStandardItemModel* game_lis
HideNotification();
}
});
retranslateUi();
}
MultiplayerState::~MultiplayerState() = default;
@ -90,14 +89,7 @@ void MultiplayerState::Close() {
void MultiplayerState::retranslateUi() {
status_text->setToolTip(tr("Current connection status"));
if (current_state == Network::RoomMember::State::Uninitialized) {
status_text->setText(tr("Not Connected. Click here to find a room!"));
} else if (current_state == Network::RoomMember::State::Joined ||
current_state == Network::RoomMember::State::Moderator) {
status_text->setText(tr("Connected"));
} else {
status_text->setText(tr("Not Connected"));
}
UpdateNotificationStatus();
if (lobby) {
lobby->RetranslateUi();
@ -113,21 +105,55 @@ void MultiplayerState::retranslateUi() {
}
}
void MultiplayerState::SetNotificationStatus(NotificationStatus status) {
notification_status = status;
UpdateNotificationStatus();
}
void MultiplayerState::UpdateNotificationStatus() {
switch (notification_status) {
case NotificationStatus::Unitialized:
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
status_text->setText(tr("Not Connected. Click here to find a room!"));
leave_room->setEnabled(false);
show_room->setEnabled(false);
break;
case NotificationStatus::Disconnected:
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
status_text->setText(tr("Not Connected"));
leave_room->setEnabled(false);
show_room->setEnabled(false);
break;
case NotificationStatus::Connected:
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
status_text->setText(tr("Connected"));
leave_room->setEnabled(true);
show_room->setEnabled(true);
break;
case NotificationStatus::Notification:
status_icon->setPixmap(
QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
status_text->setText(tr("New Messages Received"));
leave_room->setEnabled(true);
show_room->setEnabled(true);
break;
}
// Clean up status bar if game is running
if (system.IsPoweredOn()) {
status_text->clear();
}
}
void MultiplayerState::OnNetworkStateChanged(const Network::RoomMember::State& state) {
LOG_DEBUG(Frontend, "Network State: {}", Network::GetStateStr(state));
if (state == Network::RoomMember::State::Joined ||
state == Network::RoomMember::State::Moderator) {
OnOpenNetworkRoom();
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
status_text->setText(tr("Connected"));
leave_room->setEnabled(true);
show_room->setEnabled(true);
SetNotificationStatus(NotificationStatus::Connected);
} else {
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("disconnected")).pixmap(16));
status_text->setText(tr("Not Connected"));
leave_room->setEnabled(false);
show_room->setEnabled(false);
SetNotificationStatus(NotificationStatus::Disconnected);
}
current_state = state;
@ -185,6 +211,10 @@ void MultiplayerState::OnAnnounceFailed(const WebService::WebResult& result) {
QMessageBox::Ok);
}
void MultiplayerState::OnSaveConfig() {
emit SaveConfig();
}
void MultiplayerState::UpdateThemedIcons() {
if (show_notification) {
status_icon->setPixmap(
@ -209,13 +239,16 @@ static void BringWidgetToFront(QWidget* widget) {
void MultiplayerState::OnViewLobby() {
if (lobby == nullptr) {
lobby = new Lobby(this, game_list_model, announce_multiplayer_session, system);
connect(lobby, &Lobby::SaveConfig, this, &MultiplayerState::OnSaveConfig);
}
lobby->RefreshLobby();
BringWidgetToFront(lobby);
}
void MultiplayerState::OnCreateRoom() {
if (host_room == nullptr) {
host_room = new HostRoomWindow(this, game_list_model, announce_multiplayer_session, system);
connect(host_room, &HostRoomWindow::SaveConfig, this, &MultiplayerState::OnSaveConfig);
}
BringWidgetToFront(host_room);
}
@ -249,14 +282,13 @@ void MultiplayerState::ShowNotification() {
return; // Do not show notification if the chat window currently has focus
show_notification = true;
QApplication::alert(nullptr);
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected_notification")).pixmap(16));
status_text->setText(tr("New Messages Received"));
QApplication::beep();
SetNotificationStatus(NotificationStatus::Notification);
}
void MultiplayerState::HideNotification() {
show_notification = false;
status_icon->setPixmap(QIcon::fromTheme(QStringLiteral("connected")).pixmap(16));
status_text->setText(tr("Connected"));
SetNotificationStatus(NotificationStatus::Connected);
}
void MultiplayerState::OnOpenNetworkRoom() {
@ -279,6 +311,8 @@ void MultiplayerState::OnOpenNetworkRoom() {
void MultiplayerState::OnDirectConnectToRoom() {
if (direct_connect == nullptr) {
direct_connect = new DirectConnectWindow(system, this);
connect(direct_connect, &DirectConnectWindow::SaveConfig, this,
&MultiplayerState::OnSaveConfig);
}
BringWidgetToFront(direct_connect);
}

View File

@ -22,6 +22,13 @@ class MultiplayerState : public QWidget {
Q_OBJECT;
public:
enum class NotificationStatus {
Unitialized,
Disconnected,
Connected,
Notification,
};
explicit MultiplayerState(QWidget* parent, QStandardItemModel* game_list, QAction* leave_room,
QAction* show_room, Core::System& system_);
~MultiplayerState();
@ -31,6 +38,10 @@ public:
*/
void Close();
void SetNotificationStatus(NotificationStatus state);
void UpdateNotificationStatus();
ClickableLabel* GetStatusText() const {
return status_text;
}
@ -64,6 +75,7 @@ public slots:
void OnOpenNetworkRoom();
void OnDirectConnectToRoom();
void OnAnnounceFailed(const WebService::WebResult&);
void OnSaveConfig();
void UpdateThemedIcons();
void ShowNotification();
void HideNotification();
@ -72,6 +84,7 @@ signals:
void NetworkStateChanged(const Network::RoomMember::State&);
void NetworkError(const Network::RoomMember::Error&);
void AnnounceFailed(const WebService::WebResult&);
void SaveConfig();
private:
Lobby* lobby = nullptr;
@ -85,6 +98,7 @@ private:
QAction* show_room;
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
Network::RoomMember::State current_state = Network::RoomMember::State::Uninitialized;
NotificationStatus notification_status = NotificationStatus::Unitialized;
bool has_mod_perms = false;
Network::RoomMember::CallbackHandle<Network::RoomMember::State> state_callback_handle;
Network::RoomMember::CallbackHandle<Network::RoomMember::Error> error_callback_handle;

View File

@ -102,7 +102,7 @@ struct Values {
Settings::Setting<uint32_t> callout_flags{0, "calloutFlags"};
// multiplayer settings
Settings::Setting<QString> multiplayer_nickname{QStringLiteral("yuzu"), "nickname"};
Settings::Setting<QString> multiplayer_nickname{{}, "nickname"};
Settings::Setting<QString> multiplayer_ip{{}, "ip"};
Settings::SwitchableSetting<uint, true> multiplayer_port{24872, 0, UINT16_MAX, "port"};
Settings::Setting<QString> multiplayer_room_nickname{{}, "room_nickname"};