From ffb3ef9ec8c796e1248ed9c4f1212ac5a1deda49 Mon Sep 17 00:00:00 2001 From: Putta Khunchalee Date: Sun, 25 Aug 2024 01:08:12 +0700 Subject: [PATCH] Implements profile saving and loading (#947) --- src/CMakeLists.txt | 1 + src/app_data.cpp | 4 +- src/app_data.hpp | 2 +- src/core.h | 17 ++++++ src/core.hpp | 13 ++++- src/core/Cargo.toml | 3 + src/core/src/lib.rs | 1 + src/core/src/profile/mod.rs | 107 ++++++++++++++++++++++++++++++++++++ src/launch_settings.cpp | 18 +++++- src/launch_settings.hpp | 8 ++- src/main.cpp | 4 +- src/main_window.cpp | 97 ++++++++++++++++++++++++++++++-- src/main_window.hpp | 8 ++- src/profile_models.cpp | 58 +++++++++++++++++++ src/profile_models.hpp | 21 +++++++ 15 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 src/core/src/profile/mod.rs create mode 100644 src/profile_models.cpp create mode 100644 src/profile_models.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e786c9b6..e7c7baa3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,6 +72,7 @@ add_executable(obliteration WIN32 MACOSX_BUNDLE path.cpp pkg_extractor.cpp pkg_installer.cpp + profile_models.cpp progress_dialog.cpp resources.cpp resources.qrc diff --git a/src/app_data.cpp b/src/app_data.cpp index a0e81c36..730b0b21 100644 --- a/src/app_data.cpp +++ b/src/app_data.cpp @@ -9,8 +9,8 @@ static QString root() return QDir::toNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation)); } -QString kernelDebugDump() +QString profiles() { - auto path = joinPath(root(), "kernel"); + auto path = joinPath(root(), "profiles"); return QString::fromStdString(path); } diff --git a/src/app_data.hpp b/src/app_data.hpp index f7bb6df3..b988fda3 100644 --- a/src/app_data.hpp +++ b/src/app_data.hpp @@ -2,4 +2,4 @@ #include -QString kernelDebugDump(); +QString profiles(); diff --git a/src/core.h b/src/core.h index 278580d8..c592a930 100644 --- a/src/core.h +++ b/src/core.h @@ -20,6 +20,11 @@ enum VmmLog { VmmLog_Info, }; +/** + * Contains settings to launch the kernel. + */ +struct Profile; + /** * Error object managed by Rust side. */ @@ -109,6 +114,18 @@ struct RustError *pkg_extract(const Pkg *pkg, const char *dir, void (*status)(co uint64_t, void*), void *ud); +struct Profile *profile_new(const char *name); + +struct Profile *profile_load(const char *path, struct RustError **err); + +void profile_free(struct Profile *p); + +char *profile_id(const struct Profile *p); + +const char *profile_name(const struct Profile *p); + +struct RustError *profile_save(const struct Profile *p, const char *path); + struct RustError *update_firmware(const char *root, const char *fw, void *cx, diff --git a/src/core.hpp b/src/core.hpp index 0651dd46..5e58d6da 100644 --- a/src/core.hpp +++ b/src/core.hpp @@ -40,7 +40,7 @@ public: return *this; } - operator T *() { return m_ptr; } + operator T *() const { return m_ptr; } operator bool() const { return m_ptr != nullptr; } T **operator&() @@ -49,7 +49,7 @@ public: return &m_ptr; } - T *get() { return m_ptr; } + T *get() const { return m_ptr; } void free(); private: T *m_ptr; @@ -80,6 +80,15 @@ inline void Rust::free() } } +template<> +inline void Rust::free() +{ + if (m_ptr) { + profile_free(m_ptr); + m_ptr = nullptr; + } +} + template<> inline void Rust::free() { diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 94921acf..53b7b439 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -14,7 +14,10 @@ obfw = { git = "https://github.com/obhq/firmware-dumper.git", features = ["read" obvirt = { path = "../obvirt" } param = { path = "../param" } pkg = { path = "../pkg" } +postcard = { version = "1.0.10", features = ["use-std"], default-features = false } +serde = { version = "1.0.209", features = ["derive"] } thiserror = "1.0" +uuid = { version = "1.10.0", features = ["serde", "v4"] } [target.'cfg(not(target_os = "macos"))'.dependencies] ash = "0.38.0" diff --git a/src/core/src/lib.rs b/src/core/src/lib.rs index 1db9c169..1e1e6e96 100644 --- a/src/core/src/lib.rs +++ b/src/core/src/lib.rs @@ -3,6 +3,7 @@ use std::ffi::{c_char, c_void}; mod error; mod param; mod pkg; +mod profile; mod string; mod system; mod vmm; diff --git a/src/core/src/profile/mod.rs b/src/core/src/profile/mod.rs new file mode 100644 index 00000000..6298fb88 --- /dev/null +++ b/src/core/src/profile/mod.rs @@ -0,0 +1,107 @@ +use crate::error::RustError; +use crate::string::strdup; +use serde::{Deserialize, Serialize}; +use std::ffi::{c_char, CStr, CString}; +use std::fs::File; +use std::path::Path; +use std::ptr::null_mut; +use std::time::SystemTime; +use uuid::Uuid; + +#[no_mangle] +pub unsafe extern "C" fn profile_new(name: *const c_char) -> *mut Profile { + Box::into_raw(Box::new(Profile { + id: Uuid::new_v4(), + name: CStr::from_ptr(name).to_owned(), + created: SystemTime::now(), + })) +} + +#[no_mangle] +pub unsafe extern "C" fn profile_load( + path: *const c_char, + err: *mut *mut RustError, +) -> *mut Profile { + // Check if path UTF-8. + let root = match CStr::from_ptr(path).to_str() { + Ok(v) => Path::new(v), + Err(_) => { + *err = RustError::new("the specified path is not UTF-8"); + return null_mut(); + } + }; + + // TODO: Use from_io() once https://github.com/jamesmunns/postcard/issues/162 is implemented. + let path = root.join("profile.bin"); + let data = match std::fs::read(&path) { + Ok(v) => v, + Err(e) => { + *err = RustError::with_source(format_args!("couldn't read {}", path.display()), e); + return null_mut(); + } + }; + + // Load profile.bin. + let p = match postcard::from_bytes(&data) { + Ok(v) => v, + Err(e) => { + *err = RustError::with_source(format_args!("couldn't load {}", path.display()), e); + return null_mut(); + } + }; + + Box::into_raw(Box::new(p)) +} + +#[no_mangle] +pub unsafe extern "C" fn profile_free(p: *mut Profile) { + drop(Box::from_raw(p)); +} + +#[no_mangle] +pub unsafe extern "C" fn profile_id(p: *const Profile) -> *mut c_char { + strdup((*p).id.to_string()) +} + +#[no_mangle] +pub unsafe extern "C" fn profile_name(p: *const Profile) -> *const c_char { + (*p).name.as_ptr() +} + +#[no_mangle] +pub unsafe extern "C" fn profile_save(p: *const Profile, path: *const c_char) -> *mut RustError { + // Check if path UTF-8. + let root = match CStr::from_ptr(path).to_str() { + Ok(v) => Path::new(v), + Err(_) => return RustError::new("the specified path is not UTF-8"), + }; + + // Create a directory. + if let Err(e) = std::fs::create_dir_all(root) { + return RustError::with_source("couldn't create the specified path", e); + } + + // Create profile.bin. + let path = root.join("profile.bin"); + let file = match File::create(&path) { + Ok(v) => v, + Err(e) => { + return RustError::with_source(format_args!("couldn't create {}", path.display()), e) + } + }; + + // Write profile.bin. + if let Err(e) = postcard::to_io(&*p, file) { + return RustError::with_source(format_args!("couldn't write {}", path.display()), e); + } + + null_mut() +} + +/// Contains settings to launch the kernel. +#[derive(Deserialize, Serialize)] +pub struct Profile { + id: Uuid, + name: CString, + created: SystemTime, +} diff --git a/src/launch_settings.cpp b/src/launch_settings.cpp index 5730ef44..967e8b98 100644 --- a/src/launch_settings.cpp +++ b/src/launch_settings.cpp @@ -3,6 +3,7 @@ #include "game_models.hpp" #include "game_settings.hpp" #include "game_settings_dialog.hpp" +#include "profile_models.hpp" #include "resources.hpp" #include @@ -17,7 +18,7 @@ #include #include -LaunchSettings::LaunchSettings(GameListModel *games, QWidget *parent) : +LaunchSettings::LaunchSettings(ProfileList *profiles, GameListModel *games, QWidget *parent) : QWidget(parent), m_display(nullptr), m_games(nullptr), @@ -26,7 +27,7 @@ LaunchSettings::LaunchSettings(GameListModel *games, QWidget *parent) : auto layout = new QVBoxLayout(); layout->addWidget(buildSettings(games)); - layout->addLayout(buildActions()); + layout->addLayout(buildActions(profiles)); setLayout(layout); } @@ -63,12 +64,13 @@ QWidget *LaunchSettings::buildSettings(GameListModel *games) return tab; } -QLayout *LaunchSettings::buildActions() +QLayout *LaunchSettings::buildActions(ProfileList *profiles) { auto layout = new QHBoxLayout(); // Profile list. m_profiles = new QComboBox(); + m_profiles->setModel(profiles); layout->addWidget(m_profiles, 1); @@ -80,6 +82,16 @@ QLayout *LaunchSettings::buildActions() // Save button. auto save = new QPushButton(loadIcon(":/resources/content-save.svg"), "Save"); + connect(save, &QAbstractButton::clicked, [this]() { + auto index = m_profiles->currentIndex(); + + if (index >= 0) { + auto profiles = reinterpret_cast(m_profiles->model()); + + emit saveClicked(profiles->get(index)); + } + }); + actions->addButton(save, QDialogButtonBox::ApplyRole); // Start button. diff --git a/src/launch_settings.hpp b/src/launch_settings.hpp index 77c85367..9d9c21ca 100644 --- a/src/launch_settings.hpp +++ b/src/launch_settings.hpp @@ -1,9 +1,12 @@ #pragma once +#include "core.h" + #include class DisplaySettings; class GameListModel; +class ProfileList; class QComboBox; class QLayout; class QTableView; @@ -11,13 +14,14 @@ class QTableView; class LaunchSettings final : public QWidget { Q_OBJECT public: - LaunchSettings(GameListModel *games, QWidget *parent = nullptr); + LaunchSettings(ProfileList *profiles, GameListModel *games, QWidget *parent = nullptr); ~LaunchSettings() override; signals: + void saveClicked(Profile *p); void startClicked(); private: QWidget *buildSettings(GameListModel *games); - QLayout *buildActions(); + QLayout *buildActions(ProfileList *profiles); void requestGamesContextMenu(const QPoint &pos); diff --git a/src/main.cpp b/src/main.cpp index cf58e398..27ea7a06 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -111,9 +111,11 @@ int main(int argc, char *argv[]) MainWindow win(&vulkan); #endif - if (!win.loadGames()) { + if (!win.loadProfiles() || !win.loadGames()) { return 1; } + win.restoreGeometry(); + return QApplication::exec(); } diff --git a/src/main_window.cpp b/src/main_window.cpp index 495b2673..0d09ea29 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -1,9 +1,11 @@ #include "main_window.hpp" +#include "app_data.hpp" #include "game_models.hpp" #include "launch_settings.hpp" #include "logs_viewer.hpp" #include "path.hpp" #include "pkg_installer.hpp" +#include "profile_models.hpp" #include "resources.hpp" #include "screen.hpp" #include "settings.hpp" @@ -41,6 +43,7 @@ MainWindow::MainWindow() : MainWindow::MainWindow(QVulkanInstance *vulkan) : #endif m_main(nullptr), + m_profiles(nullptr), m_games(nullptr), m_launch(nullptr), m_screen(nullptr) @@ -96,9 +99,11 @@ MainWindow::MainWindow(QVulkanInstance *vulkan) : setCentralWidget(m_main); // Launch settings. + m_profiles = new ProfileList(this); m_games = new GameListModel(this); - m_launch = new LaunchSettings(m_games); + m_launch = new LaunchSettings(m_profiles, m_games); + connect(m_launch, &LaunchSettings::saveClicked, this, &MainWindow::saveClicked); connect(m_launch, &LaunchSettings::startClicked, this, &MainWindow::startKernel); m_main->addWidget(m_launch); @@ -113,15 +118,67 @@ MainWindow::MainWindow(QVulkanInstance *vulkan) : connect(m_screen, &Screen::updateRequestReceived, this, &MainWindow::updateScreen); m_main->addWidget(createWindowContainer(m_screen)); - - // Show the window. - restoreGeometry(); } MainWindow::~MainWindow() { } +bool MainWindow::loadProfiles() +{ + // List profile directories. + auto root = profiles(); + auto dirs = QDir(root).entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + // Create default profile if the user don't have any profiles. + if (dirs.isEmpty()) { + Rust p; + Rust id; + + p = profile_new("Default"); + id = profile_id(p); + + // Save. + auto path = joinPath(root, id.get()); + Rust error; + + error = profile_save(p, path.c_str()); + + if (error) { + auto text = QString("Failed to save default profile to %1: %2.") + .arg(path.c_str()) + .arg(error_message(error)); + + QMessageBox::critical(this, "Error", text); + return false; + } + + dirs.append(id.get()); + } + + // Load profiles. + for (auto &dir : dirs) { + auto path = joinPath(root, dir); + Rust error; + Rust profile; + + profile = profile_load(path.c_str(), &error); + + if (!profile) { + auto text = QString("Failed to load a profile from %1: %2.") + .arg(path.c_str()) + .arg(error_message(error)); + + QMessageBox::critical(this, "Error", text); + return false; + } + + m_profiles->add(std::move(profile)); + } + + return true; +} + bool MainWindow::loadGames() { // Get game counts. @@ -254,7 +311,37 @@ void MainWindow::reportIssue() void MainWindow::aboutObliteration() { - QMessageBox::about(this, "About Obliteration", "Obliteration is a free and open-source software for playing your PlayStation 4 titles on PC."); + QMessageBox::about( + this, + "About Obliteration", + "Obliteration is a free and open-source PlayStation 4 kernel. It will allows you to run " + "the PlayStation 4 system software that you have dumped from your PlayStation 4 on your " + "PC. This will allows you to play your games forever even if your PlayStation 4 stopped " + "working in the future."); +} + +void MainWindow::saveClicked(Profile *p) +{ + // Get ID. + Rust id; + + id = profile_id(p); + + // Save. + auto root = profiles(); + auto path = joinPath(root, id.get()); + Rust error; + + error = profile_save(p, path.c_str()); + + if (error) { + auto text = QString("Failed to save %1 profile to %2: %3.") + .arg(profile_name(p)) + .arg(path.c_str()) + .arg(error_message(error)); + + QMessageBox::critical(this, "Error", text); + } } void MainWindow::startKernel() diff --git a/src/main_window.hpp b/src/main_window.hpp index 6124f97d..0f91552b 100644 --- a/src/main_window.hpp +++ b/src/main_window.hpp @@ -8,6 +8,7 @@ class GameListModel; class LaunchSettings; class LogsViewer; +class ProfileList; class QStackedWidget; #ifndef __APPLE__ class QVulkanInstance; @@ -21,9 +22,11 @@ public: #else MainWindow(QVulkanInstance *vulkan); #endif - ~MainWindow(); + ~MainWindow() override; + bool loadProfiles(); bool loadGames(); + void restoreGeometry(); protected: void closeEvent(QCloseEvent *event) override; @@ -33,18 +36,19 @@ private slots: void viewLogs(); void reportIssue(); void aboutObliteration(); + void saveClicked(Profile *p); void startKernel(); void updateScreen(); private: void log(VmmLog type, const QString &msg); bool loadGame(const QString &gameId); - void restoreGeometry(); bool requireEmulatorStopped(); static bool vmmHandler(const VmmEvent *ev, void *cx); QStackedWidget *m_main; + ProfileList *m_profiles; GameListModel *m_games; LaunchSettings *m_launch; Screen *m_screen; diff --git a/src/profile_models.cpp b/src/profile_models.cpp new file mode 100644 index 00000000..5c1df9b7 --- /dev/null +++ b/src/profile_models.cpp @@ -0,0 +1,58 @@ +#include "profile_models.hpp" + +ProfileList::ProfileList(QObject *parent) : + QAbstractListModel(parent) +{ +} + +ProfileList::~ProfileList() +{ +} + +void ProfileList::add(Rust &&p) +{ + beginInsertRows(QModelIndex(), m_items.size(), m_items.size()); + m_items.push_back(std::move(p)); + endInsertRows(); + + sort(0); +} + +int ProfileList::rowCount(const QModelIndex &) const +{ + return m_items.size(); +} + +QVariant ProfileList::data(const QModelIndex &index, int role) const +{ + auto &p = m_items[index.row()]; + + switch (role) { + case Qt::DisplayRole: + return profile_name(p); + } + + return {}; +} + +void ProfileList::sort(int column, Qt::SortOrder order) +{ + emit layoutAboutToBeChanged(); + + switch (column) { + case 0: + std::sort( + m_items.begin(), + m_items.end(), + [order](const Rust &a, const Rust &b) { + if (order == Qt::AscendingOrder) { + return strcmp(profile_name(a), profile_name(b)); + } else { + return strcmp(profile_name(b), profile_name(a)); + } + }); + break; + } + + emit layoutChanged(); +} diff --git a/src/profile_models.hpp b/src/profile_models.hpp new file mode 100644 index 00000000..20a179fa --- /dev/null +++ b/src/profile_models.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "core.hpp" + +#include + +#include + +class ProfileList final : public QAbstractListModel { +public: + ProfileList(QObject *parent = nullptr); + ~ProfileList() override; + + void add(Rust &&p); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Profile *get(size_t i) const { return m_items[i]; } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; +private: + std::vector> m_items; +};