code:
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <array>

#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>

#include "common/assert.h"
#include "common/common_types.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"

#include "common/settings.h"
#include "common/settings_enums.h"
#include "core/file_sys/control_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/loader/loader.h"
#include "core/telemetry_session.h"

#ifdef ENABLE_WEB_SERVICE
#include "web_service/telemetry_json.h"
#include "web_service/verify_login.h"
#endif

namespace Core {

namespace Telemetry = Common::Telemetry;

static u64 GenerateTelemetryId() {
    u64 telemetry_id{};

    mbedtls_entropy_context entropy;
    mbedtls_entropy_init(&entropy);
    mbedtls_ctr_drbg_context ctr_drbg;
    static constexpr std::array<char, 18> personalization{{"yuzu Telemetry ID"}};

    mbedtls_ctr_drbg_init(&ctr_drbg);
    ASSERT(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy,
                                 reinterpret_cast<const unsigned char*>(personalization.data()),
                                 personalization.size()) == 0);
    ASSERT(mbedtls_ctr_drbg_random(&ctr_drbg, reinterpret_cast<unsigned char*>(&telemetry_id),
                                   sizeof(u64)) == 0);

    mbedtls_ctr_drbg_free(&ctr_drbg);
    mbedtls_entropy_free(&entropy);

    return telemetry_id;
}

static const char* TranslateRenderer(Settings::RendererBackend backend) {
    switch (backend) {
    case Settings::RendererBackend::OpenGL:
        return "OpenGL";
    case Settings::RendererBackend::Vulkan:
        return "Vulkan";
    case Settings::RendererBackend::Null:
        return "Null";
    }
    return "Unknown";
}

static const char* TranslateGPUAccuracyLevel(Settings::GpuAccuracy backend) {
    switch (backend) {
    case Settings::GpuAccuracy::Normal:
        return "Normal";
    case Settings::GpuAccuracy::High:
        return "High";
    case Settings::GpuAccuracy::Extreme:
        return "Extreme";
    }
    return "Unknown";
}

static const char* TranslateNvdecEmulation(Settings::NvdecEmulation backend) {
    switch (backend) {
    case Settings::NvdecEmulation::Off:
        return "Off";
    case Settings::NvdecEmulation::Cpu:
        return "CPU";
    case Settings::NvdecEmulation::Gpu:
        return "GPU";
    }
    return "Unknown";
}

static constexpr const char* TranslateVSyncMode(Settings::VSyncMode mode) {
    switch (mode) {
    case Settings::VSyncMode::Immediate:
        return "Immediate";
    case Settings::VSyncMode::Mailbox:
        return "Mailbox";
    case Settings::VSyncMode::Fifo:
        return "FIFO";
    case Settings::VSyncMode::FifoRelaxed:
        return "FIFO Relaxed";
    }
    return "Unknown";
}

static constexpr const char* TranslateASTCDecodeMode(Settings::AstcDecodeMode mode) {
    switch (mode) {
    case Settings::AstcDecodeMode::Cpu:
        return "CPU";
    case Settings::AstcDecodeMode::Gpu:
        return "GPU";
    case Settings::AstcDecodeMode::CpuAsynchronous:
        return "CPU Asynchronous";
    }
    return "Unknown";
}

u64 GetTelemetryId() {
    u64 telemetry_id{};
    const auto filename = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "telemetry_id";

    bool generate_new_id = !Common::FS::Exists(filename);

    if (!generate_new_id) {
        Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Read,
                                Common::FS::FileType::BinaryFile};

        if (!file.IsOpen()) {
            LOG_ERROR(Core, "failed to open telemetry_id: {}",
                      Common::FS::PathToUTF8String(filename));
            return {};
        }

        if (!file.ReadObject(telemetry_id) || telemetry_id == 0) {
            LOG_ERROR(Frontend, "telemetry_id is 0. Generating a new one.", telemetry_id);
            generate_new_id = true;
        }
    }

    if (generate_new_id) {
        Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Write,
                                Common::FS::FileType::BinaryFile};

        if (!file.IsOpen()) {
            LOG_ERROR(Core, "failed to open telemetry_id: {}",
                      Common::FS::PathToUTF8String(filename));
            return {};
        }

        telemetry_id = GenerateTelemetryId();

        if (!file.WriteObject(telemetry_id)) {
            LOG_ERROR(Core, "Failed to write telemetry_id to file.");
        }
    }

    return telemetry_id;
}

u64 RegenerateTelemetryId() {
    const u64 new_telemetry_id{GenerateTelemetryId()};
    const auto filename = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "telemetry_id";

    Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Write,
                            Common::FS::FileType::BinaryFile};

    if (!file.IsOpen()) {
        LOG_ERROR(Core, "failed to open telemetry_id: {}", Common::FS::PathToUTF8String(filename));
        return {};
    }

    if (!file.WriteObject(new_telemetry_id)) {
        LOG_ERROR(Core, "Failed to write telemetry_id to file.");
    }

    return new_telemetry_id;
}

bool VerifyLogin(const std::string& username, const std::string& token) {
#ifdef ENABLE_WEB_SERVICE
    return WebService::VerifyLogin(Settings::values.web_api_url.GetValue(), username, token);
#else
    return false;
#endif
}

TelemetrySession::TelemetrySession() = default;

TelemetrySession::~TelemetrySession() {
    // Log one-time session end information
    const s64 shutdown_time{std::chrono::duration_cast<std::chrono::milliseconds>(
                                std::chrono::system_clock::now().time_since_epoch())
                                .count()};
    AddField(Telemetry::FieldType::Session, "Shutdown_Time", shutdown_time);

#ifdef ENABLE_WEB_SERVICE
    auto backend = std::make_unique<WebService::TelemetryJson>(
        Settings::values.web_api_url.GetValue(), Settings::values.yuzu_username.GetValue(),
        Settings::values.yuzu_token.GetValue());
#else
    auto backend = std::make_unique<Telemetry::NullVisitor>();
#endif

    // Complete the session, submitting to the web service backend if necessary
    field_collection.Accept(*backend);
    if (Settings::values.enable_telemetry) {
        backend->Complete();
    }
}

void TelemetrySession::AddInitialInfo(Loader::AppLoader& app_loader,
                                      const Service::FileSystem::FileSystemController& fsc,
                                      const FileSys::ContentProvider& content_provider) {
    // Log one-time top-level information
    AddField(Telemetry::FieldType::None, "TelemetryId", GetTelemetryId());

    // Log one-time session start information
    const s64 init_time{std::chrono::duration_cast<std::chrono::milliseconds>(
                            std::chrono::system_clock::now().time_since_epoch())
                            .count()};
    AddField(Telemetry::FieldType::Session, "Init_Time", init_time);

    u64 program_id{};
    const Loader::ResultStatus res{app_loader.ReadProgramId(program_id)};
    if (res == Loader::ResultStatus::Success) {
        const std::string formatted_program_id{fmt::format("{:016X}", program_id)};
        AddField(Telemetry::FieldType::Session, "ProgramId", formatted_program_id);

        std::string name;
        app_loader.ReadTitle(name);

        if (name.empty()) {
            const auto metadata = [&content_provider, &fsc, program_id] {
                const FileSys::PatchManager pm{program_id, fsc, content_provider};
                return pm.GetControlMetadata();
            }();
            if (metadata.first != nullptr) {
                name = metadata.first->GetApplicationName();
            }
        }

        if (!name.empty()) {
            AddField(Telemetry::FieldType::Session, "ProgramName", name);
        }
    }

    AddField(Telemetry::FieldType::Session, "ProgramFormat",
             static_cast<u8>(app_loader.GetFileType()));

    // Log application information
    Telemetry::AppendBuildInfo(field_collection);

    // Log user system information
    Telemetry::AppendCPUInfo(field_collection);
    Telemetry::AppendOSInfo(field_collection);

    // Log user configuration information
    constexpr auto field_type = Telemetry::FieldType::UserConfig;
    AddField(field_type, "Audio_SinkId",
             Settings::CanonicalizeEnum(Settings::values.sink_id.GetValue()));
    AddField(field_type, "Core_UseMultiCore", Settings::values.use_multi_core.GetValue());
    AddField(field_type, "Renderer_Backend",
             TranslateRenderer(Settings::values.renderer_backend.GetValue()));
    AddField(field_type, "Renderer_UseSpeedLimit", Settings::values.use_speed_limit.GetValue());
    AddField(field_type, "Renderer_SpeedLimit", Settings::values.speed_limit.GetValue());
    AddField(field_type, "Renderer_UseDiskShaderCache",
             Settings::values.use_disk_shader_cache.GetValue());
    AddField(field_type, "Renderer_GPUAccuracyLevel",
             TranslateGPUAccuracyLevel(Settings::values.gpu_accuracy.GetValue()));
    AddField(field_type, "Renderer_UseAsynchronousGpuEmulation",
             Settings::values.use_asynchronous_gpu_emulation.GetValue());
    AddField(field_type, "Renderer_NvdecEmulation",
             TranslateNvdecEmulation(Settings::values.nvdec_emulation.GetValue()));
    AddField(field_type, "Renderer_AccelerateASTC",
             TranslateASTCDecodeMode(Settings::values.accelerate_astc.GetValue()));
    AddField(field_type, "Renderer_UseVsync",
             TranslateVSyncMode(Settings::values.vsync_mode.GetValue()));
    AddField(field_type, "Renderer_ShaderBackend",
             static_cast<u32>(Settings::values.shader_backend.GetValue()));
    AddField(field_type, "Renderer_UseAsynchronousShaders",
             Settings::values.use_asynchronous_shaders.GetValue());
    AddField(field_type, "System_UseDockedMode", Settings::IsDockedMode());
}

