Преглед изворни кода

Merge pull request #8876 from FearlessTobi/multiplayer-part3

ldn: Implement "local wireless" networked multiplayer
bunnei пре 3 година
родитељ
комит
2a752bbd64

+ 2 - 0
src/core/CMakeLists.txt

@@ -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

+ 2 - 1
src/core/hle/service/hid/hid.cpp

@@ -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)
 

+ 633 - 0
src/core/hle/service/ldn/lan_discovery.cpp

@@ -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

+ 134 - 0
src/core/hle/service/ldn/lan_discovery.h

@@ -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

+ 128 - 99
src/core/hle/service/ldn/ldn.cpp

@@ -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 GetDisconnectReason(Kernel::HLERequestContext& ctx) {
-        const auto disconnect_reason = DisconnectReason::None;
+    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;
+        }
 
-        LOG_WARNING(Service_LDN, "(STUBBED) called, disconnect_reason={}", disconnect_reason);
+        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) {
         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>()};
+        CreateNetworkImpl(ctx);
+    }
 
-        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);
+    void CreateNetworkPrivate(Kernel::HLERequestContext& ctx) {
+        LOG_INFO(Service_LDN, "called");
 
-        IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(ResultSuccess);
+        CreateNetworkImpl(ctx, true);
     }
 
-    void CreateNetworkPrivate(Kernel::HLERequestContext& ctx) {
+    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={}, "
-                    "local_communication_version={}",
-                    parameters.security_config.passphrase_size,
-                    parameters.security_config.security_mode,
-                    parameters.local_communication_version);
+        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);
 
         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;
         }
 
+        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;
-        // TODO (flTobi): Change this to ResultSuccess when LDN is fully implemented
-        return ResultAirplaneModeEnabled;
+        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{};
 };

+ 31 - 17
src/core/hle/service/ldn/ldn_types.h

@@ -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());
     }
-};
-static_assert(sizeof(Ssid) == 0x22, "Ssid is an invalid size");
 
-struct Ipv4Address {
-    union {
-        u32 raw{};
-        std::array<u8, 4> bytes;
-    };
+    bool operator==(const Ssid& b) const {
+        return (length == b.length) && (std::memcmp(raw.data(), b.raw.data(), length) == 0);
+    }
 
-    std::string GetStringValue() const {
-        return fmt::format("{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
+    bool operator!=(const Ssid& b) const {
+        return !operator==(b);
     }
 };
+static_assert(sizeof(Ssid) == 0x22, "Ssid is an invalid size");
+
+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;

+ 11 - 1
src/core/internal_network/network_interface.cpp

@@ -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

+ 1 - 0
src/core/internal_network/network_interface.h

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

+ 7 - 1
src/core/internal_network/socket_proxy.cpp

@@ -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);
         }
     }

+ 12 - 1
src/dedicated_room/yuzu_room.cpp

@@ -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('=');
     }
 }

+ 63 - 0
src/network/room.cpp

@@ -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);

+ 1 - 0
src/network/room.h

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

+ 57 - 0
src/network/room_member.cpp

@@ -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>);

+ 34 - 1
src/network/room_member.h

@@ -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.

+ 10 - 2
src/yuzu/main.cpp

@@ -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);

+ 1 - 0
src/yuzu/main.h

@@ -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(

+ 19 - 5
src/yuzu/main.ui

@@ -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">

+ 9 - 3
src/yuzu/multiplayer/chat_room.cpp

@@ -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;

+ 2 - 1
src/yuzu/multiplayer/client_room.cpp

@@ -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));

+ 2 - 0
src/yuzu/multiplayer/direct_connect.cpp

@@ -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()) {

+ 1 - 0
src/yuzu/multiplayer/direct_connect.h

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

+ 1 - 0
src/yuzu/multiplayer/host_room.cpp

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

+ 3 - 0
src/yuzu/multiplayer/host_room.h

@@ -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;

+ 48 - 19
src/yuzu/multiplayer/lobby.cpp

@@ -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)

+ 8 - 0
src/yuzu/multiplayer/lobby.h

@@ -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;

+ 12 - 6
src/yuzu/multiplayer/lobby_p.h

@@ -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 {

+ 3 - 3
src/yuzu/multiplayer/message.cpp

@@ -49,9 +49,9 @@ 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 "
-               "make a selection."));
+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) {
     return QMessageBox::Ok == QMessageBox::warning(nullptr, QObject::tr(title.c_str()),

+ 57 - 23
src/yuzu/multiplayer/state.cpp

@@ -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);
 }

+ 14 - 0
src/yuzu/multiplayer/state.h

@@ -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;

+ 1 - 1
src/yuzu/uisettings.h

@@ -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"};