Implements profile saving and loading (#947)

This commit is contained in:
Putta Khunchalee 2024-08-25 01:08:12 +07:00 committed by GitHub
parent 0f85fbb946
commit ffb3ef9ec8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 344 additions and 18 deletions

View File

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

View File

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

View File

@ -2,4 +2,4 @@
#include <QString>
QString kernelDebugDump();
QString profiles();

View File

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

View File

@ -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<Pkg>::free()
}
}
template<>
inline void Rust<Profile>::free()
{
if (m_ptr) {
profile_free(m_ptr);
m_ptr = nullptr;
}
}
template<>
inline void Rust<RustError>::free()
{

View File

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

View File

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

107
src/core/src/profile/mod.rs Normal file
View File

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

View File

@ -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 <QComboBox>
@ -17,7 +18,7 @@
#include <QUrl>
#include <QVBoxLayout>
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<ProfileList *>(m_profiles->model());
emit saveClicked(profiles->get(index));
}
});
actions->addButton(save, QDialogButtonBox::ApplyRole);
// Start button.

View File

@ -1,9 +1,12 @@
#pragma once
#include "core.h"
#include <QWidget>
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);

View File

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

View File

@ -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<Profile> p;
Rust<char> id;
p = profile_new("Default");
id = profile_id(p);
// Save.
auto path = joinPath(root, id.get());
Rust<RustError> 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<RustError> error;
Rust<Profile> 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<char> id;
id = profile_id(p);
// Save.
auto root = profiles();
auto path = joinPath(root, id.get());
Rust<RustError> 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()

View File

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

58
src/profile_models.cpp Normal file
View File

@ -0,0 +1,58 @@
#include "profile_models.hpp"
ProfileList::ProfileList(QObject *parent) :
QAbstractListModel(parent)
{
}
ProfileList::~ProfileList()
{
}
void ProfileList::add(Rust<Profile> &&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<Profile> &a, const Rust<Profile> &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();
}

21
src/profile_models.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include "core.hpp"
#include <QAbstractListModel>
#include <vector>
class ProfileList final : public QAbstractListModel {
public:
ProfileList(QObject *parent = nullptr);
~ProfileList() override;
void add(Rust<Profile> &&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<Rust<Profile>> m_items;
};