bool TelemetrySession::SubmitTestcase() {
#ifdef ENABLE_WEB_SERVICE
    auto backend = std::make_unique<WebService::TelemetryJson>(
        Settings::values.web_api_url.GetValue(), Settings::values.yuzu_username.GetValue(),
        Settings::values.yuzu_token.GetValue());
    field_collection.Accept(*backend);
    return backend->SubmitTestcase();
#else
    return false;
#endif
}

} // namespace Core


code:
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <string>
#include "common/telemetry.h"

namespace FileSys {
class ContentProvider;
}

namespace Loader {
class AppLoader;
}

namespace Service::FileSystem {
class FileSystemController;
}

namespace Core {

/**
 * Instruments telemetry for this emulation session. Creates a new set of telemetry fields on each
 * session, logging any one-time fields. Interfaces with the telemetry backend used for submitting
 * data to the web service. Submits session data on close.
 */
class TelemetrySession {
public:
    explicit TelemetrySession();
    ~TelemetrySession();

    TelemetrySession(const TelemetrySession&) = delete;
    TelemetrySession& operator=(const TelemetrySession&) = delete;

    TelemetrySession(TelemetrySession&&) = delete;
    TelemetrySession& operator=(TelemetrySession&&) = delete;

    /**
     * Adds the initial telemetry info necessary when starting up a title.
     *
     * This includes information such as:
     *   - Telemetry ID
     *   - Initialization time
     *   - Title ID
     *   - Title name
     *   - Title file format
     *   - Miscellaneous settings values.
     *
     * @param app_loader       The application loader to use to retrieve
     *                         title-specific information.
     * @param fsc              Filesystem controller to use to retrieve info.
     * @param content_provider Content provider to use to retrieve info.
     */
    void AddInitialInfo(Loader::AppLoader& app_loader,
                        const Service::FileSystem::FileSystemController& fsc,
                        const FileSys::ContentProvider& content_provider);

    /**
     * Wrapper around the Telemetry::FieldCollection::AddField method.
     * @param type Type of the field to add.
     * @param name Name of the field to add.
     * @param value Value for the field to add.
     */
    template <typename T>
    void AddField(Common::Telemetry::FieldType type, const char* name, T value) {
        field_collection.AddField(type, name, std::move(value));
    }

    /**
     * Submits a Testcase.
     * @returns A bool indicating whether the submission succeeded
     */
    bool SubmitTestcase();

private:
    /// Tracks all added fields for the session
    Common::Telemetry::FieldCollection field_collection;
};

/**
 * Gets TelemetryId, a unique identifier used for the user's telemetry sessions.
 * @returns The current TelemetryId for the session.
 */
u64 GetTelemetryId();

/**
 * Regenerates TelemetryId, a unique identifier used for the user's telemetry sessions.
 * @returns The new TelemetryId that was generated.
 */
u64 RegenerateTelemetryId();

/**
 * Verifies the username and token.
 * @param username yuzu username to use for authentication.
 * @param token yuzu token to use for authentication.
 * @returns Future with bool indicating whether the verification succeeded
 */
bool VerifyLogin(const std::string& username, const std::string& token);

} // namespace Core

repo_name: litucks/torzu

license: mit


code:
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "common/assert.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/memory.h"
#include "core/tools/freezer.h"

namespace Tools {
namespace {

constexpr auto memory_freezer_ns = std::chrono::nanoseconds{1000000000 / 60};

u64 MemoryReadWidth(Core::Memory::Memory& memory, u32 width, VAddr addr) {
    switch (width) {
    case 1:
        return memory.Read8(addr);
    case 2:
        return memory.Read16(addr);
    case 4:
        return memory.Read32(addr);
    case 8:
        return memory.Read64(addr);
    default:
        UNREACHABLE();
    }
}

void MemoryWriteWidth(Core::Memory::Memory& memory, u32 width, VAddr addr, u64 value) {
    switch (width) {
    case 1:
        memory.Write8(addr, static_cast<u8>(value));
        break;
    case 2:
        memory.Write16(addr, static_cast<u16>(value));
        break;
    case 4:
        memory.Write32(addr, static_cast<u32>(value));
        break;
    case 8:
        memory.Write64(addr, value);
        break;
    default:
        UNREACHABLE();
    }
}

} // Anonymous namespace

Freezer::Freezer(Core::Timing::CoreTiming& core_timing_, Core::Memory::Memory& memory_)
    : core_timing{core_timing_}, memory{memory_} {
    event = Core::Timing::CreateEvent("MemoryFreezer::FrameCallback",
                                      [this](s64 time, std::chrono::nanoseconds ns_late)
                                          -> std::optional<std::chrono::nanoseconds> {
                                          FrameCallback(ns_late);
                                          return std::nullopt;
                                      });
    core_timing.ScheduleEvent(memory_freezer_ns, event);
}

Freezer::~Freezer() {
    core_timing.UnscheduleEvent(event);
}

void Freezer::SetActive(bool is_active) {
    if (!active.exchange(is_active)) {
        FillEntryReads();
        core_timing.ScheduleEvent(memory_freezer_ns, event);
        LOG_DEBUG(Common_Memory, "Memory freezer activated!");
    } else {
        LOG_DEBUG(Common_Memory, "Memory freezer deactivated!");
    }
}

bool Freezer::IsActive() const {
    return active.load(std::memory_order_relaxed);
}

void Freezer::Clear() {
    std::scoped_lock lock{entries_mutex};

    LOG_DEBUG(Common_Memory, "Clearing all frozen memory values.");

    entries.clear();
}

u64 Freezer::Freeze(VAddr address, u32 width) {
    std::scoped_lock lock{entries_mutex};

    const auto current_value = MemoryReadWidth(memory, width, address);
    entries.push_back({address, width, current_value});

    LOG_DEBUG(Common_Memory,
              "Freezing memory for address={:016X}, width={:02X}, current_value={:016X}", address,
              width, current_value);

    return current_value;
}

void Freezer::Unfreeze(VAddr address) {
    std::scoped_lock lock{entries_mutex};

    LOG_DEBUG(Common_Memory, "Unfreezing memory for address={:016X}", address);

    std::erase_if(entries, [address](const Entry& entry) { return entry.address == address; });
}

bool Freezer::IsFrozen(VAddr address) const {
    std::scoped_lock lock{entries_mutex};

    return FindEntry(address) != entries.cend();
}

void Freezer::SetFrozenValue(VAddr address, u64 value) {
    std::scoped_lock lock{entries_mutex};

    const auto iter = FindEntry(address);

    if (iter == entries.cend()) {
        LOG_ERROR(Common_Memory,
                  "Tried to set freeze value for address={:016X} that is not frozen!", address);
        return;
    }

    LOG_DEBUG(Common_Memory,
              "Manually overridden freeze value for address={:016X}, width={:02X} to value={:016X}",
              iter->address, iter->width, value);
    iter->value = value;
}

std::optional<Freezer::Entry> Freezer::GetEntry(VAddr address) const {
    std::scoped_lock lock{entries_mutex};

    const auto iter = FindEntry(address);

    if (iter == entries.cend()) {
        return std::nullopt;
    }

    return *iter;
}

std::vector<Freezer::Entry> Freezer::GetEntries() const {
    std::scoped_lock lock{entries_mutex};

    return entries;
}

Freezer::Entries::iterator Freezer::FindEntry(VAddr address) {
    return std::find_if(entries.begin(), entries.end(),
                        [address](const Entry& entry) { return entry.address == address; });
}

Freezer::Entries::const_iterator Freezer::FindEntry(VAddr address) const {
    return std::find_if(entries.begin(), entries.end(),
                        [address](const Entry& entry) { return entry.address == address; });
}

void Freezer::FrameCallback(std::chrono::nanoseconds ns_late) {
    if (!IsActive()) {
        LOG_DEBUG(Common_Memory, "Memory freezer has been deactivated, ending callback events.");
        return;
    }

    std::scoped_lock lock{entries_mutex};

    for (const auto& entry : entries) {
        LOG_DEBUG(Common_Memory,
                  "Enforcing memory freeze at address={:016X}, value={:016X}, width={:02X}",
                  entry.address, entry.value, entry.width);
        MemoryWriteWidth(memory, entry.width, entry.address, entry.value);
    }

    core_timing.ScheduleEvent(memory_freezer_ns - ns_late, event);
}

void Freezer::FillEntryReads() {
    std::scoped_lock lock{entries_mutex};

    LOG_DEBUG(Common_Memory, "Updating memory freeze entries to current values.");

    for (auto& entry : entries) {
        entry.value = MemoryReadWidth(memory, entry.width, entry.address);
    }
}

} // namespace Tools


code:
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <atomic>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
#include <vector>
#include "common/common_types.h"

namespace Core::Timing {
class CoreTiming;
struct EventType;
} // namespace Core::Timing

namespace Core::Memory {
class Memory;
}

namespace Tools {

/**
 * This class allows the user to prevent an application from writing new values to certain memory
 * locations. This has a variety of uses when attempting to reverse a game.
 *
 * One example could be a cheat to prevent Mario from taking damage in SMO. One could freeze the
 * memory address that the game uses to store Mario's health so when he takes damage (and the game
 * tries to write the new health value to memory), the value won't change.
 */
class Freezer {
public:
    struct Entry {
        VAddr address;
        u32 width;
        u64 value;
    };

    explicit Freezer(Core::Timing::CoreTiming& core_timing_, Core::Memory::Memory& memory_);
    ~Freezer();

    // Enables or disables the entire memory freezer.
    void SetActive(bool is_active);

    // Returns whether or not the freezer is active.
    bool IsActive() const;

    // Removes all entries from the freezer.
    void Clear();

    // Freezes a value to its current memory address. The value the memory is kept at will be the
    // value that is read during this function. Width can be 1, 2, 4, or 8 (in bytes).
    u64 Freeze(VAddr address, u32 width);

    // Unfreezes the memory value at address. If the address isn't frozen, this is a no-op.
    void Unfreeze(VAddr address);

