mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-04-27 13:25:23 -06:00
Merge branch 'main' into ime-fixes-again
This commit is contained in:
commit
3f057644fc
77
.github/workflows/build.yml
vendored
77
.github/workflows/build.yml
vendored
@ -14,6 +14,8 @@ on:
|
||||
- "documents/**"
|
||||
- "**/*.md"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||||
@ -65,6 +67,81 @@ jobs:
|
||||
echo "shorthash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "fullhash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
test:
|
||||
name: Run C++ Tests on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
include:
|
||||
- os: windows-latest
|
||||
compiler_cxx: clang-cl
|
||||
compiler_c: clang-cl
|
||||
- os: ubuntu-latest
|
||||
compiler_cxx: clang++
|
||||
compiler_c: clang
|
||||
- os: macos-latest
|
||||
compiler_cxx: clang++
|
||||
compiler_c: clang
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup CMake
|
||||
uses: lukka/get-cmake@latest
|
||||
|
||||
- name: Setup Visual Studio shell (Windows only)
|
||||
if: runner.os == 'Windows'
|
||||
uses: egor-tensin/vs-shell@v2
|
||||
with:
|
||||
arch: x64
|
||||
|
||||
- name: Install dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxrandr-dev libxfixes-dev libudev-dev uuid-dev uuid-dev
|
||||
|
||||
- name: Install dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install ninja
|
||||
|
||||
- name: Configure CMake
|
||||
run: |
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_CXX_COMPILER="${{ matrix.compiler_cxx }}" \
|
||||
-DCMAKE_C_COMPILER="${{ matrix.compiler_c }}" \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DENABLE_TESTS=ON \
|
||||
${{ runner.os == 'macOS' && '-DCMAKE_OSX_ARCHITECTURES=x86_64' || '' }}
|
||||
shell: bash
|
||||
|
||||
- name: Create shadPS4 user data directory (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: mkdir -p ~/.local/share/shadPS4
|
||||
|
||||
- name: Create shadPS4 user data directory (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: mkdir -p ~/Library/Application\ Support/shadPS4
|
||||
|
||||
- name: Create shadPS4 user data directory (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: mkdir -p "$APPDATA/shadPS4"
|
||||
shell: bash
|
||||
|
||||
- name: Build all tests
|
||||
run: cmake --build build
|
||||
shell: bash
|
||||
|
||||
- name: Run tests with CTest
|
||||
run: ctest --test-dir build --output-on-failure --progress
|
||||
shell: bash
|
||||
|
||||
windows-sdl:
|
||||
runs-on: windows-2025
|
||||
needs: get-info
|
||||
|
||||
@ -33,6 +33,7 @@ endif()
|
||||
|
||||
option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON)
|
||||
option(ENABLE_UPDATER "Enables the options to updater" ON)
|
||||
option(ENABLE_TESTS "Build unit tests (requires GTest)" OFF)
|
||||
|
||||
# First, determine whether to use CMAKE_OSX_ARCHITECTURES or CMAKE_SYSTEM_PROCESSOR.
|
||||
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
@ -1116,6 +1117,8 @@ set(EMULATOR src/emulator.cpp
|
||||
src/sdl_window.cpp
|
||||
)
|
||||
|
||||
if(NOT ENABLE_TESTS)
|
||||
|
||||
add_executable(shadps4
|
||||
${AUDIO_CORE}
|
||||
${IMGUI}
|
||||
@ -1269,3 +1272,8 @@ endif()
|
||||
|
||||
# Install rules
|
||||
install(TARGETS shadps4 BUNDLE DESTINATION .)
|
||||
|
||||
else()
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@ -68,6 +68,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||
CLS(Common) \
|
||||
SUB(Common, Filesystem) \
|
||||
SUB(Common, Memory) \
|
||||
CLS(KeyManager) \
|
||||
CLS(Core) \
|
||||
SUB(Core, Linker) \
|
||||
SUB(Core, Devices) \
|
||||
@ -80,7 +81,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||
SUB(Kernel, Event) \
|
||||
SUB(Kernel, Sce) \
|
||||
CLS(Lib) \
|
||||
SUB(Lib, LibC) \
|
||||
SUB(Lib, LibcInternal) \
|
||||
SUB(Lib, Kernel) \
|
||||
SUB(Lib, Pad) \
|
||||
@ -117,7 +117,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||
SUB(Lib, NpSnsFacebookDialog) \
|
||||
SUB(Lib, NpPartner) \
|
||||
SUB(Lib, Screenshot) \
|
||||
SUB(Lib, LibCInternal) \
|
||||
SUB(Lib, AppContent) \
|
||||
SUB(Lib, Rtc) \
|
||||
SUB(Lib, Rudp) \
|
||||
@ -163,8 +162,6 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||
CLS(ImGui) \
|
||||
CLS(Input) \
|
||||
CLS(Tty) \
|
||||
CLS(KeyManager) \
|
||||
CLS(EmuSettings) \
|
||||
CLS(Loader)
|
||||
|
||||
// GetClassName is a macro defined by Windows.h, grrr...
|
||||
|
||||
@ -34,6 +34,7 @@ enum class Class : u8 {
|
||||
Common, ///< Library routines
|
||||
Common_Filesystem, ///< Filesystem interface library
|
||||
Common_Memory, ///< Memory mapping and management functions
|
||||
KeyManager, ///< Key management system
|
||||
Core, ///< LLE emulation core
|
||||
Core_Linker, ///< The module linker
|
||||
Core_Devices, ///< Devices emulation
|
||||
@ -44,10 +45,9 @@ enum class Class : u8 {
|
||||
Kernel_Fs, ///< The filesystem implementation of the kernel.
|
||||
Kernel_Vmm, ///< The virtual memory implementation of the kernel.
|
||||
Kernel_Event, ///< The event management implementation of the kernel.
|
||||
Kernel_Sce, ///< The sony specific interfaces provided by the kernel.
|
||||
Kernel_Sce, ///< The Sony-specific interfaces provided by the kernel.
|
||||
Lib, ///< HLE implementation of system library. Each major library
|
||||
///< should have its own subclass.
|
||||
Lib_LibC, ///< The LibC implementation.
|
||||
Lib_LibcInternal, ///< The LibcInternal implementation.
|
||||
Lib_Kernel, ///< The LibKernel implementation.
|
||||
Lib_Pad, ///< The LibScePad implementation.
|
||||
@ -83,7 +83,6 @@ enum class Class : u8 {
|
||||
Lib_NpProfileDialog, ///< The LibSceNpProfileDialog implementation
|
||||
Lib_NpSnsFacebookDialog, ///< The LibSceNpSnsFacebookDialog implementation
|
||||
Lib_Screenshot, ///< The LibSceScreenshot implementation
|
||||
Lib_LibCInternal, ///< The LibCInternal implementation.
|
||||
Lib_AppContent, ///< The LibSceAppContent implementation.
|
||||
Lib_Rtc, ///< The LibSceRtc implementation.
|
||||
Lib_Rudp, ///< The LibSceRudp implementation.
|
||||
@ -131,8 +130,6 @@ enum class Class : u8 {
|
||||
Loader, ///< ROM loader
|
||||
Input, ///< Input emulation
|
||||
Tty, ///< Debug output from emu
|
||||
KeyManager, ///< Key management system
|
||||
EmuSettings, /// Emulator settings system
|
||||
Count ///< Total number of logging classes
|
||||
};
|
||||
|
||||
|
||||
@ -25,10 +25,13 @@ namespace nlohmann {
|
||||
template <>
|
||||
struct adl_serializer<std::filesystem::path> {
|
||||
static void to_json(json& j, const std::filesystem::path& p) {
|
||||
j = p.string();
|
||||
const auto u8 = p.u8string();
|
||||
j = std::string(reinterpret_cast<const char*>(u8.data()), u8.size());
|
||||
}
|
||||
static void from_json(const json& j, std::filesystem::path& p) {
|
||||
p = j.get<std::string>();
|
||||
const std::string s = j.get<std::string>();
|
||||
p = std::filesystem::path(
|
||||
std::u8string_view(reinterpret_cast<const char8_t*>(s.data()), s.size()));
|
||||
}
|
||||
};
|
||||
} // namespace nlohmann
|
||||
@ -81,19 +84,20 @@ std::optional<T> get_optional(const toml::value& v, const std::string& key) {
|
||||
|
||||
void EmulatorSettingsImpl::PrintChangedSummary(const std::vector<std::string>& changed) {
|
||||
if (changed.empty()) {
|
||||
LOG_DEBUG(EmuSettings, "No game-specific overrides applied");
|
||||
LOG_DEBUG(Config, "No game-specific overrides applied");
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG(EmuSettings, "Game-specific overrides applied:");
|
||||
LOG_DEBUG(Config, "Game-specific overrides applied:");
|
||||
for (const auto& k : changed)
|
||||
LOG_DEBUG(EmuSettings, " * {}", k);
|
||||
LOG_DEBUG(Config, " * {}", k);
|
||||
}
|
||||
|
||||
// ── Singleton ────────────────────────────────────────────────────────
|
||||
EmulatorSettingsImpl::EmulatorSettingsImpl() = default;
|
||||
|
||||
EmulatorSettingsImpl::~EmulatorSettingsImpl() {
|
||||
Save();
|
||||
if (m_loaded)
|
||||
Save();
|
||||
}
|
||||
|
||||
std::shared_ptr<EmulatorSettingsImpl> EmulatorSettingsImpl::GetInstance() {
|
||||
@ -211,7 +215,7 @@ void EmulatorSettingsImpl::ClearGameSpecificOverrides() {
|
||||
ClearGroupOverrides(m_audio);
|
||||
ClearGroupOverrides(m_gpu);
|
||||
ClearGroupOverrides(m_vulkan);
|
||||
LOG_DEBUG(EmuSettings, "All game-specific overrides cleared");
|
||||
LOG_DEBUG(Config, "All game-specific overrides cleared");
|
||||
}
|
||||
|
||||
void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) {
|
||||
@ -237,7 +241,7 @@ void EmulatorSettingsImpl::ResetGameSpecificValue(const std::string& key) {
|
||||
return;
|
||||
if (tryGroup(m_vulkan))
|
||||
return;
|
||||
LOG_WARNING(EmuSettings, "ResetGameSpecificValue: key '{}' not found", key);
|
||||
LOG_WARNING(Config, "ResetGameSpecificValue: key '{}' not found", key);
|
||||
}
|
||||
|
||||
bool EmulatorSettingsImpl::Save(const std::string& serial) {
|
||||
@ -275,7 +279,7 @@ bool EmulatorSettingsImpl::Save(const std::string& serial) {
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out) {
|
||||
LOG_ERROR(EmuSettings, "Failed to open game config for writing: {}", path.string());
|
||||
LOG_ERROR(Config, "Failed to open game config for writing: {}", path.string());
|
||||
return false;
|
||||
}
|
||||
out << std::setw(2) << j;
|
||||
@ -316,14 +320,14 @@ bool EmulatorSettingsImpl::Save(const std::string& serial) {
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out) {
|
||||
LOG_ERROR(EmuSettings, "Failed to open config for writing: {}", path.string());
|
||||
LOG_ERROR(Config, "Failed to open config for writing: {}", path.string());
|
||||
return false;
|
||||
}
|
||||
out << std::setw(2) << existing;
|
||||
return !out.fail();
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(EmuSettings, "Error saving settings: {}", e.what());
|
||||
LOG_ERROR(Config, "Error saving settings: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -336,7 +340,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
// ── Global config ──────────────────────────────────────────
|
||||
const auto userDir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
|
||||
const auto configPath = userDir / "config.json";
|
||||
LOG_DEBUG(EmuSettings, "Loading global config from: {}", configPath.string());
|
||||
LOG_DEBUG(Config, "Loading global config from: {}", configPath.string());
|
||||
|
||||
if (std::ifstream in{configPath}; in.good()) {
|
||||
json gj;
|
||||
@ -357,13 +361,13 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
mergeGroup(m_gpu, "GPU");
|
||||
mergeGroup(m_vulkan, "Vulkan");
|
||||
|
||||
LOG_DEBUG(EmuSettings, "Global config loaded successfully");
|
||||
LOG_DEBUG(Config, "Global config loaded successfully");
|
||||
} else {
|
||||
if (std::filesystem::exists(Common::FS::GetUserPath(Common::FS::PathType::UserDir) /
|
||||
"config.toml")) {
|
||||
SDL_MessageBoxButtonData btns[2]{
|
||||
{0, 0, "No"},
|
||||
{0, 1, "Yes"},
|
||||
{0, 0, "Defaults"},
|
||||
{0, 1, "Update"},
|
||||
};
|
||||
SDL_MessageBoxData msg_box{
|
||||
0,
|
||||
@ -380,6 +384,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
SDL_ShowMessageBox(&msg_box, &result);
|
||||
if (result == 1) {
|
||||
if (TransferSettings()) {
|
||||
m_loaded = true;
|
||||
Save();
|
||||
return true;
|
||||
} else {
|
||||
@ -390,13 +395,14 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_DEBUG(EmuSettings, "Global config not found - using defaults");
|
||||
LOG_DEBUG(Config, "Global config not found - using defaults");
|
||||
SetDefaultValues();
|
||||
Save();
|
||||
}
|
||||
if (GetConfigVersion() != Common::g_scm_rev) {
|
||||
Save();
|
||||
}
|
||||
m_loaded = true;
|
||||
return true;
|
||||
} else {
|
||||
// ── Per-game override file ─────────────────────────────────
|
||||
@ -405,16 +411,16 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
// base configuration.
|
||||
const auto gamePath =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::CustomConfigs) / (serial + ".json");
|
||||
LOG_DEBUG(EmuSettings, "Applying game config: {}", gamePath.string());
|
||||
LOG_DEBUG(Config, "Applying game config: {}", gamePath.string());
|
||||
|
||||
if (!std::filesystem::exists(gamePath)) {
|
||||
LOG_DEBUG(EmuSettings, "No game-specific config found for {}", serial);
|
||||
LOG_DEBUG(Config, "No game-specific config found for {}", serial);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream in(gamePath);
|
||||
if (!in) {
|
||||
LOG_ERROR(EmuSettings, "Failed to open game config: {}", gamePath.string());
|
||||
LOG_ERROR(Config, "Failed to open game config: {}", gamePath.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -445,7 +451,7 @@ bool EmulatorSettingsImpl::Load(const std::string& serial) {
|
||||
return true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(EmuSettings, "Error loading settings: {}", e.what());
|
||||
LOG_ERROR(Config, "Error loading settings: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -608,7 +614,7 @@ bool EmulatorSettingsImpl::TransferSettings() {
|
||||
}
|
||||
s.install_dirs.value = settings_install_dirs;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARNING(EmuSettings, "Failed to transfer install directories: {}", e.what());
|
||||
LOG_WARNING(Config, "Failed to transfer install directories: {}", e.what());
|
||||
}
|
||||
|
||||
// Transfer addon install directory
|
||||
@ -624,7 +630,7 @@ bool EmulatorSettingsImpl::TransferSettings() {
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARNING(EmuSettings, "Failed to transfer addon install directory: {}", e.what());
|
||||
LOG_WARNING(Config, "Failed to transfer addon install directory: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@ -644,4 +650,4 @@ std::vector<std::string> EmulatorSettingsImpl::GetAllOverrideableKeys() const {
|
||||
addGroup(m_gpu.GetOverrideableFields());
|
||||
addGroup(m_vulkan.GetOverrideableFields());
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,26 +112,26 @@ inline OverrideItem make_override(const char* key, Setting<T> Struct::* member)
|
||||
return OverrideItem{
|
||||
key,
|
||||
[member, key](void* base, const nlohmann::json& entry, std::vector<std::string>& changed) {
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Processing key: {}", key);
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Entry JSON: {}", entry.dump());
|
||||
LOG_DEBUG(Config, "[make_override] Processing key: {}", key);
|
||||
LOG_DEBUG(Config, "[make_override] Entry JSON: {}", entry.dump());
|
||||
Struct* obj = reinterpret_cast<Struct*>(base);
|
||||
Setting<T>& dst = obj->*member;
|
||||
try {
|
||||
T newValue = entry.get<T>();
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Parsed value: {}", newValue);
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Current value: {}", dst.value);
|
||||
LOG_DEBUG(Config, "[make_override] Parsed value: {}", newValue);
|
||||
LOG_DEBUG(Config, "[make_override] Current value: {}", dst.value);
|
||||
if (dst.value != newValue) {
|
||||
std::ostringstream oss;
|
||||
oss << key << " ( " << dst.value << " → " << newValue << " )";
|
||||
changed.push_back(oss.str());
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Recorded change: {}", oss.str());
|
||||
LOG_DEBUG(Config, "[make_override] Recorded change: {}", oss.str());
|
||||
}
|
||||
dst.game_specific_value = newValue;
|
||||
LOG_DEBUG(EmuSettings, "[make_override] Successfully updated {}", key);
|
||||
LOG_DEBUG(Config, "[make_override] Successfully updated {}", key);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(EmuSettings, "[make_override] ERROR parsing {}: {}", key, e.what());
|
||||
LOG_ERROR(EmuSettings, "[make_override] Entry was: {}", entry.dump());
|
||||
LOG_ERROR(EmuSettings, "[make_override] Type name: {}", entry.type_name());
|
||||
LOG_ERROR(Config, "[make_override] ERROR parsing {}: {}", key, e.what());
|
||||
LOG_ERROR(Config, "[make_override] Entry was: {}", entry.dump());
|
||||
LOG_ERROR(Config, "[make_override] Type name: {}", entry.type_name());
|
||||
}
|
||||
},
|
||||
|
||||
@ -465,6 +465,8 @@ private:
|
||||
VulkanSettings m_vulkan{};
|
||||
ConfigMode m_configMode{ConfigMode::Default};
|
||||
|
||||
bool m_loaded{false};
|
||||
|
||||
static std::shared_ptr<EmulatorSettingsImpl> s_instance;
|
||||
static std::mutex s_mutex;
|
||||
|
||||
@ -592,16 +594,17 @@ public:
|
||||
SETTING_FORWARD_BOOL_READONLY(m_gpu, PatchShaders, patch_shaders)
|
||||
|
||||
u32 GetVblankFrequency() {
|
||||
if (m_gpu.vblank_frequency.value < 60) {
|
||||
m_gpu.vblank_frequency.value = 60;
|
||||
if (m_gpu.vblank_frequency.value < 30) {
|
||||
return 30;
|
||||
}
|
||||
return m_gpu.vblank_frequency.value;
|
||||
return m_gpu.vblank_frequency.get();
|
||||
}
|
||||
void SetVblankFrequency(const u32& v) {
|
||||
if (v < 60) {
|
||||
m_gpu.vblank_frequency.value = 60;
|
||||
void SetVblankFrequency(const u32& v, bool is_specific = false) {
|
||||
u32 val = v < 30 ? 30 : v;
|
||||
if (is_specific) {
|
||||
m_gpu.vblank_frequency.game_specific_value = val;
|
||||
} else {
|
||||
m_gpu.vblank_frequency.value = v;
|
||||
m_gpu.vblank_frequency.value = val;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -87,11 +87,13 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
if (!read && !write && !rdwr) {
|
||||
// Start by checking for invalid flags.
|
||||
*__Error() = POSIX_EINVAL;
|
||||
LOG_ERROR(Kernel_Fs, "Opening path {} failed, invalid flags {:#x}", raw_path, flags);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strlen(raw_path) > 255) {
|
||||
*__Error() = POSIX_ENAMETOOLONG;
|
||||
LOG_ERROR(Kernel_Fs, "Opening path {} failed, path is too long", raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -137,6 +139,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Error if file exists
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EEXIST;
|
||||
LOG_ERROR(Kernel_Fs, "Creating {} failed, file already exists", raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -145,6 +148,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Can't create files in a read only directory
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EROFS;
|
||||
LOG_ERROR(Kernel_Fs, "Creating {} failed, path is read-only", raw_path);
|
||||
return -1;
|
||||
}
|
||||
// Create a file if it doesn't exist
|
||||
@ -154,6 +158,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// If we're not creating a file, and it doesn't exist, return ENOENT
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_ENOENT;
|
||||
LOG_ERROR(Kernel_Fs, "Opening path {} failed, file does not exist", raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -169,6 +174,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// This will trigger when create & directory is specified, this is expected.
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_ENOTDIR;
|
||||
LOG_ERROR(Kernel_Fs, "Opening directory {} failed, file is not a directory", raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -176,6 +182,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Cannot open directories with any type of write access
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EISDIR;
|
||||
LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot open directories for writing",
|
||||
raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -183,6 +191,8 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Cannot open directories with truncate
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EISDIR;
|
||||
LOG_ERROR(Kernel_Fs, "Opening directory {} failed, cannot truncate directories",
|
||||
raw_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -201,6 +211,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Can't open files with truncate flag in a read only directory
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EROFS;
|
||||
LOG_ERROR(Kernel_Fs, "Truncating {} failed, path is read-only", raw_path);
|
||||
return -1;
|
||||
} else if (truncate) {
|
||||
// Open the file as read-write so we can truncate regardless of flags.
|
||||
@ -219,6 +230,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Can't open files with write/read-write access in a read only directory
|
||||
h->DeleteHandle(handle);
|
||||
*__Error() = POSIX_EROFS;
|
||||
LOG_ERROR(Kernel_Fs, "Opening {} for writing failed, path is read-only", raw_path);
|
||||
return -1;
|
||||
} else if (write) {
|
||||
if (append) {
|
||||
@ -244,6 +256,7 @@ s32 PS4_SYSV_ABI open(const char* raw_path, s32 flags, u16 mode) {
|
||||
// Open failed in platform-specific code, errno needs to be converted.
|
||||
h->DeleteHandle(handle);
|
||||
SetPosixErrno(e);
|
||||
LOG_ERROR(Kernel_Fs, "Opening {} failed, error = {}", raw_path, *__Error());
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -258,7 +271,6 @@ s32 PS4_SYSV_ABI posix_open(const char* filename, s32 flags, u16 mode) {
|
||||
s32 PS4_SYSV_ABI sceKernelOpen(const char* path, s32 flags, /* SceKernelMode*/ u16 mode) {
|
||||
s32 result = open(path, flags, mode);
|
||||
if (result < 0) {
|
||||
LOG_ERROR(Kernel_Fs, "error = {}", *__Error());
|
||||
return ErrnoToSceKernelError(*__Error());
|
||||
}
|
||||
return result;
|
||||
|
||||
@ -199,7 +199,11 @@ OrbisFILE* PS4_SYSV_ABI internal_fopen(const char* path, const char* mode) {
|
||||
std::scoped_lock lk{g_file_mtx};
|
||||
LOG_INFO(Lib_LibcInternal, "called, path {}, mode {}", path, mode);
|
||||
OrbisFILE* file = internal__Fofind();
|
||||
return internal__Foprep(path, mode, file, -1, 0, 0);
|
||||
OrbisFILE* ret_file = internal__Foprep(path, mode, file, -1, 0, 0);
|
||||
if (ret_file == nullptr) {
|
||||
LOG_ERROR(Lib_LibcInternal, "failed to open file {}", path);
|
||||
}
|
||||
return ret_file;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI internal_fflush(OrbisFILE* file) {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <mutex>
|
||||
#include <variant>
|
||||
|
||||
#include <core/emulator_settings.h>
|
||||
#include "common/config.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/libraries/error_codes.h"
|
||||
@ -784,7 +785,7 @@ void DeregisterNpCallback(std::string key) {
|
||||
}
|
||||
|
||||
void RegisterLib(Core::Loader::SymbolsResolver* sym) {
|
||||
g_signed_in = Config::getPSNSignedIn();
|
||||
g_signed_in = EmulatorSettings.IsPSNSignedIn();
|
||||
|
||||
LIB_FUNCTION("GpLQDNKICac", "libSceNpManager", 1, "libSceNpManager", sceNpCreateRequest);
|
||||
LIB_FUNCTION("eiqMCt9UshI", "libSceNpManager", 1, "libSceNpManager", sceNpCreateAsyncRequest);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
#include "common/config.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/libraries/error_codes.h"
|
||||
#include "core/libraries/libs.h"
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "common/config.h"
|
||||
#include "common/path_util.h"
|
||||
#include "common/singleton.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/file_sys/fs.h"
|
||||
#include "save_backup.h"
|
||||
#include "save_instance.h"
|
||||
@ -71,7 +72,7 @@ fs::path SaveInstance::GetParamSFOPath(const fs::path& dir_path) {
|
||||
|
||||
void SaveInstance::SetupDefaultParamSFO(PSF& param_sfo, std::string dir_name,
|
||||
std::string game_serial) {
|
||||
int locale = Config::GetLanguage();
|
||||
int locale = EmulatorSettings.GetConsoleLanguage();
|
||||
if (!default_title.contains(locale)) {
|
||||
locale = 1; // default to en_US if not found
|
||||
}
|
||||
|
||||
@ -223,8 +223,7 @@ s32 loadModuleInternal(s32 index, s32 argc, const void* argv, s32* res_out) {
|
||||
{"libSceAudiodec.sprx", nullptr},
|
||||
{"libSceFont.sprx", &Libraries::Font::RegisterlibSceFont},
|
||||
{"libSceFontFt.sprx", &Libraries::FontFt::RegisterlibSceFontFt},
|
||||
{"libSceFreeTypeOt.sprx", nullptr},
|
||||
{"libScePadTracker.sprx", nullptr}});
|
||||
{"libSceFreeTypeOt.sprx", nullptr}});
|
||||
|
||||
// Iterate through the allowed array
|
||||
const auto it = std::ranges::find_if(
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdlib>
|
||||
#include "common/config.h"
|
||||
#include "common/singleton.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/file_sys/fs.h"
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include "common/config.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
#include <queue>
|
||||
#include "core/libraries/libs.h"
|
||||
#include "core/libraries/system/userservice.h"
|
||||
#include "core/libraries/system/userservice_error.h"
|
||||
@ -105,6 +106,13 @@ int PS4_SYSV_ABI sceUserServiceGetDiscPlayerFlag() {
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
std::queue<OrbisUserServiceEvent> user_service_event_queue = {};
|
||||
|
||||
void AddUserServiceEvent(const OrbisUserServiceEvent e) {
|
||||
LOG_DEBUG(Lib_UserService, "Event added to queue: {} {}", (u8)e.event, e.userId);
|
||||
user_service_event_queue.push(e);
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceUserServiceGetEvent(OrbisUserServiceEvent* event) {
|
||||
LOG_TRACE(Lib_UserService, "(DUMMY) called");
|
||||
// fake a loggin event
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// reference :
|
||||
// https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/master/include/orbis/_types/user.h
|
||||
@ -57,6 +57,8 @@ struct OrbisUserServiceEvent {
|
||||
OrbisUserServiceUserId userId;
|
||||
};
|
||||
|
||||
void AddUserServiceEvent(const OrbisUserServiceEvent e);
|
||||
|
||||
int PS4_SYSV_ABI sceUserServiceInitializeForShellCore();
|
||||
int PS4_SYSV_ABI sceUserServiceTerminateForShellCore();
|
||||
int PS4_SYSV_ABI sceUserServiceDestroyUser();
|
||||
|
||||
@ -54,17 +54,16 @@ struct EntryParams {
|
||||
};
|
||||
|
||||
struct HeapAPI {
|
||||
PS4_SYSV_ABI void* (*heap_malloc)(size_t);
|
||||
PS4_SYSV_ABI void* (*heap_malloc)(u64);
|
||||
PS4_SYSV_ABI void (*heap_free)(void*);
|
||||
PS4_SYSV_ABI void* (*heap_calloc)(size_t, size_t);
|
||||
PS4_SYSV_ABI void* (*heap_realloc)(void*, size_t);
|
||||
PS4_SYSV_ABI void* (*heap_memalign)(size_t, size_t);
|
||||
PS4_SYSV_ABI int (*heap_posix_memalign)(void**, size_t, size_t);
|
||||
// NOTE: Fields below may be inaccurate
|
||||
PS4_SYSV_ABI int (*heap_reallocalign)(void);
|
||||
PS4_SYSV_ABI void (*heap_malloc_stats)(void);
|
||||
PS4_SYSV_ABI int (*heap_malloc_stats_fast)(void);
|
||||
PS4_SYSV_ABI size_t (*heap_malloc_usable_size)(void*);
|
||||
PS4_SYSV_ABI void* (*heap_calloc)(u64, u64);
|
||||
PS4_SYSV_ABI void* (*heap_realloc)(void*, u64);
|
||||
PS4_SYSV_ABI void* (*heap_memalign)(u64, u64);
|
||||
PS4_SYSV_ABI s32 (*heap_posix_memalign)(void**, u64, u64);
|
||||
PS4_SYSV_ABI s32 (*heap_reallocalign)(void*, u64, u64);
|
||||
PS4_SYSV_ABI s32 (*heap_malloc_stats)(void*);
|
||||
PS4_SYSV_ABI s32 (*heap_malloc_stats_fast)(void*);
|
||||
PS4_SYSV_ABI u64 (*heap_malloc_usable_size)(void*);
|
||||
};
|
||||
|
||||
using AppHeapAPI = HeapAPI*;
|
||||
|
||||
@ -172,6 +172,27 @@ LoggedInUsers UserManager::GetLoggedInUsers() const {
|
||||
return logged_in_users;
|
||||
}
|
||||
|
||||
using namespace Libraries::UserService;
|
||||
|
||||
void UserManager::LoginUser(User* u, s32 player_index) {
|
||||
if (!u) {
|
||||
return;
|
||||
}
|
||||
u->logged_in = true;
|
||||
// u->player_index = player_index;
|
||||
AddUserServiceEvent({OrbisUserServiceEventType::Login, u->user_id});
|
||||
logged_in_users[player_index - 1] = u;
|
||||
}
|
||||
|
||||
void UserManager::LogoutUser(User* u) {
|
||||
if (!u) {
|
||||
return;
|
||||
}
|
||||
u->logged_in = false;
|
||||
AddUserServiceEvent({OrbisUserServiceEventType::Logout, u->user_id});
|
||||
logged_in_users[u->player_index - 1] = {};
|
||||
}
|
||||
|
||||
bool UserManager::Save() const {
|
||||
return UserSettings.Save();
|
||||
}
|
||||
@ -42,6 +42,8 @@ public:
|
||||
void SetControllerPort(u32 user_id, int port);
|
||||
std::vector<User> GetValidUsers() const;
|
||||
LoggedInUsers GetLoggedInUsers() const;
|
||||
void LoginUser(User* u, s32 player_index);
|
||||
void LogoutUser(User* u);
|
||||
|
||||
Users& GetUsers() {
|
||||
return m_users;
|
||||
|
||||
@ -44,13 +44,13 @@ bool UserSettingsImpl::Save() const {
|
||||
|
||||
std::ofstream out(path);
|
||||
if (!out) {
|
||||
LOG_ERROR(EmuSettings, "Failed to open user settings for writing: {}", path.string());
|
||||
LOG_ERROR(Config, "Failed to open user settings for writing: {}", path.string());
|
||||
return false;
|
||||
}
|
||||
out << std::setw(2) << j;
|
||||
return !out.fail();
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(EmuSettings, "Error saving user settings: {}", e.what());
|
||||
LOG_ERROR(Config, "Error saving user settings: {}", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -59,7 +59,7 @@ bool UserSettingsImpl::Load() {
|
||||
const auto path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "users.json";
|
||||
try {
|
||||
if (!std::filesystem::exists(path)) {
|
||||
LOG_DEBUG(EmuSettings, "User settings file not found: {}", path.string());
|
||||
LOG_DEBUG(Config, "User settings file not found: {}", path.string());
|
||||
// Create default user if no file exists
|
||||
if (m_userManager.GetUsers().user.empty()) {
|
||||
m_userManager.GetUsers() = m_userManager.CreateDefaultUsers();
|
||||
@ -70,7 +70,7 @@ bool UserSettingsImpl::Load() {
|
||||
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
LOG_ERROR(EmuSettings, "Failed to open user settings: {}", path.string());
|
||||
LOG_ERROR(Config, "Failed to open user settings: {}", path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -97,10 +97,10 @@ bool UserSettingsImpl::Load() {
|
||||
Save();
|
||||
}
|
||||
|
||||
LOG_DEBUG(EmuSettings, "User settings loaded successfully");
|
||||
LOG_DEBUG(Config, "User settings loaded successfully");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(EmuSettings, "Error loading user settings: {}", e.what());
|
||||
LOG_ERROR(Config, "Error loading user settings: {}", e.what());
|
||||
// Fall back to defaults
|
||||
if (m_userManager.GetUsers().user.empty()) {
|
||||
m_userManager.GetUsers() = m_userManager.CreateDefaultUsers();
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
#include "common/io_file.h"
|
||||
#include "common/path_util.h"
|
||||
#include "core/devtools/layer.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/emulator_state.h"
|
||||
#include "input/controller.h"
|
||||
#include "input/input_mouse.h"
|
||||
@ -598,13 +599,13 @@ void ControllerOutput::FinalizeUpdate() {
|
||||
PushSDLEvent(SDL_EVENT_RDOC_CAPTURE);
|
||||
break;
|
||||
case HOTKEY_VOLUME_UP:
|
||||
Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() + 10, 0, 500),
|
||||
is_game_specific);
|
||||
EmulatorSettings.SetVolumeSlider(
|
||||
std::clamp(EmulatorSettings.GetVolumeSlider() + 10, 0, 500));
|
||||
Overlay::ShowVolume();
|
||||
break;
|
||||
case HOTKEY_VOLUME_DOWN:
|
||||
Config::setVolumeSlider(std::clamp(Config::getVolumeSlider() - 10, 0, 500),
|
||||
is_game_specific);
|
||||
EmulatorSettings.SetVolumeSlider(
|
||||
std::clamp(EmulatorSettings.GetVolumeSlider() - 10, 0, 500));
|
||||
Overlay::ShowVolume();
|
||||
break;
|
||||
case HOTKEY_QUIT:
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
#ifdef __APPLE__
|
||||
#include "SDL3/SDL_metal.h"
|
||||
#endif
|
||||
#include <core/emulator_settings.h>
|
||||
|
||||
namespace Input {
|
||||
|
||||
@ -323,9 +324,9 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
|
||||
}
|
||||
if (!error) {
|
||||
SDL_SetWindowFullscreenMode(
|
||||
window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL);
|
||||
window, EmulatorSettings.GetFullScreenMode() == "Fullscreen" ? displayMode : NULL);
|
||||
}
|
||||
SDL_SetWindowFullscreen(window, Config::getIsFullscreen());
|
||||
SDL_SetWindowFullscreen(window, EmulatorSettings.IsFullScreen());
|
||||
SDL_SyncWindow(window);
|
||||
|
||||
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
|
||||
|
||||
259
tests/.clang-format
Normal file
259
tests/.clang-format
Normal file
@ -0,0 +1,259 @@
|
||||
# SPDX-FileCopyrightText: 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlinesLeft: false
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
|
||||
IncludeCategories:
|
||||
- Regex: '^\<[^Q][^/.>]*\>'
|
||||
Priority: -2
|
||||
- Regex: '^\<'
|
||||
Priority: -1
|
||||
- Regex: '^\"'
|
||||
Priority: 0
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 150
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp11
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
---
|
||||
Language: Java
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlinesLeft: false
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 100
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
IncludeCategories:
|
||||
- Regex: '^\<[^Q][^/.>]*\>'
|
||||
Priority: -2
|
||||
- Regex: '^\<'
|
||||
Priority: -1
|
||||
- Regex: '^\"'
|
||||
Priority: 0
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 150
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
---
|
||||
Language: ObjC
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlinesLeft: false
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 100
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
IncludeCategories:
|
||||
- Regex: '^\<[^Q][^/.>]*\>'
|
||||
Priority: -2
|
||||
- Regex: '^\<'
|
||||
Priority: -1
|
||||
- Regex: '^\"'
|
||||
Priority: 0
|
||||
IndentCaseLabels: false
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 150
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
...
|
||||
84
tests/CMakeLists.txt
Normal file
84
tests/CMakeLists.txt
Normal file
@ -0,0 +1,84 @@
|
||||
# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# Find or download Google Test
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
googletest
|
||||
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
|
||||
set(SETTINGS_TEST_SOURCES
|
||||
# Under test
|
||||
${CMAKE_SOURCE_DIR}/src/core/emulator_settings.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/core/emulator_state.cpp
|
||||
|
||||
# Minimal common support
|
||||
${CMAKE_SOURCE_DIR}/src/common/path_util.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/assert.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/error.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/string_util.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/logging/filter.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/logging/text_formatter.cpp
|
||||
|
||||
# Stubs that replace dependencies
|
||||
stubs/log_stub.cpp
|
||||
stubs/scm_rev_stub.cpp
|
||||
stubs/sdl_stub.cpp
|
||||
|
||||
# Tests
|
||||
test_emulator_settings.cpp
|
||||
)
|
||||
|
||||
add_executable(shadps4_settings_test ${SETTINGS_TEST_SOURCES})
|
||||
|
||||
target_include_directories(shadps4_settings_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(shadps4_settings_test PRIVATE
|
||||
GTest::gtest_main
|
||||
fmt::fmt
|
||||
nlohmann_json::nlohmann_json
|
||||
toml11::toml11
|
||||
SDL3::SDL3
|
||||
)
|
||||
|
||||
target_compile_features(shadps4_settings_test PRIVATE cxx_std_23)
|
||||
|
||||
target_compile_definitions(shadps4_settings_test PRIVATE BOOST_ASIO_STANDALONE)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
include(CheckCXXSymbolExists)
|
||||
check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP)
|
||||
if (LIBCPP)
|
||||
target_compile_options(shadps4_settings_test PRIVATE -fexperimental-library)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
target_compile_definitions(shadps4_settings_test PRIVATE
|
||||
NOMINMAX
|
||||
WIN32_LEAN_AND_MEAN
|
||||
NTDDI_VERSION=0x0A000006
|
||||
_WIN32_WINNT=0x0A00
|
||||
WINVER=0x0A00
|
||||
)
|
||||
if (MSVC)
|
||||
target_compile_definitions(shadps4_settings_test PRIVATE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
_CRT_NONSTDC_NO_DEPRECATE
|
||||
_SCL_SECURE_NO_WARNINGS
|
||||
_TIMESPEC_DEFINED
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(shadps4_settings_test
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
PROPERTIES TIMEOUT 60
|
||||
)
|
||||
27
tests/stubs/log_stub.cpp
Normal file
27
tests/stubs/log_stub.cpp
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdio>
|
||||
#include <string_view>
|
||||
#include <fmt/format.h>
|
||||
#include "common/logging/backend.h"
|
||||
#include "common/logging/filter.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace Common::Log {
|
||||
|
||||
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
|
||||
unsigned int line_num, const char* function, const char* format,
|
||||
const fmt::format_args& args) {
|
||||
}
|
||||
|
||||
void Initialize(std::string_view) {}
|
||||
bool IsActive() { return false; }
|
||||
void SetGlobalFilter(const Filter&) {}
|
||||
void SetColorConsoleBackendEnabled(bool) {}
|
||||
void Start() {}
|
||||
void Stop() {}
|
||||
void Denitializer() {}
|
||||
void SetAppend() {}
|
||||
|
||||
} // namespace Common::Log
|
||||
22
tests/stubs/scm_rev_stub.cpp
Normal file
22
tests/stubs/scm_rev_stub.cpp
Normal file
@ -0,0 +1,22 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <string>
|
||||
#include "common/scm_rev.h"
|
||||
|
||||
namespace Common {
|
||||
|
||||
constexpr char g_version[] = "0.0.0 TEST";
|
||||
constexpr bool g_is_release = false;
|
||||
constexpr char g_scm_rev[] = "test_rev_hash";
|
||||
constexpr char g_scm_branch[] = "test_branch";
|
||||
constexpr char g_scm_desc[] = "test_desc";
|
||||
constexpr char g_scm_remote_name[] = "origin";
|
||||
constexpr char g_scm_remote_url[] = "https://github.com/test/shadPS4";
|
||||
constexpr char g_scm_date[] = "2026-03-23";
|
||||
|
||||
const std::string GetRemoteNameFromLink() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
18
tests/stubs/sdl_stub.cpp
Normal file
18
tests/stubs/sdl_stub.cpp
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <SDL3/SDL_messagebox.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) {
|
||||
if (buttonid) *buttonid = 0; // "No",skip migration
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */,
|
||||
const char* /* message */, SDL_Window* /* window */) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
837
tests/test_emulator_settings.cpp
Normal file
837
tests/test_emulator_settings.cpp
Normal file
@ -0,0 +1,837 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "common/path_util.h"
|
||||
#include "common/scm_rev.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/emulator_state.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
class TempDir {
|
||||
public:
|
||||
TempDir() {
|
||||
auto ns = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
temp_path = fs::temp_directory_path() / ("shadps4_test_" + std::to_string(ns) + "_" +
|
||||
std::to_string(reinterpret_cast<uintptr_t>(this)));
|
||||
fs::create_directories(temp_path);
|
||||
}
|
||||
~TempDir() {
|
||||
std::error_code ec;
|
||||
fs::remove_all(temp_path, ec);
|
||||
}
|
||||
const fs::path& path() const {
|
||||
return temp_path;
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path temp_path;
|
||||
};
|
||||
|
||||
static void WriteJson(const fs::path& p, const json& j) {
|
||||
std::ofstream out(p);
|
||||
ASSERT_TRUE(out.is_open()) << "Cannot write: " << p;
|
||||
out << std::setw(2) << j;
|
||||
}
|
||||
|
||||
static json ReadJson(const fs::path& p) {
|
||||
std::ifstream in(p);
|
||||
EXPECT_TRUE(in.is_open()) << "Cannot read: " << p;
|
||||
json j;
|
||||
in >> j;
|
||||
return j;
|
||||
}
|
||||
|
||||
class EmulatorSettingsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
temp_dir = std::make_unique<TempDir>();
|
||||
const fs::path root = temp_dir->path();
|
||||
|
||||
using PT = Common::FS::PathType;
|
||||
const struct {
|
||||
PT type;
|
||||
const char* sub;
|
||||
} dirs[] = {
|
||||
{PT::UserDir, ""},
|
||||
{PT::LogDir, "log"},
|
||||
{PT::ScreenshotsDir, "screenshots"},
|
||||
{PT::ShaderDir, "shader"},
|
||||
{PT::GameDataDir, "data"},
|
||||
{PT::TempDataDir, "temp"},
|
||||
{PT::SysModuleDir, "sys_modules"},
|
||||
{PT::DownloadDir, "download"},
|
||||
{PT::CapturesDir, "captures"},
|
||||
{PT::CheatsDir, "cheats"},
|
||||
{PT::PatchesDir, "patches"},
|
||||
{PT::MetaDataDir, "game_data"},
|
||||
{PT::CustomTrophy, "custom_trophy"},
|
||||
{PT::CustomConfigs, "custom_configs"},
|
||||
{PT::CacheDir, "cache"},
|
||||
{PT::FontsDir, "fonts"},
|
||||
{PT::HomeDir, "home"},
|
||||
};
|
||||
for (const auto& d : dirs) {
|
||||
fs::path p = d.sub[0] ? (root / d.sub) : root;
|
||||
fs::create_directories(p);
|
||||
Common::FS::SetUserPath(d.type, p);
|
||||
}
|
||||
|
||||
temp_state = std::make_shared<EmulatorState>();
|
||||
EmulatorState::SetInstance(temp_state);
|
||||
|
||||
temp_settings = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(temp_settings);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EmulatorSettingsImpl::SetInstance(nullptr);
|
||||
EmulatorState::SetInstance(nullptr);
|
||||
temp_settings.reset();
|
||||
temp_state.reset();
|
||||
temp_dir.reset();
|
||||
}
|
||||
|
||||
fs::path ConfigJson() const {
|
||||
return temp_dir->path() / "config.json";
|
||||
}
|
||||
fs::path GameConfig(const std::string& serial) const {
|
||||
return temp_dir->path() / "custom_configs" / (serial + ".json");
|
||||
}
|
||||
|
||||
std::unique_ptr<TempDir> temp_dir;
|
||||
std::shared_ptr<EmulatorSettingsImpl> temp_settings;
|
||||
std::shared_ptr<EmulatorState> temp_state;
|
||||
};
|
||||
|
||||
// tests Settting<T> template , default , Global override modes
|
||||
|
||||
TEST(SettingTest, DefaultCtorZeroInitialises) {
|
||||
Setting<int> s;
|
||||
EXPECT_EQ(s.value, 0);
|
||||
EXPECT_EQ(s.default_value, 0);
|
||||
EXPECT_FALSE(s.game_specific_value.has_value());
|
||||
}
|
||||
|
||||
TEST(SettingTest, ValueCtorSetsBothValueAndDefault) {
|
||||
Setting<int> s{42};
|
||||
EXPECT_EQ(s.value, 42);
|
||||
EXPECT_EQ(s.default_value, 42);
|
||||
}
|
||||
|
||||
TEST(SettingTest, GetDefaultPrefersGameSpecificOverBase) {
|
||||
Setting<int> s{10};
|
||||
s.value = 20;
|
||||
s.game_specific_value = 99;
|
||||
EXPECT_EQ(s.get(ConfigMode::Default), 99);
|
||||
}
|
||||
|
||||
TEST(SettingTest, GetDefaultFallsBackToBaseWhenNoOverride) {
|
||||
Setting<int> s{10};
|
||||
s.value = 20;
|
||||
EXPECT_EQ(s.get(ConfigMode::Default), 20);
|
||||
}
|
||||
|
||||
TEST(SettingTest, GetGlobalIgnoresGameSpecific) {
|
||||
Setting<int> s{10};
|
||||
s.value = 20;
|
||||
s.game_specific_value = 99;
|
||||
EXPECT_EQ(s.get(ConfigMode::Global), 20);
|
||||
}
|
||||
|
||||
TEST(SettingTest, GetCleanAlwaysReturnsFactoryDefault) {
|
||||
Setting<int> s{10};
|
||||
s.value = 20;
|
||||
s.game_specific_value = 99;
|
||||
EXPECT_EQ(s.get(ConfigMode::Clean), 10);
|
||||
}
|
||||
|
||||
TEST(SettingTest, SetWritesToBaseOnly) {
|
||||
Setting<int> s{0};
|
||||
s.game_specific_value = 55;
|
||||
s.set(77);
|
||||
EXPECT_EQ(s.value, 77);
|
||||
EXPECT_EQ(s.game_specific_value.value(), 55); // override untouched
|
||||
}
|
||||
|
||||
TEST(SettingTest, ResetGameSpecificClearsOverride) {
|
||||
Setting<int> s{0};
|
||||
s.game_specific_value = 55;
|
||||
s.reset_game_specific();
|
||||
EXPECT_FALSE(s.game_specific_value.has_value());
|
||||
// base and default must be intact
|
||||
EXPECT_EQ(s.value, 0);
|
||||
EXPECT_EQ(s.default_value, 0);
|
||||
}
|
||||
|
||||
TEST(SettingTest, BoolSettingAllModes) {
|
||||
Setting<bool> s{false};
|
||||
s.value = true;
|
||||
s.game_specific_value = false;
|
||||
EXPECT_FALSE(s.get(ConfigMode::Default));
|
||||
EXPECT_TRUE(s.get(ConfigMode::Global));
|
||||
EXPECT_FALSE(s.get(ConfigMode::Clean));
|
||||
}
|
||||
|
||||
TEST(SettingTest, StringSettingAllModes) {
|
||||
Setting<std::string> s{"shadow"};
|
||||
s.value = "rule";
|
||||
s.game_specific_value = "override";
|
||||
EXPECT_EQ(s.get(ConfigMode::Default), "override");
|
||||
EXPECT_EQ(s.get(ConfigMode::Global), "rule");
|
||||
EXPECT_EQ(s.get(ConfigMode::Clean), "shadow");
|
||||
}
|
||||
|
||||
TEST(SettingTest, NoGameSpecificDefaultAndGlobalAgree) {
|
||||
Setting<int> s{7};
|
||||
s.value = 7;
|
||||
EXPECT_EQ(s.get(ConfigMode::Default), s.get(ConfigMode::Global));
|
||||
}
|
||||
|
||||
// tests for default settings
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SetDefaultValuesResetsAllGroupsToFactory) {
|
||||
// set random values
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->SetWindowWidth(3840u);
|
||||
temp_settings->SetGpuId(2);
|
||||
temp_settings->SetDebugDump(true);
|
||||
temp_settings->SetCursorState(HideCursorState::Always);
|
||||
|
||||
temp_settings->SetDefaultValues(); // reset to defaults
|
||||
// check if values are reset to defaults
|
||||
EXPECT_FALSE(temp_settings->IsNeo());
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u);
|
||||
EXPECT_EQ(temp_settings->GetGpuId(), -1);
|
||||
EXPECT_FALSE(temp_settings->IsDebugDump());
|
||||
EXPECT_EQ(temp_settings->GetCursorState(), static_cast<int>(HideCursorState::Idle));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SetDefaultValuesClearsGameSpecificOverrides) {
|
||||
// check that game-specific overrides are cleared by SetDefaultValues
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA00001"), game);
|
||||
temp_settings->Load("CUSA00001");
|
||||
|
||||
temp_settings->SetDefaultValues();
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
|
||||
EXPECT_FALSE(temp_settings->IsNeo()); // default is false should be loaded instead of override
|
||||
}
|
||||
|
||||
// configModes tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigModeSetAndGetRoundTrips) {
|
||||
temp_settings->SetConfigMode(ConfigMode::Clean);
|
||||
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Clean);
|
||||
temp_settings->SetConfigMode(ConfigMode::Global);
|
||||
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Global);
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Default);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigModeCleanReturnFactoryDefaults) {
|
||||
temp_settings->SetWindowWidth(3840u);
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 2560;
|
||||
WriteJson(GameConfig("CUSA00001"), game);
|
||||
temp_settings->Load("CUSA00001");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Clean);
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // factory default
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigModeGlobalIgnoresGameSpecific) {
|
||||
temp_settings->SetNeo(false);
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA00001"), game);
|
||||
temp_settings->Load("CUSA00001");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Global);
|
||||
EXPECT_FALSE(temp_settings->IsNeo());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigModeDefaultResolvesGameSpecificWhenPresent) {
|
||||
temp_settings->SetNeo(false);
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA00001"), game);
|
||||
temp_settings->Load("CUSA00001");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_TRUE(temp_settings->IsNeo());
|
||||
}
|
||||
|
||||
// tests for global config.json file
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SaveCreatesConfigJson) {
|
||||
ASSERT_TRUE(temp_settings->Save());
|
||||
EXPECT_TRUE(fs::exists(ConfigJson()));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SaveWritesAllExpectedSections) {
|
||||
ASSERT_TRUE(temp_settings->Save());
|
||||
json j = ReadJson(ConfigJson());
|
||||
for (const char* section : {"General", "Debug", "Input", "Audio", "GPU", "Vulkan"})
|
||||
EXPECT_TRUE(j.contains(section)) << "Missing section: " << section;
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadReturnsTrueForExistingFile) {
|
||||
temp_settings->Save();
|
||||
auto fresh = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(fresh);
|
||||
EXPECT_TRUE(fresh->Load());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, RoundTripAllGroups) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->SetDebugDump(true);
|
||||
temp_settings->SetWindowWidth(1920u);
|
||||
temp_settings->SetGpuId(1);
|
||||
temp_settings->SetCursorState(HideCursorState::Always);
|
||||
temp_settings->SetAudioBackend(AudioBackend::OpenAL);
|
||||
temp_settings->Save();
|
||||
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load();
|
||||
EXPECT_TRUE(f->IsNeo());
|
||||
EXPECT_TRUE(f->IsDebugDump());
|
||||
EXPECT_EQ(f->GetWindowWidth(), 1920u);
|
||||
EXPECT_EQ(f->GetGpuId(), 1);
|
||||
EXPECT_EQ(f->GetCursorState(), static_cast<int>(HideCursorState::Always));
|
||||
EXPECT_EQ(f->GetAudioBackend(), static_cast<u32>(AudioBackend::OpenAL));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadMissingFileCreatesDefaultsOnDisk) {
|
||||
ASSERT_FALSE(fs::exists(ConfigJson()));
|
||||
temp_settings->Load();
|
||||
EXPECT_TRUE(fs::exists(ConfigJson()));
|
||||
EXPECT_FALSE(temp_settings->IsNeo()); // defaults
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadMissingSectionDoesNotZeroOtherSections) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->Save();
|
||||
json j = ReadJson(ConfigJson());
|
||||
j.erase("GPU");
|
||||
WriteJson(ConfigJson(), j);
|
||||
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load();
|
||||
|
||||
EXPECT_TRUE(f->IsNeo()); // belongs to General, should be loaded
|
||||
EXPECT_EQ(f->GetWindowWidth(), 1280); // GPU fell back to default
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadPreservesUnknownKeysOnResave) {
|
||||
temp_settings->Save();
|
||||
json j = ReadJson(ConfigJson());
|
||||
j["General"]["future_feature"] = "preserved";
|
||||
WriteJson(ConfigJson(), j);
|
||||
|
||||
// A fresh load + save (triggered by version mismatch) must keep the key
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load();
|
||||
f->Save();
|
||||
|
||||
json after = ReadJson(ConfigJson());
|
||||
EXPECT_EQ(after["General"]["future_feature"], "preserved");
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadUnknownTopLevelSectionPreserved) {
|
||||
temp_settings->Save();
|
||||
json j = ReadJson(ConfigJson());
|
||||
j["FutureSection"]["key"] = 42;
|
||||
WriteJson(ConfigJson(), j);
|
||||
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->Save(); // merge path
|
||||
|
||||
json after = ReadJson(ConfigJson());
|
||||
EXPECT_TRUE(after.contains("FutureSection"));
|
||||
EXPECT_EQ(after["FutureSection"]["key"], 42);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadCorruptJsonDoesNotCrash) {
|
||||
{
|
||||
std::ofstream out(ConfigJson());
|
||||
out << "{NOT VALID JSON!!!";
|
||||
}
|
||||
EXPECT_NO_THROW(temp_settings->Load());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadEmptyJsonObjectDoesNotCrash) {
|
||||
WriteJson(ConfigJson(), json::object());
|
||||
EXPECT_NO_THROW(temp_settings->Load());
|
||||
}
|
||||
|
||||
// tests for per game config
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SaveSerialCreatesPerGameFile) {
|
||||
ASSERT_TRUE(temp_settings->Save("CUSA01234"));
|
||||
EXPECT_TRUE(fs::exists(GameConfig("CUSA01234")));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialReturnsFalseWhenFileAbsent) {
|
||||
EXPECT_FALSE(temp_settings->Load("CUSA99999"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialAppliesOverrideToGameSpecificValue) {
|
||||
temp_settings->SetNeo(false);
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
|
||||
ASSERT_TRUE(temp_settings->Load("CUSA01234"));
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_TRUE(temp_settings->IsNeo());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialBaseValueUntouched) {
|
||||
temp_settings->SetWindowWidth(1280);
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Global);
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialOverridesMultipleGroups) {
|
||||
temp_settings->SetNeo(false);
|
||||
temp_settings->SetWindowWidth(1280u);
|
||||
temp_settings->SetDebugDump(false);
|
||||
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
game["Debug"]["debug_dump"] = true;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_TRUE(temp_settings->IsNeo());
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 3840);
|
||||
EXPECT_TRUE(temp_settings->IsDebugDump());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialUnrecognisedKeyIgnored) {
|
||||
json game;
|
||||
game["GPU"]["key_that_does_not_exist"] = 999;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialTypeMismatch_DoesNotCrash) {
|
||||
json game;
|
||||
game["GPU"]["window_width"] = "not_a_number";
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
|
||||
// base unchanged
|
||||
temp_settings->SetConfigMode(ConfigMode::Global);
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, LoadSerialCorruptFileDoesNotCrash) {
|
||||
{
|
||||
std::ofstream out(GameConfig("CUSA01234"));
|
||||
out << "{{{{totally broken";
|
||||
}
|
||||
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SaveSerialWritesGameSpecificValueWhenOverrideLoaded) {
|
||||
temp_settings->SetWindowWidth(1280);
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->Save("CUSA01234");
|
||||
|
||||
json saved = ReadJson(GameConfig("CUSA01234"));
|
||||
EXPECT_EQ(saved["GPU"]["window_width"].get<unsigned>(), 3840);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SaveSerialWritesBaseValueWhenNoOverrideSet) {
|
||||
temp_settings->SetWindowWidth(2560);
|
||||
temp_settings->Save("CUSA01234");
|
||||
|
||||
json saved = ReadJson(GameConfig("CUSA01234"));
|
||||
EXPECT_EQ(saved["GPU"]["window_width"].get<unsigned>(), 2560);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, MultipleSerialsDoNotInterfere) {
|
||||
json g1;
|
||||
g1["General"]["neo_mode"] = true;
|
||||
g1["GPU"]["window_width"] = 3840;
|
||||
WriteJson(GameConfig("CUSA00001"), g1);
|
||||
|
||||
json g2;
|
||||
g2["General"]["neo_mode"] = false;
|
||||
g2["GPU"]["window_width"] = 1920;
|
||||
WriteJson(GameConfig("CUSA00002"), g2);
|
||||
|
||||
{
|
||||
auto s = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(s);
|
||||
s->Load();
|
||||
s->Load("CUSA00001");
|
||||
s->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_TRUE(s->IsNeo());
|
||||
EXPECT_EQ(s->GetWindowWidth(), 3840);
|
||||
}
|
||||
{
|
||||
auto s = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(s);
|
||||
s->Load();
|
||||
s->Load("CUSA00002");
|
||||
s->SetConfigMode(ConfigMode::Default);
|
||||
EXPECT_FALSE(s->IsNeo());
|
||||
EXPECT_EQ(s->GetWindowWidth(), 1920);
|
||||
}
|
||||
}
|
||||
|
||||
// ClearGameSpecificOverrides tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesRemovesAllGroups) {
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
game["Debug"]["debug_dump"] = true;
|
||||
game["Input"]["cursor_state"] = 2;
|
||||
game["Audio"]["audio_backend"] = 1;
|
||||
game["Vulkan"]["gpu_id"] = 2;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->ClearGameSpecificOverrides();
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
|
||||
EXPECT_FALSE(temp_settings->IsNeo());
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
|
||||
EXPECT_FALSE(temp_settings->IsDebugDump());
|
||||
EXPECT_EQ(temp_settings->GetCursorState(), static_cast<int>(HideCursorState::Idle));
|
||||
EXPECT_EQ(temp_settings->GetGpuId(), -1);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesDoesNotTouchBaseValues) {
|
||||
temp_settings->SetWindowWidth(1920);
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->ClearGameSpecificOverrides();
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Global);
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1920);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverrides_NoopWhenNothingLoaded) {
|
||||
EXPECT_NO_THROW(temp_settings->ClearGameSpecificOverrides());
|
||||
}
|
||||
|
||||
// ResetGameSpecificValue tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ResetGameSpecificValue_ClearsNamedKey) {
|
||||
temp_settings->SetWindowWidth(1280);
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
ASSERT_EQ(temp_settings->GetWindowWidth(), 3840);
|
||||
|
||||
temp_settings->ResetGameSpecificValue("window_width");
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ResetGameSpecificValueOnlyAffectsTargetKey) {
|
||||
json game;
|
||||
game["GPU"]["window_width"] = 3840;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
|
||||
temp_settings->ResetGameSpecificValue("window_width");
|
||||
temp_settings->SetConfigMode(ConfigMode::Default);
|
||||
|
||||
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // cleared
|
||||
EXPECT_TRUE(temp_settings->IsNeo()); // still set
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ResetGameSpecificValueUnknownKeyNoOp) {
|
||||
EXPECT_NO_THROW(temp_settings->ResetGameSpecificValue("does_not_exist"));
|
||||
}
|
||||
|
||||
// GameInstallDir tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, AddGameInstallDirAddsEnabled) {
|
||||
fs::path dir = temp_dir->path() / "games";
|
||||
fs::create_directories(dir);
|
||||
EXPECT_TRUE(temp_settings->AddGameInstallDir(dir));
|
||||
ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
|
||||
EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], dir);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, AddGameInstallDirRejectsDuplicate) {
|
||||
fs::path dir = temp_dir->path() / "games";
|
||||
fs::create_directories(dir);
|
||||
temp_settings->AddGameInstallDir(dir);
|
||||
EXPECT_FALSE(temp_settings->AddGameInstallDir(dir));
|
||||
EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, RemoveGameInstallDirRemovesEntry) {
|
||||
fs::path dir = temp_dir->path() / "games";
|
||||
fs::create_directories(dir);
|
||||
temp_settings->AddGameInstallDir(dir);
|
||||
temp_settings->RemoveGameInstallDir(dir);
|
||||
EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, RemoveGameInstallDirNoopForMissing) {
|
||||
EXPECT_NO_THROW(temp_settings->RemoveGameInstallDir("/nonexistent/path"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledDisablesDir) {
|
||||
fs::path dir = temp_dir->path() / "games";
|
||||
fs::create_directories(dir);
|
||||
temp_settings->AddGameInstallDir(dir, true);
|
||||
temp_settings->SetGameInstallDirEnabled(dir, false);
|
||||
EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledReEnablesDir) {
|
||||
fs::path dir = temp_dir->path() / "games";
|
||||
fs::create_directories(dir);
|
||||
temp_settings->AddGameInstallDir(dir, false);
|
||||
ASSERT_TRUE(temp_settings->GetGameInstallDirs().empty());
|
||||
temp_settings->SetGameInstallDirEnabled(dir, true);
|
||||
EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, SetAllGameInstallDirsReplacesExistingList) {
|
||||
fs::path d1 = temp_dir->path() / "g1";
|
||||
fs::path d2 = temp_dir->path() / "g2";
|
||||
fs::create_directories(d1);
|
||||
fs::create_directories(d2);
|
||||
temp_settings->AddGameInstallDir(d1);
|
||||
|
||||
temp_settings->SetAllGameInstallDirs({{d2, true}});
|
||||
ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
|
||||
EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], d2);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GameInstallDirsFullRoundTripWithEnabledFlags) {
|
||||
fs::path d1 = temp_dir->path() / "g1";
|
||||
fs::path d2 = temp_dir->path() / "g2";
|
||||
fs::create_directories(d1);
|
||||
fs::create_directories(d2);
|
||||
temp_settings->AddGameInstallDir(d1, true);
|
||||
temp_settings->AddGameInstallDir(d2, false);
|
||||
temp_settings->Save();
|
||||
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load();
|
||||
|
||||
const auto& all = f->GetAllGameInstallDirs();
|
||||
ASSERT_EQ(all.size(), 2u);
|
||||
EXPECT_EQ(all[0].path, d1);
|
||||
EXPECT_TRUE(all[0].enabled);
|
||||
EXPECT_EQ(all[1].path, d2);
|
||||
EXPECT_FALSE(all[1].enabled);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetGameInstallDirsEnabledReflectsState) {
|
||||
fs::path d1 = temp_dir->path() / "g1";
|
||||
fs::path d2 = temp_dir->path() / "g2";
|
||||
fs::create_directories(d1);
|
||||
fs::create_directories(d2);
|
||||
temp_settings->AddGameInstallDir(d1, true);
|
||||
temp_settings->AddGameInstallDir(d2, false);
|
||||
|
||||
auto enabled = temp_settings->GetGameInstallDirsEnabled();
|
||||
ASSERT_EQ(enabled.size(), 2u);
|
||||
EXPECT_TRUE(enabled[0]);
|
||||
EXPECT_FALSE(enabled[1]);
|
||||
}
|
||||
|
||||
// GetAllOverrideableKeys tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysIsNonEmpty) {
|
||||
EXPECT_FALSE(temp_settings->GetAllOverrideableKeys().empty());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysContainsRepresentativeKeys) {
|
||||
auto keys = temp_settings->GetAllOverrideableKeys();
|
||||
auto has = [&](const char* k) { return std::find(keys.begin(), keys.end(), k) != keys.end(); };
|
||||
// General
|
||||
EXPECT_TRUE(has("neo_mode"));
|
||||
EXPECT_TRUE(has("volume_slider"));
|
||||
// GPU
|
||||
EXPECT_TRUE(has("window_width"));
|
||||
EXPECT_TRUE(has("null_gpu"));
|
||||
EXPECT_TRUE(has("vblank_frequency"));
|
||||
// Vulkan
|
||||
EXPECT_TRUE(has("gpu_id"));
|
||||
EXPECT_TRUE(has("pipeline_cache_enabled"));
|
||||
// Debug
|
||||
EXPECT_TRUE(has("debug_dump"));
|
||||
EXPECT_TRUE(has("log_enabled"));
|
||||
// Input
|
||||
EXPECT_TRUE(has("cursor_state"));
|
||||
// Audio
|
||||
EXPECT_TRUE(has("audio_backend"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysNoDuplicates) {
|
||||
auto keys = temp_settings->GetAllOverrideableKeys();
|
||||
std::vector<std::string> sorted = keys;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
auto it = std::unique(sorted.begin(), sorted.end());
|
||||
EXPECT_EQ(it, sorted.end()) << "Duplicate key found in overrideable keys list";
|
||||
}
|
||||
|
||||
// Per-group GetOverrideableFields tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetGeneralOverrideableFieldsNonEmpty) {
|
||||
EXPECT_FALSE(temp_settings->GetGeneralOverrideableFields().empty());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetGPUOverrideableFieldsContainsWindowAndFullscreen) {
|
||||
auto fields = temp_settings->GetGPUOverrideableFields();
|
||||
auto has = [&](const char* k) {
|
||||
return std::any_of(fields.begin(), fields.end(),
|
||||
[k](const OverrideItem& f) { return std::string(f.key) == k; });
|
||||
};
|
||||
EXPECT_TRUE(has("window_width"));
|
||||
EXPECT_TRUE(has("window_height"));
|
||||
EXPECT_TRUE(has("full_screen"));
|
||||
EXPECT_TRUE(has("vblank_frequency"));
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, GetVulkanOverrideableFieldsContainsGpuId) {
|
||||
auto fields = temp_settings->GetVulkanOverrideableFields();
|
||||
bool found = std::any_of(fields.begin(), fields.end(),
|
||||
[](const OverrideItem& f) { return std::string(f.key) == "gpu_id"; });
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
// Path accessors tests
|
||||
TEST_F(EmulatorSettingsTest, GetHomeDirReturnsCustomWhenSet) {
|
||||
fs::path dir = temp_dir->path() / "custom_home";
|
||||
fs::create_directories(dir);
|
||||
temp_settings->SetHomeDir(dir);
|
||||
EXPECT_EQ(temp_settings->GetHomeDir(), dir);
|
||||
}
|
||||
TEST_F(EmulatorSettingsTest, GetSysModulesDirFallsBackToPathUtilWhenEmpty) {
|
||||
// default_value is empty; GetSysModulesDir falls back to GetUserPath(SysModuleDir)
|
||||
auto result = temp_settings->GetSysModulesDir();
|
||||
EXPECT_FALSE(result.empty());
|
||||
}
|
||||
TEST_F(EmulatorSettingsTest, GetFontsDirFallsBackToPathUtilWhenEmpty) {
|
||||
auto result = temp_settings->GetFontsDir();
|
||||
EXPECT_FALSE(result.empty());
|
||||
}
|
||||
|
||||
// edge cases tests
|
||||
|
||||
TEST_F(EmulatorSettingsTest, VersionMismatchPreservesSettings) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->SetWindowWidth(2560u);
|
||||
temp_settings->Save();
|
||||
|
||||
// Force a stale version string so the mismatch branch fires
|
||||
json j = ReadJson(ConfigJson());
|
||||
j["Debug"]["config_version"] = "old_hash_0000";
|
||||
WriteJson(ConfigJson(), j);
|
||||
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load(); // triggers version-bump Save() internally
|
||||
|
||||
EXPECT_TRUE(f->IsNeo());
|
||||
EXPECT_EQ(f->GetWindowWidth(), 2560u);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, DoubleGlobalLoadIsIdempotent) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->SetWindowWidth(2560u);
|
||||
temp_settings->Save();
|
||||
|
||||
auto f = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(f);
|
||||
f->Load(""); // first — loads from disk
|
||||
f->Load(""); // second — must not reset anything
|
||||
|
||||
EXPECT_TRUE(f->IsNeo());
|
||||
EXPECT_EQ(f->GetWindowWidth(), 2560u);
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigUsedFlagTrueWhenFileExists) {
|
||||
json game;
|
||||
game["General"]["neo_mode"] = true;
|
||||
WriteJson(GameConfig("CUSA01234"), game);
|
||||
temp_settings->Load("CUSA01234");
|
||||
EXPECT_TRUE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, ConfigUsedFlagFalseWhenFileAbsent) {
|
||||
temp_settings->Load("CUSA99999");
|
||||
EXPECT_FALSE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed());
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, DestructorNoSaveIfLoadNeverCalled) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->Save();
|
||||
auto t0 = fs::last_write_time(ConfigJson());
|
||||
|
||||
{
|
||||
// Create and immediately destroy without calling Load()
|
||||
auto untouched = std::make_shared<EmulatorSettingsImpl>();
|
||||
// destructor fires here
|
||||
}
|
||||
|
||||
auto t1 = fs::last_write_time(ConfigJson());
|
||||
EXPECT_EQ(t0, t1) << "Destructor wrote config.json without a prior Load()";
|
||||
}
|
||||
|
||||
TEST_F(EmulatorSettingsTest, DestructorSavesAfterSuccessfulLoad) {
|
||||
temp_settings->SetNeo(true);
|
||||
temp_settings->Save();
|
||||
|
||||
{
|
||||
auto s = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(s);
|
||||
s->Load();
|
||||
s->SetWindowWidth(2560u); // mutate after successful load
|
||||
// destructor should write this change
|
||||
}
|
||||
|
||||
auto verify = std::make_shared<EmulatorSettingsImpl>();
|
||||
EmulatorSettingsImpl::SetInstance(verify);
|
||||
verify->Load();
|
||||
EXPECT_EQ(verify->GetWindowWidth(), 2560);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user