    // Returns whether or not the address is frozen.
    bool IsFrozen(VAddr address) const;

    // Sets the value that address should be frozen to. This doesn't change the width set by using
    // Freeze(). If the value isn't frozen, this will not freeze it and is thus a no-op.
    void SetFrozenValue(VAddr address, u64 value);

    // Returns the entry corresponding to the address if the address is frozen, otherwise
    // std::nullopt.
    std::optional<Entry> GetEntry(VAddr address) const;

    // Returns all the entries in the freezer, an empty vector means nothing is frozen.
    std::vector<Entry> GetEntries() const;

private:
    using Entries = std::vector<Entry>;

    Entries::iterator FindEntry(VAddr address);
    Entries::const_iterator FindEntry(VAddr address) const;

    void FrameCallback(std::chrono::nanoseconds ns_late);
    void FillEntryReads();

    std::atomic_bool active{false};

    mutable std::mutex entries_mutex;
    Entries entries;

    std::shared_ptr<Core::Timing::EventType> event;
    Core::Timing::CoreTiming& core_timing;
    Core::Memory::Memory& memory;
};

} // namespace Tools


code:
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <renderdoc_app.h>

#include "common/assert.h"
#include "common/dynamic_library.h"
#include "core/tools/renderdoc.h"

#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif

namespace Tools {

RenderdocAPI::RenderdocAPI() {
#ifdef WIN32
    if (HMODULE mod = GetModuleHandleA("renderdoc.dll")) {
        const auto RENDERDOC_GetAPI =
            reinterpret_cast<pRENDERDOC_GetAPI>(GetProcAddress(mod, "RENDERDOC_GetAPI"));
        const s32 ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_6_0, (void**)&rdoc_api);
        ASSERT(ret == 1);
    }
#else
#ifdef ANDROID
    static constexpr const char RENDERDOC_LIB[] = "libVkLayer_GLES_RenderDoc.so";
#else
    static constexpr const char RENDERDOC_LIB[] = "librenderdoc.so";
#endif
    if (void* mod = dlopen(RENDERDOC_LIB, RTLD_NOW | RTLD_NOLOAD)) {
        const auto RENDERDOC_GetAPI =
            reinterpret_cast<pRENDERDOC_GetAPI>(dlsym(mod, "RENDERDOC_GetAPI"));
        const s32 ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_6_0, (void**)&rdoc_api);
        ASSERT(ret == 1);
    }
#endif
}

RenderdocAPI::~RenderdocAPI() = default;

void RenderdocAPI::ToggleCapture() {
    if (!rdoc_api) [[unlikely]] {
        return;
    }
    if (!is_capturing) {
        rdoc_api->StartFrameCapture(NULL, NULL);
    } else {
        rdoc_api->EndFrameCapture(NULL, NULL);
    }
    is_capturing = !is_capturing;
}

} // namespace Tools


code:
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

struct RENDERDOC_API_1_6_0;

namespace Tools {

class RenderdocAPI {
public:
    explicit RenderdocAPI();
    ~RenderdocAPI();

    void ToggleCapture();

private:
    RENDERDOC_API_1_6_0* rdoc_api{};
    bool is_capturing{false};
};

} // namespace Tools


code:
# SPDX-FileCopyrightText: 2017 Citra Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later

add_executable(yuzu-room
    precompiled_headers.h
    yuzu_room.cpp
    yuzu_room.rc
)

target_link_libraries(yuzu-room PRIVATE common network)
if (ENABLE_WEB_SERVICE)
    target_compile_definitions(yuzu-room PRIVATE -DENABLE_WEB_SERVICE)
    target_link_libraries(yuzu-room PRIVATE web_service)
endif()

target_link_libraries(yuzu-room PRIVATE mbedtls mbedcrypto)
if (MSVC)
    target_link_libraries(yuzu-room PRIVATE getopt)
endif()
target_link_libraries(yuzu-room PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)

if(UNIX AND NOT APPLE)
    install(TARGETS yuzu-room)
endif()

if (YUZU_USE_PRECOMPILED_HEADERS)
    target_precompile_headers(yuzu-room PRIVATE precompiled_headers.h)
endif()

create_target_directory_groups(yuzu-room)


code:
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "common/common_precompiled_headers.h"


code:
// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <chrono>
#include <fstream>
#include <iostream>
#include <memory>
#include <regex>
#include <string>
#include <thread>

#ifdef _WIN32
// windows.h needs to be included before shellapi.h
#include <windows.h>

#include <shellapi.h>
#endif

#include <mbedtls/base64.h>
#include "common/common_types.h"
#include "common/detached_tasks.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "network/announce_multiplayer_session.h"
#include "network/network.h"
#include "network/room.h"
#include "network/verify_user.h"

#ifdef ENABLE_WEB_SERVICE
#include "web_service/verify_user_jwt.h"
#endif

#undef _UNICODE
#include <getopt.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif

static void PrintHelp(const char* argv0) {
    LOG_INFO(Network,
             "Usage: {}"
             " [options] <filename>\n"
             "--room-name         The name of the room\n"
             "--room-description  The room description\n"
             "--bind-address      The bind address for the room\n"
             "--port              The port used for the room\n"
             "--max_members       The maximum number of players for this room\n"
             "--password          The password for the room\n"
             "--preferred-game    The preferred game for this room\n"
             "--preferred-game-id The preferred game-id for this room\n"
             "--username          The username used for announce\n"
             "--token             The token used for announce\n"
             "--web-api-url       yuzu Web API url\n"
             "--ban-list-file     The file for storing the room ban list\n"
             "--log-file          The file for storing the room log\n"
             "--enable-yuzu-mods Allow yuzu Community Moderators to moderate on your room\n"
             "-h, --help          Display this help and exit\n"
             "-v, --version       Output version information and exit\n",
             argv0);
}

static void PrintVersion() {
    LOG_INFO(Network, "yuzu dedicated room {} {} Libnetwork: {}", Common::g_scm_branch,
             Common::g_scm_desc, Network::network_version);
}

/// The magic text at the beginning of a yuzu-room ban list file.
static constexpr char BanListMagic[] = "YuzuRoom-BanList-1";

static constexpr char token_delimiter{':'};

static void PadToken(std::string& token) {
    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('=');
    }
}

static std::string UsernameFromDisplayToken(const std::string& display_token) {
    std::size_t outlen;

    std::array<unsigned char, 512> output{};
    mbedtls_base64_decode(output.data(), output.size(), &outlen,
                          reinterpret_cast<const unsigned char*>(display_token.c_str()),
                          display_token.length());
    std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen);
    return decoded_display_token.substr(0, decoded_display_token.find(token_delimiter));
}

static std::string TokenFromDisplayToken(const std::string& display_token) {
    std::size_t outlen;

    std::array<unsigned char, 512> output{};
    mbedtls_base64_decode(output.data(), output.size(), &outlen,
                          reinterpret_cast<const unsigned char*>(display_token.c_str()),
                          display_token.length());
    std::string decoded_display_token(reinterpret_cast<char*>(&output), outlen);
    return decoded_display_token.substr(decoded_display_token.find(token_delimiter) + 1);
}

static Network::Room::BanList LoadBanList(const std::string& path) {
    std::ifstream file;
    Common::FS::OpenFileStream(file, path, std::ios_base::in);
    if (!file || file.eof()) {
        LOG_ERROR(Network, "Could not open ban list!");
        return {};
    }
    std::string magic;
    std::getline(file, magic);
    if (magic != BanListMagic) {
        LOG_ERROR(Network, "Ban list is not valid!");
        return {};
    }

    // false = username ban list, true = ip ban list
    bool ban_list_type = false;
    Network::Room::UsernameBanList username_ban_list;
    Network::Room::IPBanList ip_ban_list;
    while (!file.eof()) {
        std::string line;
        std::getline(file, line);
        line.erase(std::remove(line.begin(), line.end(), '\0'), line.end());
        line = Common::StripSpaces(line);
        if (line.empty()) {
            // An empty line marks start of the IP ban list
            ban_list_type = true;
            continue;
        }
        if (ban_list_type) {
            ip_ban_list.emplace_back(line);
        } else {
            username_ban_list.emplace_back(line);
        }
    }

    return {username_ban_list, ip_ban_list};
}

static void SaveBanList(const Network::Room::BanList& ban_list, const std::string& path) {
    std::ofstream file;
    Common::FS::OpenFileStream(file, path, std::ios_base::out);
    if (!file) {
        LOG_ERROR(Network, "Could not save ban list!");
        return;
    }

    file << BanListMagic << "\n";

    // Username ban list
    for (const auto& username : ban_list.first) {
        file << username << "\n";
    }
    file << "\n";

    // IP ban list
    for (const auto& ip : ban_list.second) {
        file << ip << "\n";
    }
}

static void InitializeLogging(const std::string& log_file) {
    Common::Log::Initialize();
    Common::Log::SetColorConsoleBackendEnabled(true);
    Common::Log::Start();
}

/// Application entry point
int main(int argc, char** argv) {
    Common::DetachedTasks detached_tasks;
    int option_index = 0;
    char* endarg;

    std::string room_name;
    std::string room_description;
    std::string password;
    std::string preferred_game;
    std::string username;
    std::string token;
    std::string web_api_url;
    std::string ban_list_file;
    std::string log_file = "yuzu-room.log";
    std::string bind_address;
    u64 preferred_game_id = 0;
    u32 port = Network::DefaultRoomPort;
    u32 max_members = 16;
    bool enable_yuzu_mods = false;

    static struct option long_options[] = {
        {"room-name", required_argument, 0, 'n'},
        {"room-description", required_argument, 0, 'd'},
        {"bind-address", required_argument, 0, 's'},
        {"port", required_argument, 0, 'p'},
        {"max_members", required_argument, 0, 'm'},
        {"password", required_argument, 0, 'w'},
        {"preferred-game", required_argument, 0, 'g'},
        {"preferred-game-id", required_argument, 0, 'i'},
        {"username", optional_argument, 0, 'u'},
        {"token", required_argument, 0, 't'},
        {"web-api-url", required_argument, 0, 'a'},
        {"ban-list-file", required_argument, 0, 'b'},
        {"log-file", required_argument, 0, 'l'},
        {"enable-yuzu-mods", no_argument, 0, 'e'},
        {"help", no_argument, 0, 'h'},
        {"version", no_argument, 0, 'v'},
        {0, 0, 0, 0},
    };

    InitializeLogging(log_file);

    while (optind < argc) {
        int arg =
            getopt_long(argc, argv, "n:d:s:p:m:w:g:u:t:a:i:l:hv", long_options, &option_index);
        if (arg != -1) {
            switch (static_cast<char>(arg)) {
            case 'n':
                room_name.assign(optarg);
                break;
            case 'd':
                room_description.assign(optarg);
                break;
            case 's':
                bind_address.assign(optarg);
                break;
            case 'p':
                port = strtoul(optarg, &endarg, 0);
                break;
            case 'm':
                max_members = strtoul(optarg, &endarg, 0);
                break;
            case 'w':
                password.assign(optarg);
                break;
            case 'g':
                preferred_game.assign(optarg);
                break;
            case 'i':
                preferred_game_id = strtoull(optarg, &endarg, 16);
                break;
            case 'u':
                username.assign(optarg);
                break;
            case 't':
                token.assign(optarg);
                break;
            case 'a':
                web_api_url.assign(optarg);
                break;
            case 'b':
                ban_list_file.assign(optarg);
                break;
            case 'l':
                log_file.assign(optarg);
                break;
            case 'e':
                enable_yuzu_mods = true;
                break;
            case 'h':
                PrintHelp(argv[0]);
                return 0;
            case 'v':
                PrintVersion();
                return 0;
            }
        }
    }

    if (room_name.empty()) {
        LOG_ERROR(Network, "Room name is empty!");
        PrintHelp(argv[0]);
        return -1;
    }
    if (preferred_game.empty()) {
        LOG_ERROR(Network, "Preferred game is empty!");
        PrintHelp(argv[0]);
        return -1;
    }
    if (preferred_game_id == 0) {
        LOG_ERROR(Network,
                  "preferred-game-id not set!\nThis should get set to allow users to find your "
                  "room.\nSet with --preferred-game-id id");
    }
    if (max_members > Network::MaxConcurrentConnections || max_members < 2) {
        LOG_ERROR(Network, "max_members needs to be in the range 2 - {}!",
                  Network::MaxConcurrentConnections);
        PrintHelp(argv[0]);
        return -1;
    }
    if (bind_address.empty()) {
        LOG_INFO(Network, "Bind address is empty: defaulting to 0.0.0.0");
    }
    if (port > UINT16_MAX) {
        LOG_ERROR(Network, "Port needs to be in the range 0 - 65535!");
        PrintHelp(argv[0]);
        return -1;
    }
    if (ban_list_file.empty()) {
        LOG_ERROR(Network, "Ban list file not set!\nThis should get set to load and save room ban "
                           "list.\nSet with --ban-list-file <file>");
    }
    bool announce = true;
    if (token.empty() && announce) {
        announce = false;
        LOG_INFO(Network, "Token is empty: Hosting a private room");
    }
    if (web_api_url.empty() && announce) {
        announce = false;
        LOG_INFO(Network, "Endpoint url is empty: Hosting a private room");
    }
    if (announce) {
        if (username.empty()) {
            LOG_INFO(Network, "Hosting a public room");
            Settings::values.web_api_url = web_api_url;
            PadToken(token);
            Settings::values.yuzu_username = UsernameFromDisplayToken(token);
            username = Settings::values.yuzu_username.GetValue();
            Settings::values.yuzu_token = TokenFromDisplayToken(token);
        } else {
            LOG_INFO(Network, "Hosting a public room");
            Settings::values.web_api_url = web_api_url;
            Settings::values.yuzu_username = username;
            Settings::values.yuzu_token = token;
        }
    }
    if (!announce && enable_yuzu_mods) {
        enable_yuzu_mods = false;
        LOG_INFO(Network, "Can not enable yuzu Moderators for private rooms");
    }

    // Load the ban list
    Network::Room::BanList ban_list;
    if (!ban_list_file.empty()) {
        ban_list = LoadBanList(ban_list_file);
    }

    std::unique_ptr<Network::VerifyUser::Backend> verify_backend;
    if (announce) {
#ifdef ENABLE_WEB_SERVICE
        verify_backend =
            std::make_unique<WebService::VerifyUserJWT>(Settings::values.web_api_url.GetValue());
#else
        LOG_INFO(Network,
                 "yuzu Web Services is not available with this build: validation is disabled.");
        verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
#endif
    } else {
        verify_backend = std::make_unique<Network::VerifyUser::NullBackend>();
    }

    Network::RoomNetwork network{};
    network.Init();
    if (auto room = network.GetRoom().lock()) {
        AnnounceMultiplayerRoom::GameInfo preferred_game_info{.name = preferred_game,
                                                              .id = preferred_game_id};
        if (!room->Create(room_name, room_description, bind_address, static_cast<u16>(port),
                          password, max_members, username, preferred_game_info,
                          std::move(verify_backend), ban_list, enable_yuzu_mods)) {
            LOG_INFO(Network, "Failed to create room: ");
            return -1;
        }
        LOG_INFO(Network, "Room is open. Close with Q+Enter...");
        auto announce_session = std::make_unique<Core::AnnounceMultiplayerSession>(network);
        if (announce) {
            announce_session->Start();
        }
        while (room->GetState() == Network::Room::State::Open) {
            std::string in;
            std::cin >> in;
            if (in.size() > 0) {
                break;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        if (announce) {
            announce_session->Stop();
        }
        announce_session.reset();
        // Save the ban list
        if (!ban_list_file.empty()) {
            SaveBanList(room->GetBanList(), ban_list_file);
        }
        room->Destroy();
    }
    network.Shutdown();
    detached_tasks.WaitForAllTasks();
    return 0;
}


code:
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "winresrc.h"
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
YUZU_ICON              ICON                    "../../dist/yuzu.ico"

/////////////////////////////////////////////////////////////////////////////
//
// RT_MANIFEST
//

0                       RT_MANIFEST             "../../dist/yuzu.manifest"


code:
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later

add_library(frontend_common STATIC
    config.cpp
    config.h
    content_manager.h
)

create_target_directory_groups(frontend_common)
target_link_libraries(frontend_common PUBLIC core SimpleIni::SimpleIni PRIVATE common Boost::headers)


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <algorithm>
#include <array>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "common/settings_common.h"
#include "common/settings_enums.h"
#include "config.h"
#include "core/core.h"
#include "core/hle/service/acc/profile_manager.h"
#include "hid_core/resources/npad/npad.h"
#include "network/network.h"

#include <boost/algorithm/string/replace.hpp>

#include "common/string_util.h"

namespace FS = Common::FS;

Config::Config(const ConfigType config_type)
    : type(config_type), global{config_type == ConfigType::GlobalConfig} {}

void Config::Initialize(const std::string& config_name) {
    const std::filesystem::path fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir);
    const auto config_file = fmt::format("{}.ini", config_name);

    switch (type) {
    case ConfigType::GlobalConfig:
        config_loc = FS::PathToUTF8String(fs_config_loc / config_file);
        void(FS::CreateParentDir(config_loc));
        SetUpIni();
        Reload();
        break;
    case ConfigType::PerGameConfig:
        config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / FS::ToU8String(config_file));
        void(FS::CreateParentDir(config_loc));
        SetUpIni();
        Reload();
        break;
    case ConfigType::InputProfile:
        config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file);
        void(FS::CreateParentDir(config_loc));
        SetUpIni();
        break;
    }
}

void Config::Initialize(const std::optional<std::string> config_path) {
    const std::filesystem::path default_sdl_config_path =
        FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "sdl2-config.ini";
    config_loc = config_path.value_or(FS::PathToUTF8String(default_sdl_config_path));
    void(FS::CreateParentDir(config_loc));
    SetUpIni();
    Reload();
}

void Config::WriteToIni() const {
    std::string config_type;
    switch (type) {
    case ConfigType::GlobalConfig:
        config_type = "Global";
        break;
    case ConfigType::PerGameConfig:
        config_type = "Game Specific";
        break;
    case ConfigType::InputProfile:
        config_type = "Input Profile";
        break;
    }
    LOG_INFO(Config, "Writing {} configuration to: {}", config_type, config_loc);
    FILE* fp = nullptr;
#ifdef _WIN32
    fp = _wfopen(Common::UTF8ToUTF16W(config_loc).data(), L"wb");
#else
    fp = fopen(config_loc.c_str(), "wb");
#endif

    if (fp == nullptr) {
        LOG_ERROR(Frontend, "Config file could not be saved!");
        return;
    }

    CSimpleIniA::FileWriter writer(fp);
    const SI_Error rc = config->Save(writer, false);
    if (rc < 0) {
        LOG_ERROR(Frontend, "Config file could not be saved!");
    }
    fclose(fp);
}

void Config::SetUpIni() {
    config = std::make_unique<CSimpleIniA>();
    config->SetUnicode(true);
    config->SetSpaces(false);

    FILE* fp = nullptr;
#ifdef _WIN32
    _wfopen_s(&fp, Common::UTF8ToUTF16W(config_loc).data(), L"rb, ccs=UTF-8");
    if (fp == nullptr) {
        fp = _wfopen(Common::UTF8ToUTF16W(config_loc).data(), L"wb, ccs=UTF-8");
    }
#else
    fp = fopen(config_loc.c_str(), "rb");
    if (fp == nullptr) {
        fp = fopen(config_loc.c_str(), "wb");
    }
#endif

    if (fp == nullptr) {
        LOG_ERROR(Frontend, "Config file could not be loaded!");
        return;
    }

    if (SI_Error rc = config->LoadFile(fp); rc < 0) {
        LOG_ERROR(Frontend, "Config file could not be loaded!");
    }
    fclose(fp);
}

bool Config::IsCustomConfig() const {
    return type == ConfigType::PerGameConfig;
}

void Config::ReadPlayerValues(const std::size_t player_index) {
    std::string player_prefix;
    if (type != ConfigType::InputProfile) {
        player_prefix.append("player_").append(ToString(player_index)).append("_");
    }

    const auto profile_name = ReadStringSetting(std::string(player_prefix).append("profile_name"));

    auto& player = Settings::values.players.GetValue()[player_index];
    if (IsCustomConfig()) {
        if (profile_name.empty()) {
            // Use the global input config
            player = Settings::values.players.GetValue(true)[player_index];
            player.profile_name = "";
            return;
        }
        player.profile_name = profile_name;
    }

    if (player_prefix.empty() && Settings::IsConfiguringGlobal()) {
        const auto controller = static_cast<Settings::ControllerType>(
            ReadIntegerSetting(std::string(player_prefix).append("type"),
                               static_cast<u8>(Settings::ControllerType::ProController)));

        if (controller == Settings::ControllerType::LeftJoycon ||
            controller == Settings::ControllerType::RightJoycon) {
            player.controller_type = controller;
        }
    } else {
        if (global) {
            auto& player_global = Settings::values.players.GetValue(true)[player_index];
            player_global.profile_name = profile_name;
        }
        std::string connected_key = player_prefix;
        player.connected = ReadBooleanSetting(connected_key.append("connected"),
                                              std::make_optional(player_index == 0));

        player.controller_type = static_cast<Settings::ControllerType>(
            ReadIntegerSetting(std::string(player_prefix).append("type"),
                               static_cast<u8>(Settings::ControllerType::ProController)));

        player.vibration_enabled = ReadBooleanSetting(
            std::string(player_prefix).append("vibration_enabled"), std::make_optional(true));

        player.vibration_strength = static_cast<int>(
            ReadIntegerSetting(std::string(player_prefix).append("vibration_strength"), 100));

        player.body_color_left = static_cast<u32>(ReadIntegerSetting(
            std::string(player_prefix).append("body_color_left"), Settings::JOYCON_BODY_NEON_BLUE));
        player.body_color_right = static_cast<u32>(ReadIntegerSetting(
            std::string(player_prefix).append("body_color_right"), Settings::JOYCON_BODY_NEON_RED));
        player.button_color_left = static_cast<u32>(
            ReadIntegerSetting(std::string(player_prefix).append("button_color_left"),
                               Settings::JOYCON_BUTTONS_NEON_BLUE));
        player.button_color_right = static_cast<u32>(
            ReadIntegerSetting(std::string(player_prefix).append("button_color_right"),
                               Settings::JOYCON_BUTTONS_NEON_RED));
    }
}

void Config::ReadTouchscreenValues() {
    Settings::values.touchscreen.enabled =
        ReadBooleanSetting(std::string("touchscreen_enabled"), std::make_optional(true));
    Settings::values.touchscreen.rotation_angle =
        static_cast<u32>(ReadIntegerSetting(std::string("touchscreen_angle"), 0));
    Settings::values.touchscreen.diameter_x =
        static_cast<u32>(ReadIntegerSetting(std::string("touchscreen_diameter_x"), 90));
    Settings::values.touchscreen.diameter_y =
        static_cast<u32>(ReadIntegerSetting(std::string("touchscreen_diameter_y"), 90));
}

void Config::ReadAudioValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Audio));

    ReadCategory(Settings::Category::Audio);
    ReadCategory(Settings::Category::UiAudio);

    EndGroup();
}

void Config::ReadControlValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));

    ReadCategory(Settings::Category::Controls);

    Settings::values.players.SetGlobal(!IsCustomConfig());
    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
        ReadPlayerValues(p);
    }

    // Disable docked mode if handheld is selected
    const auto controller_type = Settings::values.players.GetValue()[0].controller_type;
    if (controller_type == Settings::ControllerType::Handheld) {
        Settings::values.use_docked_mode.SetGlobal(!IsCustomConfig());
        Settings::values.use_docked_mode.SetValue(Settings::ConsoleMode::Handheld);
    }

    if (IsCustomConfig()) {
        EndGroup();
        return;
    }
    ReadTouchscreenValues();
    ReadMotionTouchValues();

    EndGroup();
}

void Config::ReadMotionTouchValues() {
    Settings::values.touch_from_button_maps.clear();
    int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps"));

    if (num_touch_from_button_maps > 0) {
        for (int i = 0; i < num_touch_from_button_maps; ++i) {
            SetArrayIndex(i);

            Settings::TouchFromButtonMap map;
            map.name = ReadStringSetting(std::string("name"), std::string("default"));

            const int num_touch_maps = BeginArray(std::string("entries"));
            map.buttons.reserve(num_touch_maps);
            for (int j = 0; j < num_touch_maps; j++) {
                SetArrayIndex(j);
                std::string touch_mapping = ReadStringSetting(std::string("bind"));
                map.buttons.emplace_back(std::move(touch_mapping));
            }
            EndArray(); // entries
            Settings::values.touch_from_button_maps.emplace_back(std::move(map));
        }
    } else {
        Settings::values.touch_from_button_maps.emplace_back(
            Settings::TouchFromButtonMap{"default", {}});
        num_touch_from_button_maps = 1;
    }
    EndArray(); // touch_from_button_maps

    Settings::values.touch_from_button_map_index = std::clamp(
        Settings::values.touch_from_button_map_index.GetValue(), 0, num_touch_from_button_maps - 1);
}

void Config::ReadCoreValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Core));

    ReadCategory(Settings::Category::Core);

    EndGroup();
}

void Config::ReadDataStorageValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));

    FS::SetYuzuPath(FS::YuzuPath::NANDDir, ReadStringSetting(std::string("nand_directory")));
    FS::SetYuzuPath(FS::YuzuPath::SDMCDir, ReadStringSetting(std::string("sdmc_directory")));
    FS::SetYuzuPath(FS::YuzuPath::LoadDir, ReadStringSetting(std::string("load_directory")));
    FS::SetYuzuPath(FS::YuzuPath::DumpDir, ReadStringSetting(std::string("dump_directory")));
    FS::SetYuzuPath(FS::YuzuPath::TASDir, ReadStringSetting(std::string("tas_directory")));

    ReadCategory(Settings::Category::DataStorage);

    EndGroup();
}

void Config::ReadDebuggingValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Debugging));

    // Intentionally not using the QT default setting as this is intended to be changed in the ini
    Settings::values.record_frame_times =
        ReadBooleanSetting(std::string("record_frame_times"), std::make_optional(false));

    ReadCategory(Settings::Category::Debugging);
    ReadCategory(Settings::Category::DebuggingGraphics);

    EndGroup();
}

#ifdef __unix__
void Config::ReadLinuxValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Linux));

    ReadCategory(Settings::Category::Linux);

    EndGroup();
}
#endif

void Config::ReadServiceValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Services));

    ReadCategory(Settings::Category::Services);

    EndGroup();
}

void Config::ReadDisabledAddOnValues() {
    // Custom config section
    BeginGroup(std::string("DisabledAddOns"));

    const int size = BeginArray(std::string(""));
    for (int i = 0; i < size; ++i) {
        SetArrayIndex(i);
        const auto title_id = ReadUnsignedIntegerSetting(std::string("title_id"), 0);
        std::vector<std::string> out;
        const int d_size = BeginArray("disabled");
        for (int j = 0; j < d_size; ++j) {
            SetArrayIndex(j);
            out.push_back(ReadStringSetting(std::string("d"), std::string("")));
        }
        EndArray(); // d
        Settings::values.disabled_addons.insert_or_assign(title_id, out);
    }
    EndArray(); // Base disabled addons array - Has no base key

    EndGroup();
}

void Config::ReadMiscellaneousValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Miscellaneous));

    ReadCategory(Settings::Category::Miscellaneous);

    EndGroup();
}

void Config::ReadCpuValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Cpu));

    ReadCategory(Settings::Category::Cpu);
    ReadCategory(Settings::Category::CpuDebug);
    ReadCategory(Settings::Category::CpuUnsafe);

    EndGroup();
}

void Config::ReadRendererValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Renderer));

    ReadCategory(Settings::Category::Renderer);
    ReadCategory(Settings::Category::RendererAdvanced);
    ReadCategory(Settings::Category::RendererDebug);

    EndGroup();
}

void Config::ReadScreenshotValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Screenshots));

    ReadCategory(Settings::Category::Screenshots);
    FS::SetYuzuPath(FS::YuzuPath::ScreenshotsDir,
                    ReadStringSetting(std::string("screenshot_path")));

    EndGroup();
}

void Config::ReadSystemValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::System));

    ReadCategory(Settings::Category::System);
    ReadCategory(Settings::Category::SystemAudio);

    EndGroup();
}

void Config::ReadWebServiceValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::WebService));

    ReadCategory(Settings::Category::WebService);

    EndGroup();
}

void Config::ReadNetworkValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Services));

    ReadCategory(Settings::Category::Network);

    EndGroup();
}

void Config::ReadLibraryAppletValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::LibraryApplet));

    ReadCategory(Settings::Category::LibraryApplet);

    EndGroup();
}

void Config::ReadValues() {
    if (global) {
        ReadDataStorageValues();
        ReadDebuggingValues();
        ReadDisabledAddOnValues();
        ReadNetworkValues();
        ReadServiceValues();
        ReadWebServiceValues();
        ReadMiscellaneousValues();
        ReadLibraryAppletValues();
    }
    ReadControlValues();
    ReadCoreValues();
    ReadCpuValues();
#ifdef __unix__
    ReadLinuxValues();
#endif
    ReadRendererValues();
    ReadAudioValues();
    ReadSystemValues();
}

void Config::SavePlayerValues(const std::size_t player_index) {
    std::string player_prefix;
    if (type != ConfigType::InputProfile) {
        player_prefix = std::string("player_").append(ToString(player_index)).append("_");
    }

    const auto& player = Settings::values.players.GetValue()[player_index];
    if (IsCustomConfig()) {
        if (player.profile_name.empty()) {
            // No custom profile selected
            return;
        }
        WriteStringSetting(std::string(player_prefix).append("profile_name"), player.profile_name,
                           std::make_optional(std::string("")));
    }

    WriteIntegerSetting(
        std::string(player_prefix).append("type"), static_cast<u8>(player.controller_type),
        std::make_optional(static_cast<u8>(Settings::ControllerType::ProController)));

    if (!player_prefix.empty() || !Settings::IsConfiguringGlobal()) {
        if (global) {
            const auto& player_global = Settings::values.players.GetValue(true)[player_index];
            WriteStringSetting(std::string(player_prefix).append("profile_name"),
                               player_global.profile_name, std::make_optional(std::string("")));
        }
        WriteBooleanSetting(std::string(player_prefix).append("connected"), player.connected,
                            std::make_optional(player_index == 0));
        WriteIntegerSetting(std::string(player_prefix).append("vibration_enabled"),
                            player.vibration_enabled, std::make_optional(true));
        WriteIntegerSetting(std::string(player_prefix).append("vibration_strength"),
                            player.vibration_strength, std::make_optional(100));
        WriteIntegerSetting(std::string(player_prefix).append("body_color_left"),
                            player.body_color_left,
                            std::make_optional(Settings::JOYCON_BODY_NEON_BLUE));
        WriteIntegerSetting(std::string(player_prefix).append("body_color_right"),
                            player.body_color_right,
                            std::make_optional(Settings::JOYCON_BODY_NEON_RED));
        WriteIntegerSetting(std::string(player_prefix).append("button_color_left"),
                            player.button_color_left,
                            std::make_optional(Settings::JOYCON_BUTTONS_NEON_BLUE));
        WriteIntegerSetting(std::string(player_prefix).append("button_color_right"),
                            player.button_color_right,
                            std::make_optional(Settings::JOYCON_BUTTONS_NEON_RED));
    }
}

void Config::SaveTouchscreenValues() {
    const auto& touchscreen = Settings::values.touchscreen;

    WriteBooleanSetting(std::string("touchscreen_enabled"), touchscreen.enabled,
                        std::make_optional(true));

    WriteIntegerSetting(std::string("touchscreen_angle"), touchscreen.rotation_angle,
                        std::make_optional(static_cast<u32>(0)));
    WriteIntegerSetting(std::string("touchscreen_diameter_x"), touchscreen.diameter_x,
                        std::make_optional(static_cast<u32>(90)));
    WriteIntegerSetting(std::string("touchscreen_diameter_y"), touchscreen.diameter_y,
                        std::make_optional(static_cast<u32>(90)));
}

void Config::SaveMotionTouchValues() {
    BeginArray(std::string("touch_from_button_maps"));
    for (std::size_t p = 0; p < Settings::values.touch_from_button_maps.size(); ++p) {
        SetArrayIndex(static_cast<int>(p));
        WriteStringSetting(std::string("name"), Settings::values.touch_from_button_maps[p].name,
                           std::make_optional(std::string("default")));

        BeginArray(std::string("entries"));
        for (std::size_t q = 0; q < Settings::values.touch_from_button_maps[p].buttons.size();
             ++q) {
            SetArrayIndex(static_cast<int>(q));
            WriteStringSetting(std::string("bind"),
                               Settings::values.touch_from_button_maps[p].buttons[q]);
        }
        EndArray(); // entries
    }
    EndArray(); // touch_from_button_maps
}

void Config::SaveValues() {
    if (global) {
        LOG_DEBUG(Config, "Saving global generic configuration values");
        SaveDataStorageValues();
        SaveDebuggingValues();
        SaveDisabledAddOnValues();
        SaveNetworkValues();
        SaveWebServiceValues();
        SaveMiscellaneousValues();
        SaveLibraryAppletValues();
    } else {
        LOG_DEBUG(Config, "Saving only generic configuration values");
    }
    SaveControlValues();
    SaveCoreValues();
    SaveCpuValues();
#ifdef __unix__
    SaveLinuxValues();
#endif
    SaveRendererValues();
    SaveAudioValues();
    SaveSystemValues();

    WriteToIni();
}

void Config::SaveAudioValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Audio));

    WriteCategory(Settings::Category::Audio);
    WriteCategory(Settings::Category::UiAudio);

    EndGroup();
}

void Config::SaveControlValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));

    WriteCategory(Settings::Category::Controls);

    Settings::values.players.SetGlobal(!IsCustomConfig());
    for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
        SavePlayerValues(p);
    }
    if (IsCustomConfig()) {
        EndGroup();
        return;
    }
    SaveTouchscreenValues();
    SaveMotionTouchValues();

    EndGroup();
}

void Config::SaveCoreValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Core));

    WriteCategory(Settings::Category::Core);

    EndGroup();
}

void Config::SaveDataStorageValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));

    WriteStringSetting(std::string("nand_directory"), FS::GetYuzuPathString(FS::YuzuPath::NANDDir),
                       std::make_optional(FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
    WriteStringSetting(std::string("sdmc_directory"), FS::GetYuzuPathString(FS::YuzuPath::SDMCDir),
                       std::make_optional(FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
    WriteStringSetting(std::string("load_directory"), FS::GetYuzuPathString(FS::YuzuPath::LoadDir),
                       std::make_optional(FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
    WriteStringSetting(std::string("dump_directory"), FS::GetYuzuPathString(FS::YuzuPath::DumpDir),
                       std::make_optional(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
    WriteStringSetting(std::string("tas_directory"), FS::GetYuzuPathString(FS::YuzuPath::TASDir),
                       std::make_optional(FS::GetYuzuPathString(FS::YuzuPath::TASDir)));

    WriteCategory(Settings::Category::DataStorage);

    EndGroup();
}

void Config::SaveDebuggingValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Debugging));

    // Intentionally not using the QT default setting as this is intended to be changed in the ini
    WriteBooleanSetting(std::string("record_frame_times"), Settings::values.record_frame_times);

    WriteCategory(Settings::Category::Debugging);
    WriteCategory(Settings::Category::DebuggingGraphics);

    EndGroup();
}

#ifdef __unix__
void Config::SaveLinuxValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Linux));

    WriteCategory(Settings::Category::Linux);

    EndGroup();
}
#endif

void Config::SaveNetworkValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Services));

    WriteCategory(Settings::Category::Network);

    EndGroup();
}

void Config::SaveDisabledAddOnValues() {
    // Custom config section
    BeginGroup(std::string("DisabledAddOns"));

    int i = 0;
    BeginArray(std::string(""));
    for (const auto& elem : Settings::values.disabled_addons) {
        SetArrayIndex(i);
        WriteIntegerSetting(std::string("title_id"), elem.first,
                            std::make_optional(static_cast<u64>(0)));
        BeginArray(std::string("disabled"));
        for (std::size_t j = 0; j < elem.second.size(); ++j) {
            SetArrayIndex(static_cast<int>(j));
            WriteStringSetting(std::string("d"), elem.second[j],
                               std::make_optional(std::string("")));
        }
        EndArray(); // disabled
        ++i;
    }
    EndArray(); // Base disabled addons array - Has no base key

    EndGroup();
}

void Config::SaveMiscellaneousValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Miscellaneous));

    WriteCategory(Settings::Category::Miscellaneous);

    EndGroup();
}

void Config::SaveCpuValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Cpu));

    WriteCategory(Settings::Category::Cpu);
    WriteCategory(Settings::Category::CpuDebug);
    WriteCategory(Settings::Category::CpuUnsafe);

    EndGroup();
}

void Config::SaveRendererValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Renderer));

    WriteCategory(Settings::Category::Renderer);
    WriteCategory(Settings::Category::RendererAdvanced);
    WriteCategory(Settings::Category::RendererDebug);

    EndGroup();
}

void Config::SaveScreenshotValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::Screenshots));

    WriteStringSetting(std::string("screenshot_path"),
                       FS::GetYuzuPathString(FS::YuzuPath::ScreenshotsDir));
    WriteCategory(Settings::Category::Screenshots);

    EndGroup();
}

void Config::SaveSystemValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::System));

    WriteCategory(Settings::Category::System);
    WriteCategory(Settings::Category::SystemAudio);

    EndGroup();
}

void Config::SaveWebServiceValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::WebService));

    WriteCategory(Settings::Category::WebService);

    EndGroup();
}

void Config::SaveLibraryAppletValues() {
    BeginGroup(Settings::TranslateCategory(Settings::Category::LibraryApplet));

    WriteCategory(Settings::Category::LibraryApplet);

    EndGroup();
}

bool Config::ReadBooleanSetting(const std::string& key, const std::optional<bool> default_value) {
    std::string full_key = GetFullKey(key, false);
    if (!default_value.has_value()) {
        return config->GetBoolValue(GetSection().c_str(), full_key.c_str(), false);
    }

    if (config->GetBoolValue(GetSection().c_str(),
                             std::string(full_key).append("\\default").c_str(), false)) {
        return static_cast<bool>(default_value.value());
    } else {
        return config->GetBoolValue(GetSection().c_str(), full_key.c_str(),
                                    static_cast<bool>(default_value.value()));
    }
}

s64 Config::ReadIntegerSetting(const std::string& key, const std::optional<s64> default_value) {
    std::string full_key = GetFullKey(key, false);
    if (!default_value.has_value()) {
        try {
            return std::stoll(
                std::string(config->GetValue(GetSection().c_str(), full_key.c_str(), "0")));
        } catch (...) {
            return 0;
        }
    }

    s64 result = 0;
    if (config->GetBoolValue(GetSection().c_str(),
                             std::string(full_key).append("\\default").c_str(), true)) {
        result = default_value.value();
    } else {
        try {
            result = std::stoll(std::string(config->GetValue(
                GetSection().c_str(), full_key.c_str(), ToString(default_value.value()).c_str())));
        } catch (...) {
            result = default_value.value();
        }
    }
    return result;
}

u64 Config::ReadUnsignedIntegerSetting(const std::string& key,
                                       const std::optional<u64> default_value) {
    std::string full_key = GetFullKey(key, false);
    if (!default_value.has_value()) {
        try {
            return std::stoull(
                std::string(config->GetValue(GetSection().c_str(), full_key.c_str(), "0")));
        } catch (...) {
            return 0;
        }
    }

    u64 result = 0;
    if (config->GetBoolValue(GetSection().c_str(),
                             std::string(full_key).append("\\default").c_str(), true)) {
        result = default_value.value();
    } else {
        try {
            result = std::stoull(std::string(config->GetValue(
                GetSection().c_str(), full_key.c_str(), ToString(default_value.value()).c_str())));
        } catch (...) {
            result = default_value.value();
        }
    }
    return result;
}

double Config::ReadDoubleSetting(const std::string& key,
                                 const std::optional<double> default_value) {
    std::string full_key = GetFullKey(key, false);
    if (!default_value.has_value()) {
        return config->GetDoubleValue(GetSection().c_str(), full_key.c_str(), 0);
    }

    double result;
    if (config->GetBoolValue(GetSection().c_str(),
                             std::string(full_key).append("\\default").c_str(), true)) {
        result = default_value.value();
    } else {
        result =
            config->GetDoubleValue(GetSection().c_str(), full_key.c_str(), default_value.value());
    }
    return result;
}

std::string Config::ReadStringSetting(const std::string& key,
                                      const std::optional<std::string> default_value) {
    std::string result;
    std::string full_key = GetFullKey(key, false);
    if (!default_value.has_value()) {
        result = config->GetValue(GetSection().c_str(), full_key.c_str(), "");
        boost::replace_all(result, "\"", "");
        return result;
    }

    if (config->GetBoolValue(GetSection().c_str(),
                             std::string(full_key).append("\\default").c_str(), true)) {
        result = default_value.value();
    } else {
        result =
            config->GetValue(GetSection().c_str(), full_key.c_str(), default_value.value().c_str());
    }
    boost::replace_all(result, "\"", "");
    boost::replace_all(result, "//", "/");
    return result;
}

bool Config::Exists(const std::string& section, const std::string& key) const {
    const std::string value = config->GetValue(section.c_str(), key.c_str(), "");
    return !value.empty();
}

void Config::WriteBooleanSetting(const std::string& key, const bool& value,
                                 const std::optional<bool>& default_value,
                                 const std::optional<bool>& use_global) {
    std::optional<std::string> string_default = std::nullopt;
    if (default_value.has_value()) {
        string_default = std::make_optional(ToString(default_value.value()));
    }
    WritePreparedSetting(key, AdjustOutputString(ToString(value)), string_default, use_global);
}

void Config::WriteDoubleSetting(const std::string& key, const double& value,
                                const std::optional<double>& default_value,
                                const std::optional<bool>& use_global) {
    std::optional<std::string> string_default = std::nullopt;
    if (default_value.has_value()) {
        string_default = std::make_optional(ToString(default_value.value()));
    }
    WritePreparedSetting(key, AdjustOutputString(ToString(value)), string_default, use_global);
}

void Config::WriteStringSetting(const std::string& key, const std::string& value,
                                const std::optional<std::string>& default_value,
                                const std::optional<bool>& use_global) {
    std::optional string_default = default_value;
    if (default_value.has_value()) {
        string_default.value().append(AdjustOutputString(default_value.value()));
    }
    WritePreparedSetting(key, AdjustOutputString(value), string_default, use_global);
}

void Config::WritePreparedSetting(const std::string& key, const std::string& adjusted_value,
                                  const std::optional<std::string>& adjusted_default_value,
                                  const std::optional<bool>& use_global) {
    std::string full_key = GetFullKey(key, false);
    if (adjusted_default_value.has_value() && use_global.has_value()) {
        if (!global) {
            WriteString(std::string(full_key).append("\\global"), ToString(use_global.value()));
        }
        if (global || use_global.value() == false) {
            WriteString(std::string(full_key).append("\\default"),
                        ToString(adjusted_default_value == adjusted_value));
            WriteString(full_key, adjusted_value);
        }
    } else if (adjusted_default_value.has_value() && !use_global.has_value()) {
        WriteString(std::string(full_key).append("\\default"),
                    ToString(adjusted_default_value == adjusted_value));
        WriteString(full_key, adjusted_value);
    } else {
        WriteString(full_key, adjusted_value);
    }
}

void Config::WriteString(const std::string& key, const std::string& value) {
    config->SetValue(GetSection().c_str(), key.c_str(), value.c_str());
}

void Config::Reload() {
    ReadValues();
    // To apply default value changes
    SaveValues();
}

void Config::ClearControlPlayerValues() const {
    // Removes the entire [Controls] section
    const char* section = Settings::TranslateCategory(Settings::Category::Controls);
    config->Delete(section, nullptr, true);
}

const std::string& Config::GetConfigFilePath() const {
    return config_loc;
}

void Config::ReadCategory(const Settings::Category category) {
    const auto& settings = FindRelevantList(category);
    std::ranges::for_each(settings, [&](const auto& setting) { ReadSettingGeneric(setting); });
}

void Config::WriteCategory(const Settings::Category category) {
    const auto& settings = FindRelevantList(category);
    std::ranges::for_each(settings, [&](const auto& setting) { WriteSettingGeneric(setting); });
}

void Config::ReadSettingGeneric(Settings::BasicSetting* const setting) {
    if (!setting->Save() || (!setting->Switchable() && !global)) {
        return;
    }

    const std::string key = AdjustKey(setting->GetLabel());
    const std::string default_value(setting->DefaultToString());

    bool use_global = true;
    if (setting->Switchable() && !global) {
        use_global =
            ReadBooleanSetting(std::string(key).append("\\use_global"), std::make_optional(true));
        setting->SetGlobal(use_global);
    }

    if (global || !use_global) {
        const bool is_default =
            ReadBooleanSetting(std::string(key).append("\\default"), std::make_optional(true));
        if (!is_default) {
            const std::string setting_string = ReadStringSetting(key, default_value);
            setting->LoadString(setting_string);
        } else {
            // Empty string resets the Setting to default
            setting->LoadString("");
        }
    }
}

void Config::WriteSettingGeneric(const Settings::BasicSetting* const setting) {
    if (!setting->Save()) {
        return;
    }

    std::string key = AdjustKey(setting->GetLabel());
    if (setting->Switchable()) {
        if (!global) {
            WriteBooleanSetting(std::string(key).append("\\use_global"), setting->UsingGlobal());
        }
        if (global || !setting->UsingGlobal()) {
            auto value = global ? setting->ToStringGlobal() : setting->ToString();
            WriteBooleanSetting(std::string(key).append("\\default"),
                                value == setting->DefaultToString());
            WriteStringSetting(key, value);
        }
    } else if (global) {
        WriteBooleanSetting(std::string(key).append("\\default"),
                            setting->ToString() == setting->DefaultToString());
        WriteStringSetting(key, setting->ToString());
    }
}

void Config::BeginGroup(const std::string& group) {
    // You can't begin a group while reading/writing from a config array
    ASSERT(array_stack.empty());

    key_stack.push_back(AdjustKey(group));
}

void Config::EndGroup() {
    // You can't end a group if you haven't started one yet
    ASSERT(!key_stack.empty());

    // You can't end a group when reading/writing from a config array
    ASSERT(array_stack.empty());

    key_stack.pop_back();
}

std::string Config::GetSection() {
    if (key_stack.empty()) {
        return std::string{""};
    }

    return key_stack.front();
}

std::string Config::GetGroup() const {
    if (key_stack.size() <= 1) {
        return std::string{""};
    }

    std::string key;
    for (size_t i = 1; i < key_stack.size(); ++i) {
        key.append(key_stack[i]).append("\\");
    }
    return key;
}

std::string Config::AdjustKey(const std::string& key) {
    std::string adjusted_key(key);
    boost::replace_all(adjusted_key, "/", "\\");
    boost::replace_all(adjusted_key, " ", "%20");
    return adjusted_key;
}

std::string Config::AdjustOutputString(const std::string& string) {
    std::string adjusted_string(string);
    boost::replace_all(adjusted_string, "\\", "/");

    // Windows requires that two forward slashes are used at the start of a path for unmapped
    // network drives so we have to watch for that here
#ifndef ANDROID
    if (string.substr(0, 2) == "//") {
        boost::replace_all(adjusted_string, "//", "/");
        adjusted_string.insert(0, "/");
    } else {
        boost::replace_all(adjusted_string, "//", "/");
    }
#endif

    // Needed for backwards compatibility with QSettings deserialization
    for (const auto& special_character : special_characters) {
        if (adjusted_string.find(special_character) != std::string::npos) {
            adjusted_string.insert(0, "\"");
            adjusted_string.append("\"");
            break;
        }
    }
    return adjusted_string;
}

std::string Config::GetFullKey(const std::string& key, bool skipArrayIndex) {
    if (array_stack.empty()) {
        return std::string(GetGroup()).append(AdjustKey(key));
    }

    std::string array_key;
    for (size_t i = 0; i < array_stack.size(); ++i) {
        if (!array_stack[i].name.empty()) {
            array_key.append(array_stack[i].name).append("\\");
        }

        if (!skipArrayIndex || (array_stack.size() - 1 != i && array_stack.size() > 1)) {
            array_key.append(ToString(array_stack[i].index)).append("\\");
        }
    }
    std::string final_key = std::string(GetGroup()).append(array_key).append(AdjustKey(key));
    return final_key;
}

int Config::BeginArray(const std::string& array) {
    array_stack.push_back(ConfigArray{AdjustKey(array), 0, 0});
    const int size = config->GetLongValue(GetSection().c_str(),
                                          GetFullKey(std::string("size"), true).c_str(), 0);
    array_stack.back().size = size;
    return size;
}

void Config::EndArray() {
    // You can't end a config array before starting one
    ASSERT(!array_stack.empty());

    // Set the array size to 0 if the array is ended without changing the index
    int size = 0;
    if (array_stack.back().index != 0) {
        size = array_stack.back().size;
    }

    // Write out the size to config
    if (key_stack.size() == 1 && array_stack.back().name.empty()) {
        // Edge-case where the first array created doesn't have a name
        config->SetValue(GetSection().c_str(), std::string("size").c_str(), ToString(size).c_str());
    } else {
        const auto key = GetFullKey(std::string("size"), true);
        config->SetValue(GetSection().c_str(), key.c_str(), ToString(size).c_str());
    }

    array_stack.pop_back();
}

void Config::SetArrayIndex(const int index) {
    // You can't set the array index if you haven't started one yet
    ASSERT(!array_stack.empty());

    const int array_index = index + 1;

    // You can't exceed the known max size of the array by more than 1
    ASSERT(array_stack.front().size + 1 >= array_index);

    // Change the config array size to the current index since you may want
    // to reduce the number of elements that you read back from the config
    // in the future.
    array_stack.back().size = array_index;
    array_stack.back().index = array_index;
}


code:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <memory>
#include <string>
#include "common/settings.h"

#define SI_NO_CONVERSION
#include <SimpleIni.h>
#include <boost/algorithm/string/replace.hpp>

// Workaround for conflicting definition in libloaderapi.h caused by SimpleIni
#undef LoadString
#undef CreateFile
#undef DeleteFile
#undef CopyFile
#undef CreateDirectory
#undef MoveFile

namespace Core {
class System;
}

class Config {
public:
    enum class ConfigType {
        GlobalConfig,
        PerGameConfig,
        InputProfile,
    };

    virtual ~Config() = default;

    void ClearControlPlayerValues() const;

    [[nodiscard]] const std::string& GetConfigFilePath() const;

    [[nodiscard]] bool Exists(const std::string& section, const std::string& key) const;

protected:
    explicit Config(ConfigType config_type = ConfigType::GlobalConfig);

    void Initialize(const std::string& config_name = "config");
    void Initialize(std::optional<std::string> config_path);

    void WriteToIni() const;

    void SetUpIni();
    [[nodiscard]] bool IsCustomConfig() const;

    void Reload();

    /**
     * Derived config classes must implement this so they can reload all platform-specific
     * values and global ones.
     */
    virtual void ReloadAllValues() = 0;

    /**
     * Derived config classes must implement this so they can save all platform-specific
     * and global values.
     */
    virtual void SaveAllValues() = 0;

    void ReadValues();
    void ReadPlayerValues(std::size_t player_index);

    void ReadTouchscreenValues();
    void ReadMotionTouchValues();

    // Read functions bases off the respective config section names.
    void ReadAudioValues();
    void ReadControlValues();
    void ReadCoreValues();
    void ReadDataStorageValues();
    void ReadDebuggingValues();
#ifdef __unix__
    void ReadLinuxValues();
#endif
    void ReadServiceValues();
    void ReadDisabledAddOnValues();
    void ReadMiscellaneousValues();
    void ReadCpuValues();
    void ReadRendererValues();
    void ReadScreenshotValues();
    void ReadSystemValues();
    void ReadWebServiceValues();
    void ReadNetworkValues();
    void ReadLibraryAppletValues();

    // Read platform specific sections
    virtual void ReadHidbusValues() = 0;
    virtual void ReadDebugControlValues() = 0;
    virtual void ReadPathValues() = 0;
    virtual void ReadShortcutValues() = 0;
    virtual void ReadUIValues() = 0;
    virtual void ReadUIGamelistValues() = 0;
    virtual void ReadUILayoutValues() = 0;
    virtual void ReadMultiplayerValues() = 0;

    void SaveValues();
    void SavePlayerValues(std::size_t player_index);
    void SaveTouchscreenValues();
    void SaveMotionTouchValues();

    // Save functions based off the respective config section names.
    void SaveAudioValues();
    void SaveControlValues();
    void SaveCoreValues();
    void SaveDataStorageValues();
    void SaveDebuggingValues();
#ifdef __unix__
    void SaveLinuxValues();
#endif
    void SaveNetworkValues();
    void SaveDisabledAddOnValues();
    void SaveMiscellaneousValues();
    void SaveCpuValues();
    void SaveRendererValues();
    void SaveScreenshotValues();
    void SaveSystemValues();
    void SaveWebServiceValues();
    void SaveLibraryAppletValues();

    // Save platform specific sections
    virtual void SaveHidbusValues() = 0;
    virtual void SaveDebugControlValues() = 0;
    virtual void SavePathValues() = 0;
    virtual void SaveShortcutValues() = 0;
    virtual void SaveUIValues() = 0;
    virtual void SaveUIGamelistValues() = 0;
    virtual void SaveUILayoutValues() = 0;
    virtual void SaveMultiplayerValues() = 0;

    virtual std::vector<Settings::BasicSetting*>& FindRelevantList(Settings::Category category) = 0;

    /**
     * Reads a setting from the qt_config.
     *
     * @param key The setting's identifier
     * @param default_value The value to use when the setting is not already present in the config
     */
    bool ReadBooleanSetting(const std::string& key,
                            std::optional<bool> default_value = std::nullopt);
    s64 ReadIntegerSetting(const std::string& key, std::optional<s64> default_value = std::nullopt);
    u64 ReadUnsignedIntegerSetting(const std::string& key,
                                   std::optional<u64> default_value = std::nullopt);
    double ReadDoubleSetting(const std::string& key,
                             std::optional<double> default_value = std::nullopt);
    std::string ReadStringSetting(const std::string& key,
                                  std::optional<std::string> default_value = std::nullopt);

    /**
     * Writes a setting to the qt_config.
     *
     * @param key The setting's idetentifier
     * @param value Value of the setting
     * @param default_value Default of the setting if not present in config
     * @param use_global Specifies if the custom or global config should be in use, for custom
     * configs
     */
    void WriteBooleanSetting(const std::string& key, const bool& value,
                             const std::optional<bool>& default_value = std::nullopt,
                             const std::optional<bool>& use_global = std::nullopt);
    void WriteDoubleSetting(const std::string& key, const double& value,
                            const std::optional<double>& default_value = std::nullopt,
                            const std::optional<bool>& use_global = std::nullopt);
    void WriteStringSetting(const std::string& key, const std::string& value,
                            const std::optional<std::string>& default_value = std::nullopt,
                            const std::optional<bool>& use_global = std::nullopt);
    template <typename T>
    std::enable_if_t<std::is_integral_v<T>> WriteIntegerSetting(
        const std::string& key, const T& value,
        const std::optional<T>& default_value = std::nullopt,
        const std::optional<bool>& use_global = std::nullopt) {
        std::optional<std::string> string_default = std::nullopt;
        if (default_value.has_value()) {
            string_default = std::make_optional(ToString(default_value.value()));
        }
        WritePreparedSetting(key, AdjustOutputString(ToString(value)), string_default, use_global);
    }

    void ReadCategory(Settings::Category category);
    void WriteCategory(Settings::Category category);
    void ReadSettingGeneric(Settings::BasicSetting* setting);
    void WriteSettingGeneric(const Settings::BasicSetting* setting);

    template <typename T>
    [[nodiscard]] std::string ToString(const T& value_) {
        if constexpr (std::is_same_v<T, std::string>) {
            return value_;
        } else if constexpr (std::is_same_v<T, std::optional<u32>>) {
            return value_.has_value() ? std::to_string(*value_) : "none";
        } else if constexpr (std::is_same_v<T, bool>) {
            return value_ ? "true" : "false";
        } else if constexpr (std::is_same_v<T, u64>) {
            return std::to_string(static_cast<u64>(value_));
        } else if constexpr (std::is_same_v<T, s64>) {
            return std::to_string(static_cast<s64>(value_));
        } else {
            return std::to_string(value_);
        }
    }

    void BeginGroup(const std::string& group);
    void EndGroup();
    std::string GetSection();
    [[nodiscard]] std::string GetGroup() const;
    static std::string AdjustKey(const std::string& key);
    static std::string AdjustOutputString(const std::string& string);
    std::string GetFullKey(const std::string& key, bool skipArrayIndex);
    int BeginArray(const std::string& array);
    void EndArray();
    void SetArrayIndex(int index);

    const ConfigType type;
    std::unique_ptr<CSimpleIniA> config;
    std::string config_loc;
    const bool global;

private:
    void WritePreparedSetting(const std::string& key, const std::string& adjusted_value,
                              const std::optional<std::string>& adjusted_default_value,
                              const std::optional<bool>& use_global);
    void WriteString(const std::string& key, const std::string& value);

    inline static std::array<char, 18> special_characters = {
        '!', '#', '$', '%', '^', '&', '*', '|', ';', '\'', '\"', ',', '<', '>', '?', '`', '~', '='};

    struct ConfigArray {
        std::string name;
        int size;
        int index;
    };
    std::vector<ConfigArray> array_stack;
    std::vector<std::string> key_stack;
};