Initial commit

This commit is contained in:
nmzik
2026-07-16 10:21:41 +02:00
commit 147188b143
427 changed files with 205923 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
file(GLOB launcher_src
"include/*.h"
"src/*.cpp"
)
file(GLOB launcher_forms
"forms/*.ui"
)
add_executable(launcher WIN32
${launcher_src}
${launcher_forms}
)
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets)
if(WIN32)
target_link_libraries(launcher setupapi)
endif()
if (CLANG AND NOT KYTY_CLANG_CL)
target_link_libraries(launcher pthread)
endif()
target_include_directories(launcher PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/src")
set_target_properties(launcher PROPERTIES AUTOUIC ON AUTOMOC ON AUTORCC ON)
if(WIN32 AND NOT KYTY_CLANG_CL)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher KytyGitVersion)
add_dependencies(launcher kyty_emulator)
if(KYTY_CLANG_CL)
add_custom_command(TARGET launcher POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_PDB_FILE:kyty_emulator> $<TARGET_FILE_DIR:launcher>/$<TARGET_PDB_FILE_NAME:kyty_emulator>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE_DIR:kyty_emulator>/${KYTY_EMULATOR_MAP_NAME} $<TARGET_FILE_DIR:launcher>/${KYTY_EMULATOR_MAP_NAME}
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${KYTY_THIRD_PARTY_DIR}/winpthread/bin/libwinpthread-1.dll" $<TARGET_FILE_DIR:launcher>/libwinpthread-1.dll
)
endif()
if(WIN32)
find_program(QT_WINDEPLOYQT NAMES windeployqt PATHS "${Qt6_DIR}/../../../bin")
if(NOT QT_WINDEPLOYQT)
message(FATAL_ERROR "Could not find windeployqt")
endif()
add_custom_command(TARGET launcher POST_BUILD
COMMAND ${QT_WINDEPLOYQT} $<TARGET_FILE:launcher>
--no-compiler-runtime
--no-system-d3d-compiler
--no-system-dxc-compiler
--no-opengl-sw
--no-translations
COMMENT "Deploying Qt runtime and TLS plugins"
VERBATIM
)
endif()
set(launcher_name "launcher")
set_target_properties(launcher PROPERTIES OUTPUT_NAME ${launcher_name})
install(TARGETS launcher DESTINATION .)
if(WIN32)
install(CODE "
execute_process(
COMMAND ${QT_WINDEPLOYQT} \"\${CMAKE_INSTALL_PREFIX}/${launcher_name}.exe\"
--no-compiler-runtime
--no-system-d3d-compiler
--no-system-dxc-compiler
--no-opengl-sw
--no-translations
)
")
endif()
set(tidy_dirs "${CMAKE_SOURCE_DIR}/launcher/include")
set(iwyu_maps "${CMAKE_SOURCE_DIR}/launcher/utils/qt6_16.imp")
get_property(inc_headers TARGET launcher PROPERTY INCLUDE_DIRECTORIES)
list(APPEND inc_headers
${CMAKE_SOURCE_DIR}/launcher
${CMAKE_BINARY_DIR}/launcher/launcher_autogen/include
${Qt6Widgets_INCLUDE_DIRS}
${Qt6Core_INCLUDE_DIRS}
${Qt6Gui_INCLUDE_DIRS}
#${Qt6_DIR}/../../../include
#${Qt6_DIR}/../../../include/QtWidgets
#${Qt6_DIR}/../../../include/QtCore
#${Qt6_DIR}/../../../include/QtGui
)
include_what_you_use_with_mappings(launcher "${inc_headers}" "${iwyu_maps}")
clang_tidy_check(launcher "" "${tidy_dirs}" "${inc_headers}")
@@ -0,0 +1,338 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigurationEditDialog</class>
<widget class="QDialog" name="ConfigurationEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>560</width>
<height>420</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QFrame" name="configuaration_groupbox">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<layout class="QFormLayout" name="formLayout_6">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0" colspan="2">
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QCheckBox" name="checkBox_vulkan_validation">
<property name="toolTip">
<string>Enable Vulkan validation layers</string>
</property>
<property name="text">
<string>Vulkan validation</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="checkBox_shader_validation">
<property name="toolTip">
<string>Validate SPIR-V binary</string>
</property>
<property name="text">
<string>Shader validation</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="checkBox_cmd_dump">
<property name="toolTip">
<string>Dump command buffers</string>
</property>
<property name="text">
<string>Command buffer dump</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
<property name="toolTip">
<string>Enable RenderDoc capture</string>
</property>
<property name="text">
<string>RenderDoc capture</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_ngg_rectlist_draw">
<property name="toolTip">
<string>Use the NGG 4-vertex path for rect-list DrawIndexAuto primitive 7</string>
</property>
<property name="text">
<string>Use NGG rect-list draw</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Screen resolution:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip">
<string>Window resolution</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text">
<string>Vblank frequency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip">
<string>Virtual display refresh rate used for frame pacing</string>
</property>
<property name="minimum">
<number>30</number>
</property>
<property name="maximum">
<number>360</number>
</property>
<property name="value">
<number>60</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Shader optimization type:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip">
<string>Optimize shaders for code size or performance</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Shader log direction:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip">
<string>Dump shaders to file or console window. If enabled may decrease emulator performance</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Shader log folder:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip">
<string>Specify directory to dump shaders</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Command buffer dump folder:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip">
<string>Specify directory to dump command buffers</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Printf direction:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip">
<string>Print logs to file or console window. If enabled may decrease emulator performance</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Printf output file:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip">
<string>Specify file to dump logs</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Profiler direction:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QComboBox" name="comboBox_profiler_direction">
<property name="toolTip">
<string>Enable or disable profiler. If enabled may decrease emulator performance</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="ok_button">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Save</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel_button">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clear_button">
<property name="minimumSize">
<size>
<width>80</width>
<height>25</height>
</size>
</property>
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MandatoryLineEdit</class>
<extends>QLineEdit</extends>
<header>mandatoryLineEdit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>cancel_button</sender>
<signal>clicked()</signal>
<receiver>ConfigurationEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>496</x>
<y>65</y>
</hint>
<hint type="destinationlabel">
<x>280</x>
<y>180</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigurationListWidget</class>
<widget class="QWidget" name="ConfigurationListWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>292</width>
<height>268</height>
</rect>
</property>
<property name="windowTitle">
<string>Configurations</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>4</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>4</number>
</property>
<item>
<widget class="QToolButton" name="global_settings_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit global settings&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="edit_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edit configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="delete_button">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Delete configuration&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string/>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLineEdit" name="search_line_edit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>220</width>
<height>30</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>320</width>
<height>30</height>
</size>
</property>
<property name="toolTip">
<string>Search game name or serial</string>
</property>
<property name="placeholderText">
<string>Search name or serial</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="GameListTreeWidget" name="cfgs_list">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Game</string>
</property>
</column>
<column>
<property name="text">
<string>Serial</string>
</property>
</column>
<column>
<property name="text">
<string>Firmware Version</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Status</string>
</property>
</column>
<column>
<property name="text">
<string>Comments</string>
</property>
</column>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GameListTreeWidget</class>
<extends>QTreeWidget</extends>
<header>gameListTreeWidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainDialog</class>
<widget class="QDialog" name="MainDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>428</width>
<height>454</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Kyty Launcher</string>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="ConfigurationListWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_settings_file">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_Interpreter">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_Version">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>ConfigurationListWidget</class>
<extends>QWidget</extends>
<header>configurationListWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup_Console"/>
</buttongroups>
</ui>
+20
View File
@@ -0,0 +1,20 @@
#ifndef LAUNCHER_INCLUDE_COMMON_H_
#define LAUNCHER_INCLUDE_COMMON_H_
// IWYU pragma: begin_exports
#include <QArgument>
#include <QObject>
#include <cinttypes>
#include <cstdint>
#include <cstdio>
// IWYU pragma: end_exports
#define KYTY_QT_CLASS_NO_COPY(name) \
public: \
name(const name&) = delete; /* NOLINT(bugprone-macro-parentheses) */ \
name& operator=(const name&) = delete; /* NOLINT(bugprone-macro-parentheses) */ \
name(name&&) noexcept = delete; /* NOLINT(bugprone-macro-parentheses) */ \
name& operator=(name&&) noexcept = delete; /* NOLINT(bugprone-macro-parentheses) */
#endif /* LAUNCHER_INCLUDE_COMMON_H_ */
@@ -0,0 +1,40 @@
#ifndef COMPATIBILITY_DATABASE_H
#define COMPATIBILITY_DATABASE_H
#include "common.h"
#include "configuration.h"
#include <QMap>
#include <QObject>
#include <QString>
struct CompatibilityEntry {
Configuration::GameStatus status = Configuration::GameStatus::Unknown;
QString comment;
};
class CompatibilityDatabase: public QObject {
Q_OBJECT
KYTY_QT_CLASS_NO_COPY(CompatibilityDatabase);
public:
explicit CompatibilityDatabase(bool local, QObject* parent = nullptr);
[[nodiscard]] bool IsLocal() const { return m_local; }
[[nodiscard]] const CompatibilityEntry* Find(const QString& title_id) const;
void Load();
void SetStatus(const QString& title_id, Configuration::GameStatus status);
void SetComment(const QString& title_id, const QString& comment);
signals:
void Updated();
private:
void Save() const;
QMap<QString, CompatibilityEntry> m_entries;
bool m_local = false;
};
#endif // COMPATIBILITY_DATABASE_H
+178
View File
@@ -0,0 +1,178 @@
#ifndef LAUNCHER_INCLUDE_CONFIGURATION_H_
#define LAUNCHER_INCLUDE_CONFIGURATION_H_
#include "common.h"
#include <QByteArray>
#include <QChar>
#include <QMetaEnum>
#include <QMetaType>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QVariant>
#define KYTY_CFG_SET(n) s->setValue(#n, QVariant::fromValue(n).toString());
#define KYTY_CFG_GET(n) n = s->value(#n).value<decltype(n)>();
template <class T>
inline QStringList EnumToList() {
QStringList ret;
auto me = QMetaEnum::fromType<T>();
int count = me.keyCount();
for (int i = 0; i < count; i++) {
auto key = QString(me.key(i));
ret << (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit()
? key.remove('R').toLower()
: key);
}
return ret;
}
template <class T>
T TextToEnum(const QString& text) {
auto me = QMetaEnum::fromType<T>();
return static_cast<T>(me.keyToValue(
((text.size() > 1 && text.at(0).isDigit()) ? 'R' + text.toUpper() : text).toUtf8().data()));
}
template <class T>
QString EnumToText(T value) {
auto me = QMetaEnum::fromType<T>();
auto key = QString(me.valueToKey(static_cast<int>(value)));
return (key.startsWith('R') && key.size() > 2 && key.at(1).isDigit() ? key.remove('R').toLower()
: key);
}
class Configuration: public QObject {
Q_OBJECT
public:
enum class Resolution {
R1280X720,
R1920X1080,
};
Q_ENUM(Resolution)
enum class ShaderOptimizationType { None, Size, Performance };
Q_ENUM(ShaderOptimizationType)
enum class ShaderLogDirection { Silent, Console, File };
Q_ENUM(ShaderLogDirection)
enum class ProfilerDirection { None, Network };
Q_ENUM(ProfilerDirection)
enum class LogDirection { Silent, Console, File };
Q_ENUM(LogDirection)
enum class GameStatus { Unknown, InGame, Logo, DoesntBoot, MainMenu };
Q_ENUM(GameStatus)
Configuration() = default;
QString name;
QString title_id; /* Serial / title id from sce_sys/param.json */
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
QString basedir; /* Game base directory */
QString game_path; /* Launcher-unique game path */
bool custom_settings = false;
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
Resolution screen_resolution = Resolution::R1280X720;
int vblank_frequency = 60;
bool vulkan_validation_enabled = true;
bool shader_validation_enabled = true;
ShaderOptimizationType shader_optimization_type = ShaderOptimizationType::Performance;
ShaderLogDirection shader_log_direction = ShaderLogDirection::Silent;
QString shader_log_folder = "_Shaders";
bool command_buffer_dump_enabled = false;
QString command_buffer_dump_folder = "_Buffers";
LogDirection printf_direction = LogDirection::Silent;
QString printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool renderdoc_enabled = false;
bool ngg_rectlist_draw_enabled = true;
QString elf = QStringLiteral("eboot.bin");
void CopyEmulatorSettingsFrom(const Configuration& other) {
screen_resolution = other.screen_resolution;
vblank_frequency = other.vblank_frequency;
vulkan_validation_enabled = other.vulkan_validation_enabled;
shader_validation_enabled = other.shader_validation_enabled;
shader_optimization_type = other.shader_optimization_type;
shader_log_direction = other.shader_log_direction;
shader_log_folder = other.shader_log_folder;
command_buffer_dump_enabled = other.command_buffer_dump_enabled;
command_buffer_dump_folder = other.command_buffer_dump_folder;
printf_direction = other.printf_direction;
printf_output_file = other.printf_output_file;
profiler_direction = other.profiler_direction;
renderdoc_enabled = other.renderdoc_enabled;
ngg_rectlist_draw_enabled = other.ngg_rectlist_draw_enabled;
}
void CopyFrom(const Configuration& other) {
name = other.name;
title_id = other.title_id;
firmwareVer = other.firmwareVer;
basedir = other.basedir;
game_path = other.game_path;
custom_settings = other.custom_settings;
game_status = other.game_status;
game_comment = other.game_comment;
CopyEmulatorSettingsFrom(other);
elf = other.elf;
}
void WriteSettings(QSettings* s) const {
KYTY_CFG_SET(name);
KYTY_CFG_SET(basedir);
KYTY_CFG_SET(game_path);
KYTY_CFG_SET(custom_settings);
KYTY_CFG_SET(screen_resolution);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(vulkan_validation_enabled);
KYTY_CFG_SET(shader_validation_enabled);
KYTY_CFG_SET(shader_optimization_type);
KYTY_CFG_SET(shader_log_direction);
KYTY_CFG_SET(shader_log_folder);
KYTY_CFG_SET(command_buffer_dump_enabled);
KYTY_CFG_SET(command_buffer_dump_folder);
KYTY_CFG_SET(printf_direction);
KYTY_CFG_SET(printf_output_file);
KYTY_CFG_SET(profiler_direction);
KYTY_CFG_SET(renderdoc_enabled);
KYTY_CFG_SET(ngg_rectlist_draw_enabled);
KYTY_CFG_SET(elf);
}
void ReadSettings(QSettings* s) {
KYTY_CFG_GET(name);
KYTY_CFG_GET(basedir);
KYTY_CFG_GET(game_path);
KYTY_CFG_GET(custom_settings);
KYTY_CFG_GET(screen_resolution);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
KYTY_CFG_GET(vulkan_validation_enabled);
KYTY_CFG_GET(shader_validation_enabled);
KYTY_CFG_GET(shader_optimization_type);
KYTY_CFG_GET(shader_log_direction);
KYTY_CFG_GET(shader_log_folder);
KYTY_CFG_GET(command_buffer_dump_enabled);
KYTY_CFG_GET(command_buffer_dump_folder);
KYTY_CFG_GET(printf_direction);
KYTY_CFG_GET(printf_output_file);
KYTY_CFG_GET(profiler_direction);
KYTY_CFG_GET(renderdoc_enabled);
ngg_rectlist_draw_enabled =
s->value("ngg_rectlist_draw_enabled", ngg_rectlist_draw_enabled).toBool();
elf = s->value("elf", elf).toString();
}
};
Q_DECLARE_METATYPE(Configuration*)
#endif /* LAUNCHER_INCLUDE_CONFIGURATION_H_ */
@@ -0,0 +1,65 @@
#ifndef CONFIGURATION_EDIT_DIALOG_H
#define CONFIGURATION_EDIT_DIALOG_H
#include "common.h"
#include <QDialog>
#include <QString>
#include <QStringList>
class QByteArray;
class QGroupBox;
class QListWidget;
class QMoveEvent;
class QToolButton;
class QSettings;
class QWidget;
class Configuration;
namespace Ui {
class ConfigurationEditDialog;
} // namespace Ui
class ConfigurationEditDialog: public QDialog {
Q_OBJECT
KYTY_QT_CLASS_NO_COPY(ConfigurationEditDialog);
public:
explicit ConfigurationEditDialog(Configuration& info, QWidget* parent = nullptr);
~ConfigurationEditDialog() override;
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
void SetTitle(const QString& str);
void SetGameDirectories(const QStringList& dirs);
[[nodiscard]] QStringList GetGameDirectories() const;
private:
Ui::ConfigurationEditDialog* m_ui = nullptr;
Configuration& m_info;
QGroupBox* m_game_dirs_group = nullptr;
QListWidget* m_game_dirs_list = nullptr;
QToolButton* m_remove_game_dir_button = nullptr;
bool m_show_game_dirs = false;
protected:
void Init(const Configuration& info);
void InitGameDirectories();
void AddGameDirectoryItem(const QString& dir);
void moveEvent(QMoveEvent* event) override;
static QByteArray g_last_geometry;
/*slots:*/
void update_info();
void adjust_size();
void save();
void clear();
void add_game_directory();
void remove_selected_game_directories();
void update_game_directory_buttons();
};
#endif // CONFIGURATION_EDIT_DIALOG_H
+48
View File
@@ -0,0 +1,48 @@
#ifndef CONFIGURATION_ITEM_H
#define CONFIGURATION_ITEM_H
#include "common.h"
#include <QTreeWidgetItem>
#include <memory>
class QComboBox;
class QLabel;
class QLineEdit;
class QTreeWidget;
class QWidget;
class Configuration;
class ConfigurationItem: public QTreeWidgetItem {
public:
explicit ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent);
~ConfigurationItem() override;
void Update();
bool operator<(const QTreeWidgetItem& other) const override;
KYTY_QT_CLASS_NO_COPY(ConfigurationItem);
Configuration& GetInfo() { return *m_info; }
[[nodiscard]] const Configuration& GetInfo() const { return *m_info; }
// void SetInfo(Configuration* info);
void SetRunning(bool state);
void SetCompatibilityEditable(bool editable);
[[nodiscard]] bool IsRunning() const { return m_running; }
QComboBox* GetStatusCombo() { return m_status_combo; }
QLineEdit* GetCommentEdit() { return m_comment_edit; }
private:
void UpdateIcon();
void UpdateStatusIndicator();
std::unique_ptr<Configuration> m_info;
bool m_running = false;
QComboBox* m_status_combo = nullptr;
QLabel* m_status_indicator = nullptr;
QWidget* m_status_widget = nullptr;
QLineEdit* m_comment_edit = nullptr;
};
#endif // CONFIGURATION_ITEM_H
@@ -0,0 +1,86 @@
#ifndef CONFIGURATION_LIST_WIDGET_H
#define CONFIGURATION_LIST_WIDGET_H
#include "common.h"
#include "configuration.h"
#include <QMap>
#include <QString>
#include <QStringList>
#include <QWidget>
class ConfigurationItem;
class CompatibilityDatabase;
class QTreeWidgetItem;
class QPoint;
class MainDialog;
namespace Ui {
class ConfigurationListWidget;
} // namespace Ui
class ConfigurationListWidget: public QWidget {
Q_OBJECT
KYTY_QT_CLASS_NO_COPY(ConfigurationListWidget);
public:
explicit ConfigurationListWidget(QWidget* parent = nullptr);
~ConfigurationListWidget() override;
void SetRunEnabled(bool flag) { m_run_enabled = flag; }
[[nodiscard]] bool IsRunEnabled() const { return m_run_enabled; }
[[nodiscard]] const ConfigurationItem* GetSelectedItem() const { return m_selected_item; }
ConfigurationItem* GetSelectedItem() { return m_selected_item; }
void SetMainDialog(MainDialog* main_dialog) { m_main_dialog = main_dialog; }
MainDialog* GetMainDialog() { return m_main_dialog; }
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QStringList& GetGameDirectories() const { return m_game_dirs; }
[[nodiscard]] bool CanViewSelectedTrophies() const;
bool EnsureGameDirectory();
void ScanGameDirectory();
void ViewTrophies();
signals:
void Run();
void Select();
public slots:
void WriteSettings();
void ReadSettings();
protected slots:
void edit_configuration();
void delete_configuartion();
void edit_global_settings();
void run_configuration();
void list_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
void list_itemDoubleClicked(QTreeWidgetItem* witem, int column);
void show_context_menu(const QPoint& pos);
void open_game_folder();
void remove_save_data();
void filter_configurations(const QString& text);
private:
void ClearCustomSettings(ConfigurationItem* item);
void SelectItem(QTreeWidgetItem* witem);
void ApplyCompatibility();
[[nodiscard]] bool HasValidGameDirectory() const;
ConfigurationItem* m_selected_item = nullptr;
bool m_run_enabled = true;
Ui::ConfigurationListWidget* m_ui = nullptr;
MainDialog* m_main_dialog = nullptr;
QString m_settings_file;
QStringList m_game_dirs;
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
CompatibilityDatabase* m_compatibility = nullptr;
};
#endif // CONFIGURATION_LIST_WIDGET_H
+73
View File
@@ -0,0 +1,73 @@
#ifndef GAME_LIST_TREE_WIDGET_H
#define GAME_LIST_TREE_WIDGET_H
#include <QColor>
#include <QPaintEvent>
#include <QPainter>
#include <QPalette>
#include <QPixmap>
#include <QPoint>
#include <QResizeEvent>
#include <QString>
#include <QTreeWidget>
class GameListTreeWidget: public QTreeWidget {
public:
explicit GameListTreeWidget(QWidget* parent = nullptr): QTreeWidget(parent) {
setAutoFillBackground(false);
viewport()->setAutoFillBackground(false);
}
void SetBackgroundImage(const QString& path) {
if (m_path == path) {
return;
}
m_path = path;
m_source = QPixmap();
if (!path.isEmpty()) {
m_source.load(path);
}
UpdateScaledBackground();
viewport()->update();
}
protected:
void resizeEvent(QResizeEvent* event) override {
QTreeWidget::resizeEvent(event);
UpdateScaledBackground();
}
void paintEvent(QPaintEvent* event) override {
{
QPainter painter(viewport());
if (m_scaled.isNull()) {
painter.fillRect(event->rect(), palette().brush(QPalette::Base));
} else {
painter.drawPixmap(m_scaled_pos, m_scaled);
painter.fillRect(event->rect(), QColor(0, 0, 0, 115));
}
}
QTreeWidget::paintEvent(event);
}
private:
void UpdateScaledBackground() {
m_scaled = QPixmap();
if (m_source.isNull() || viewport()->size().isEmpty()) {
return;
}
m_scaled = m_source.scaled(viewport()->size(), Qt::KeepAspectRatioByExpanding,
Qt::FastTransformation);
m_scaled_pos = QPoint((viewport()->width() - m_scaled.width()) / 2,
(viewport()->height() - m_scaled.height()) / 2);
}
QString m_path;
QPixmap m_source;
QPixmap m_scaled;
QPoint m_scaled_pos;
};
#endif // GAME_LIST_TREE_WIDGET_H
+40
View File
@@ -0,0 +1,40 @@
#ifndef MAIN_DIALOG_H
#define MAIN_DIALOG_H
#include "common.h"
#include <QDialog>
#include <QString>
class QWidget;
class QProcess;
class MainDialogPrivate;
class QSettings;
class QResizeEvent;
class Configuration;
class MainDialog: public QDialog {
Q_OBJECT
KYTY_QT_CLASS_NO_COPY(MainDialog);
signals:
void Start();
void Quit();
void Resize();
public:
explicit MainDialog(QWidget* parent = nullptr);
~MainDialog() override = default;
void RunInterpreter(QProcess* process, const Configuration& info);
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
void resizeEvent(QResizeEvent* event) override;
private:
MainDialogPrivate* m_p = nullptr;
};
#endif // MAIN_DIALOG_H
+29
View File
@@ -0,0 +1,29 @@
#ifndef MANDATORY_LINEEDIT_H
#define MANDATORY_LINEEDIT_H
#include "common.h"
#include <QLineEdit>
#include <QString>
class QPaintEvent;
class QWidget;
constexpr char MANDATORY_AND_EMPTY[] = "mandatory_and_empty";
class MandatoryLineEdit: public QLineEdit {
Q_OBJECT
public:
explicit MandatoryLineEdit(QWidget* parent = nullptr);
void paintEvent(QPaintEvent* event) override;
static bool FindEmpty(QObject* p);
protected slots:
void changed(const QString& text);
};
#endif // MANDATORY_LINEEDIT_H
+27
View File
@@ -0,0 +1,27 @@
#ifndef TROPHY_VIEWER_DIALOG_H
#define TROPHY_VIEWER_DIALOG_H
#include "common.h"
#include <QDialog>
class QTabWidget;
class QWidget;
class Configuration;
class TrophyViewerDialog: public QDialog {
KYTY_QT_CLASS_NO_COPY(TrophyViewerDialog);
public:
explicit TrophyViewerDialog(QWidget* parent = nullptr);
static bool HasTrophyData(const Configuration* info);
static void ShowForGame(const Configuration* info, QWidget* parent);
private:
bool LoadGame(const Configuration& info, QString& error);
QTabWidget* m_tabs = nullptr;
};
#endif // TROPHY_VIEWER_DIALOG_H
+200
View File
@@ -0,0 +1,200 @@
#include "compatibilityDatabase.h"
#include <QDebug>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QFutureWatcher>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QThread>
#include <QTimer>
#include <QUrl>
#include <QtConcurrentRun>
#include <utility>
namespace {
constexpr char FILE_NAME[] = "compatibility_db.json";
constexpr char URL[] =
"https://github.com/Nmzik/KytyPS5/releases/download/compat-db/compatibility_db.json";
constexpr int RETRY_COUNT = 3;
constexpr int RETRY_DELAY_MS = 750;
struct LoadResult {
QMap<QString, CompatibilityEntry> entries;
QString error;
};
QString TitleKey(const QString& title_id) {
return title_id.trimmed().toUpper();
}
Configuration::GameStatus StatusFromText(const QString& text) {
const auto value = text.trimmed();
if (value == QStringLiteral("InGame") || value == QStringLiteral("In game")) {
return Configuration::GameStatus::InGame;
}
if (value == QStringLiteral("MainMenu") || value == QStringLiteral("Main menu")) {
return Configuration::GameStatus::MainMenu;
}
if (value == QStringLiteral("Logo")) {
return Configuration::GameStatus::Logo;
}
if (value == QStringLiteral("DoesntBoot") || value == QStringLiteral("Doesn't boot")) {
return Configuration::GameStatus::DoesntBoot;
}
return Configuration::GameStatus::Unknown;
}
QString StatusToText(Configuration::GameStatus status) {
switch (status) {
case Configuration::GameStatus::InGame: return QStringLiteral("InGame");
case Configuration::GameStatus::MainMenu: return QStringLiteral("MainMenu");
case Configuration::GameStatus::Logo: return QStringLiteral("Logo");
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("DoesntBoot");
case Configuration::GameStatus::Unknown: return QStringLiteral("Unknown");
}
return QStringLiteral("Unknown");
}
LoadResult Parse(const QByteArray& data) {
LoadResult ret;
QJsonParseError parse_error;
const auto doc = QJsonDocument::fromJson(data, &parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
ret.error = QStringLiteral("Invalid compatibility JSON: %1").arg(parse_error.errorString());
return ret;
}
const auto root = doc.object();
for (auto it = root.constBegin(); it != root.constEnd(); ++it) {
const auto title_id = TitleKey(it.key());
const auto object = it.value().toObject();
if (title_id.isEmpty() || object.isEmpty()) {
continue;
}
ret.entries.insert(title_id,
{StatusFromText(object.value(QStringLiteral("status")).toString()),
object.value(QStringLiteral("comment")).toString()});
}
return ret;
}
LoadResult DownloadOnce() {
QNetworkAccessManager manager;
QNetworkRequest request(QUrl(QString::fromLatin1(URL)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
auto* reply = manager.get(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(&timeout, &QTimer::timeout, reply, &QNetworkReply::abort);
timeout.start(15000);
loop.exec();
if (!timeout.isActive()) {
return {{}, QStringLiteral("Compatibility download timed out")};
}
if (reply->error() != QNetworkReply::NoError) {
return {{}, reply->errorString()};
}
return Parse(reply->readAll());
}
LoadResult Download() {
auto result = DownloadOnce();
for (int retry = 1; retry <= RETRY_COUNT && !result.error.isEmpty(); retry++) {
QThread::msleep(static_cast<unsigned long>(RETRY_DELAY_MS * retry));
result = DownloadOnce();
}
return result;
}
} // namespace
CompatibilityDatabase::CompatibilityDatabase(bool local, QObject* parent)
: QObject(parent), m_local(local) {}
const CompatibilityEntry* CompatibilityDatabase::Find(const QString& title_id) const {
const auto entry = m_entries.constFind(TitleKey(title_id));
return entry != m_entries.constEnd() ? &entry.value() : nullptr;
}
void CompatibilityDatabase::Load() {
if (m_local) {
QFile file(QDir(".").absoluteFilePath(FILE_NAME));
if (!file.exists()) {
emit Updated();
return;
}
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Could not open compatibility database:" << file.errorString();
return;
}
auto result = Parse(file.readAll());
if (!result.error.isEmpty()) {
qWarning() << result.error;
return;
}
m_entries = std::move(result.entries);
emit Updated();
return;
}
auto* watcher = new QFutureWatcher<LoadResult>(this);
connect(watcher, &QFutureWatcher<LoadResult>::finished, this, [this, watcher]() {
const auto result = watcher->result();
watcher->deleteLater();
if (!result.error.isEmpty()) {
qWarning() << "Could not refresh compatibility database:" << result.error;
return;
}
m_entries = result.entries;
emit Updated();
});
watcher->setFuture(QtConcurrent::run(Download));
}
void CompatibilityDatabase::SetStatus(const QString& title_id, Configuration::GameStatus status) {
const auto key = TitleKey(title_id);
if (!m_local || key.isEmpty()) {
return;
}
m_entries[key].status = status;
Save();
}
void CompatibilityDatabase::SetComment(const QString& title_id, const QString& comment) {
const auto key = TitleKey(title_id);
if (!m_local || key.isEmpty()) {
return;
}
m_entries[key].comment = comment;
Save();
}
void CompatibilityDatabase::Save() const {
QJsonObject root;
for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) {
root.insert(it.key(),
QJsonObject {{QStringLiteral("status"), StatusToText(it.value().status)},
{QStringLiteral("comment"), it.value().comment}});
}
QSaveFile file(QDir(".").absoluteFilePath(FILE_NAME));
const auto data = QJsonDocument(root).toJson(QJsonDocument::Indented);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) {
qWarning() << "Could not save compatibility database:" << file.errorString();
}
}
@@ -0,0 +1,331 @@
#include "configurationEditDialog.h"
#include "configuration.h"
#include "mandatoryLineEdit.h"
#include <QAbstractItemView>
#include <QCheckBox>
#include <QComboBox>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLayout>
#include <QListView>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QSettings>
#include <QSpinBox>
#include <QStyle>
#include <QToolButton>
#include <QTreeView>
#include <QVBoxLayout>
#include <QtAlgorithms>
#include "ui_configuration_edit_dialog.h"
constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog";
constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry";
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560;
static QString NormalizeGameDirectory(const QString& dir) {
const auto trimmed = dir.trimmed();
if (trimmed.isEmpty()) {
return {};
}
return QDir::cleanPath(QDir(trimmed).absolutePath());
}
static QString GameDirectoryKey(const QString& dir) {
const auto normalized = NormalizeGameDirectory(dir);
if (normalized.isEmpty()) {
return {};
}
auto canonical = QFileInfo(normalized).canonicalFilePath();
if (canonical.isEmpty()) {
canonical = normalized;
}
canonical = QDir::cleanPath(canonical);
#ifdef __linux__
return canonical;
#else
return canonical.toCaseFolded();
#endif
}
ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* parent)
: QDialog(parent, Qt::WindowCloseButtonHint), m_ui(new Ui::ConfigurationEditDialog),
m_info(info) {
m_ui->setupUi(this);
InitGameDirectories();
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
connect(m_ui->clear_button, &QPushButton::clicked, this, &ConfigurationEditDialog::clear);
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
[this](const QString& text) {
auto log = TextToEnum<Configuration::ShaderLogDirection>(text);
m_ui->lineEdit_shader_log_folder->setEnabled(
log == Configuration::ShaderLogDirection::File);
});
connect(m_ui->checkBox_cmd_dump, &QCheckBox::toggled, this,
[this](bool flag) { m_ui->lineEdit_cmd_dump_folder->setEnabled(flag); });
connect(m_ui->comboBox_printf_direction, &QComboBox::currentTextChanged, this,
[this](const QString& text) {
auto log = TextToEnum<Configuration::LogDirection>(text);
m_ui->lineEdit_printf_file->setEnabled(log == Configuration::LogDirection::File);
});
layout()->setSizeConstraint(QLayout::SetFixedSize);
restoreGeometry(g_last_geometry);
Init(info);
}
QByteArray ConfigurationEditDialog::g_last_geometry;
ConfigurationEditDialog::~ConfigurationEditDialog() {
delete m_ui;
}
void ConfigurationEditDialog::WriteSettings(QSettings& s) {
s.beginGroup(SETTINGS_CFG_DIALOG);
if (!g_last_geometry.isEmpty()) {
s.setValue(SETTINGS_CFG_LAST_GEOMETRY, g_last_geometry);
}
s.endGroup();
}
void ConfigurationEditDialog::ReadSettings(QSettings& s) {
s.beginGroup(SETTINGS_CFG_DIALOG);
g_last_geometry = s.value(SETTINGS_CFG_LAST_GEOMETRY, g_last_geometry).toByteArray();
s.endGroup();
}
template <class T>
static void ListInit(QComboBox* combo, T value) {
combo->clear();
combo->addItems(EnumToList<T>());
combo->setCurrentText(EnumToText(value));
}
void ConfigurationEditDialog::Init(const Configuration& info) {
ListInit(m_ui->comboBox_screen_resolution, info.screen_resolution);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->checkBox_shader_validation->setChecked(info.shader_validation_enabled);
m_ui->checkBox_vulkan_validation->setChecked(info.vulkan_validation_enabled);
m_ui->checkBox_renderdoc_capture->setChecked(info.renderdoc_enabled);
m_ui->checkBox_ngg_rectlist_draw->setChecked(info.ngg_rectlist_draw_enabled);
ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type);
ListInit(m_ui->comboBox_shader_log_direction, info.shader_log_direction);
m_ui->lineEdit_shader_log_folder->setText(info.shader_log_folder);
m_ui->lineEdit_shader_log_folder->setEnabled(info.shader_log_direction ==
Configuration::ShaderLogDirection::File);
m_ui->checkBox_cmd_dump->setChecked(info.command_buffer_dump_enabled);
m_ui->lineEdit_cmd_dump_folder->setText(info.command_buffer_dump_folder);
m_ui->lineEdit_cmd_dump_folder->setEnabled(info.command_buffer_dump_enabled);
ListInit(m_ui->comboBox_printf_direction, info.printf_direction);
m_ui->lineEdit_printf_file->setText(info.printf_output_file);
m_ui->lineEdit_printf_file->setEnabled(info.printf_direction ==
Configuration::LogDirection::File);
ListInit(m_ui->comboBox_profiler_direction, info.profiler_direction);
}
void ConfigurationEditDialog::InitGameDirectories() {
m_game_dirs_group = new QGroupBox(tr("Game folders"), this);
auto* group_layout = new QVBoxLayout(m_game_dirs_group);
group_layout->setContentsMargins(8, 8, 8, 8);
group_layout->setSpacing(6);
m_game_dirs_list = new QListWidget(m_game_dirs_group);
m_game_dirs_list->setMinimumHeight(120);
m_game_dirs_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
group_layout->addWidget(m_game_dirs_list);
auto* button_layout = new QHBoxLayout;
button_layout->setContentsMargins(0, 0, 0, 0);
button_layout->setSpacing(4);
auto* add_button = new QToolButton(m_game_dirs_group);
add_button->setIcon(style()->standardIcon(QStyle::SP_DirOpenIcon));
add_button->setToolTip(tr("Add game folder"));
add_button->setAutoRaise(true);
button_layout->addWidget(add_button);
m_remove_game_dir_button = new QToolButton(m_game_dirs_group);
m_remove_game_dir_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
m_remove_game_dir_button->setToolTip(tr("Remove selected game folders"));
m_remove_game_dir_button->setAutoRaise(true);
button_layout->addWidget(m_remove_game_dir_button);
button_layout->addStretch(1);
group_layout->addLayout(button_layout);
m_ui->gridLayout->addWidget(m_game_dirs_group, 1, 0, 1, 1);
m_game_dirs_group->setVisible(false);
connect(add_button, &QToolButton::clicked, this, &ConfigurationEditDialog::add_game_directory);
connect(m_remove_game_dir_button, &QToolButton::clicked, this,
&ConfigurationEditDialog::remove_selected_game_directories);
connect(m_game_dirs_list, &QListWidget::itemSelectionChanged, this,
&ConfigurationEditDialog::update_game_directory_buttons);
update_game_directory_buttons();
}
void ConfigurationEditDialog::SetTitle(const QString& str) {
setWindowTitle(str);
}
void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
m_show_game_dirs = true;
m_game_dirs_list->clear();
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
for (const auto& dir: dirs) {
AddGameDirectoryItem(dir);
}
m_game_dirs_group->setVisible(true);
update_game_directory_buttons();
adjustSize();
}
QStringList ConfigurationEditDialog::GetGameDirectories() const {
QStringList dirs;
if (!m_show_game_dirs) {
return dirs;
}
for (int index = 0; index < m_game_dirs_list->count(); index++) {
auto* item = m_game_dirs_list->item(index);
dirs.append(item->text());
}
return dirs;
}
void ConfigurationEditDialog::AddGameDirectoryItem(const QString& dir) {
const auto normalized = NormalizeGameDirectory(dir);
const auto key = GameDirectoryKey(normalized);
if (normalized.isEmpty() || key.isEmpty()) {
return;
}
for (int index = 0; index < m_game_dirs_list->count(); index++) {
auto* item = m_game_dirs_list->item(index);
if (GameDirectoryKey(item->text()) == key) {
return;
}
}
auto* item = new QListWidgetItem(normalized, m_game_dirs_list);
item->setToolTip(normalized);
}
void ConfigurationEditDialog::moveEvent(QMoveEvent* event) {
QDialog::moveEvent(event);
g_last_geometry = saveGeometry();
}
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked();
info.shader_validation_enabled = ui.checkBox_shader_validation->isChecked();
info.renderdoc_enabled = ui.checkBox_renderdoc_capture->isChecked();
info.ngg_rectlist_draw_enabled = ui.checkBox_ngg_rectlist_draw->isChecked();
info.shader_optimization_type = TextToEnum<Configuration::ShaderOptimizationType>(
ui.comboBox_shader_optimization_type->currentText());
info.shader_log_direction = TextToEnum<Configuration::ShaderLogDirection>(
ui.comboBox_shader_log_direction->currentText());
info.shader_log_folder = ui.lineEdit_shader_log_folder->text();
info.command_buffer_dump_enabled = ui.checkBox_cmd_dump->isChecked();
info.command_buffer_dump_folder = ui.lineEdit_cmd_dump_folder->text();
info.printf_direction =
TextToEnum<Configuration::LogDirection>(ui.comboBox_printf_direction->currentText());
info.printf_output_file = ui.lineEdit_printf_file->text();
info.profiler_direction =
TextToEnum<Configuration::ProfilerDirection>(ui.comboBox_profiler_direction->currentText());
}
void ConfigurationEditDialog::update_info() {
UpdateInfo(m_info, *m_ui);
}
void ConfigurationEditDialog::adjust_size() {
this->adjustSize();
}
void ConfigurationEditDialog::save() {
if (MandatoryLineEdit::FindEmpty(this)) {
QMessageBox::critical(this, tr("Save failed"), tr("Please fill all mandatory fields"));
return;
}
update_info();
emit accept();
}
void ConfigurationEditDialog::clear() {
Configuration default_info;
Init(default_info);
if (m_show_game_dirs) {
m_game_dirs_list->clear();
update_game_directory_buttons();
}
}
void ConfigurationEditDialog::add_game_directory() {
QString start_dir = QDir::homePath();
if (m_game_dirs_list->count() > 0) {
start_dir = m_game_dirs_list->item(m_game_dirs_list->count() - 1)->text();
}
QFileDialog dialog(this, tr("Select game folders"), start_dir);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly, true);
dialog.setOption(QFileDialog::DontUseNativeDialog, true);
dialog.setLabelText(QFileDialog::Accept, tr("Add"));
auto* list_view = dialog.findChild<QListView*>(QStringLiteral("listView"));
if (list_view != nullptr) {
list_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
auto* tree_view = dialog.findChild<QTreeView*>(QStringLiteral("treeView"));
if (tree_view != nullptr) {
tree_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
if (dialog.exec() != QDialog::Accepted) {
return;
}
for (const auto& dir: dialog.selectedFiles()) {
AddGameDirectoryItem(dir);
}
update_game_directory_buttons();
}
void ConfigurationEditDialog::remove_selected_game_directories() {
qDeleteAll(m_game_dirs_list->selectedItems());
update_game_directory_buttons();
}
void ConfigurationEditDialog::update_game_directory_buttons() {
m_remove_game_dir_button->setEnabled(!m_game_dirs_list->selectedItems().isEmpty());
}
+257
View File
@@ -0,0 +1,257 @@
#include "configurationItem.h"
#include "configuration.h"
#include <QApplication>
#include <QComboBox>
#include <QDir>
#include <QFileInfo>
#include <QFont>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QSize>
#include <QStringList>
#include <QStyle>
#include <QTreeWidget>
#include <QVersionNumber>
#include <QWheelEvent>
#include <QWidget>
namespace {
enum Column {
NameColumn,
SerialColumn,
FirmwareVersionColumn,
PathColumn,
StatusColumn,
CommentsColumn,
};
class NoWheelComboBox: public QComboBox {
public:
explicit NoWheelComboBox(QWidget* parent = nullptr): QComboBox(parent) {}
protected:
void wheelEvent(QWheelEvent* event) override { event->ignore(); }
};
QString GetStatusText(Configuration::GameStatus status) {
switch (status) {
case Configuration::GameStatus::Unknown: return QStringLiteral("Unknown");
case Configuration::GameStatus::MainMenu: return QStringLiteral("Main menu");
case Configuration::GameStatus::InGame: return QStringLiteral("In game");
case Configuration::GameStatus::Logo: return QStringLiteral("Logo");
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("Doesn't boot");
}
return QStringLiteral("Unknown");
}
QString GetStatusColor(Configuration::GameStatus status) {
switch (status) {
case Configuration::GameStatus::InGame: return QStringLiteral("#2fb344");
case Configuration::GameStatus::MainMenu: return QStringLiteral("#2f80ed");
case Configuration::GameStatus::Logo: return QStringLiteral("#f2c94c");
case Configuration::GameStatus::DoesntBoot: return QStringLiteral("#e55353");
case Configuration::GameStatus::Unknown: return QStringLiteral("#8a8a8a");
}
return QStringLiteral("#8a8a8a");
}
void AddStatus(QComboBox* combo, Configuration::GameStatus status) {
combo->addItem(GetStatusText(status), static_cast<int>(status));
}
void SetStatus(QComboBox* combo, Configuration::GameStatus status) {
const int index = combo->findData(static_cast<int>(status));
combo->setCurrentIndex(index >= 0 ? index : 0);
}
QIcon StandardIcon(QStyle::StandardPixmap icon) {
return QApplication::style()->standardIcon(icon);
}
QString GetPathText(const Configuration& info) {
return !info.game_path.isEmpty() ? info.game_path : info.basedir;
}
QString GetDisplayText(const Configuration& info) {
QStringList lines({info.name});
if (!info.title_id.isEmpty()) {
lines.append(QStringLiteral("Serial ID: %1").arg(info.title_id));
}
if (!info.firmwareVer.isEmpty()) {
lines.append(QStringLiteral("Firmware version: %1").arg(info.firmwareVer));
}
const auto path = GetPathText(info);
if (!path.isEmpty()) {
lines.append(QStringLiteral("Path: %1").arg(path));
}
if (!info.game_comment.isEmpty()) {
lines.append(QStringLiteral("Comment: %1").arg(info.game_comment));
}
return lines.join(QLatin1Char('\n'));
}
QString GetSortText(const Configuration& info) {
return QStringLiteral("%1\n%2").arg(info.name.toCaseFolded(), info.game_path.toCaseFolded());
}
void MakeTransparent(QWidget* widget) {
widget->setAutoFillBackground(false);
widget->setAttribute(Qt::WA_NoSystemBackground);
widget->setAttribute(Qt::WA_TranslucentBackground);
}
} // namespace
ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent)
: QTreeWidgetItem(parent), m_info(std::move(info)) {
setSizeHint(NameColumn, QSize(0, 72));
m_status_widget = new QWidget(parent);
MakeTransparent(m_status_widget);
auto* layout = new QHBoxLayout(m_status_widget);
layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(6);
m_status_indicator = new QLabel(m_status_widget);
m_status_indicator->setFixedSize(QSize(12, 12));
layout->addWidget(m_status_indicator);
m_status_combo = new NoWheelComboBox(m_status_widget);
MakeTransparent(m_status_combo);
AddStatus(m_status_combo, Configuration::GameStatus::Unknown);
AddStatus(m_status_combo, Configuration::GameStatus::MainMenu);
AddStatus(m_status_combo, Configuration::GameStatus::InGame);
AddStatus(m_status_combo, Configuration::GameStatus::Logo);
AddStatus(m_status_combo, Configuration::GameStatus::DoesntBoot);
m_status_combo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_status_combo->setFixedWidth(125);
m_status_combo->setStyleSheet(QStringLiteral(
"QComboBox { background: transparent; border: 0; color: #eef4ff; padding-left: 2px; }"
"QComboBox:focus { background: rgba(255,255,255,35); border-radius: 3px; }"
"QComboBox::drop-down { border: 0; }"
"QComboBox QAbstractItemView { background: #181c24; color: #eef4ff; "
"selection-background-color: #267bd8; }"));
layout->addWidget(m_status_combo);
layout->addStretch(1);
parent->setItemWidget(this, StatusColumn, m_status_widget);
m_comment_edit = new QLineEdit(parent);
MakeTransparent(m_comment_edit);
m_comment_edit->setClearButtonEnabled(true);
m_comment_edit->setFrame(false);
m_comment_edit->setStyleSheet(QStringLiteral(
"QLineEdit { background: transparent; border: 0; color: #eef4ff; padding-left: 2px; "
"selection-background-color: #267bd8; }"
"QLineEdit:focus { background: rgba(255,255,255,35); "
"border: 1px solid rgba(255,255,255,90); border-radius: 3px; }"));
parent->setItemWidget(this, CommentsColumn, m_comment_edit);
Update();
SetRunning(false);
}
ConfigurationItem::~ConfigurationItem() = default;
void ConfigurationItem::Update() {
const auto display_text = GetDisplayText(*m_info);
const auto path = GetPathText(*m_info);
setText(NameColumn, m_info->name);
setText(SerialColumn, m_info->title_id);
setText(FirmwareVersionColumn,
m_info->firmwareVer.isEmpty() ? QStringLiteral("\u2014") : m_info->firmwareVer);
setText(PathColumn, path);
setText(StatusColumn, {});
setText(CommentsColumn, {});
for (int column = NameColumn; column <= CommentsColumn; column++) {
setToolTip(column, display_text);
}
SetStatus(m_status_combo, m_info->game_status);
if (m_comment_edit->text() != m_info->game_comment) {
m_comment_edit->setText(m_info->game_comment);
}
UpdateIcon();
UpdateStatusIndicator();
}
bool ConfigurationItem::operator<(const QTreeWidgetItem& other) const {
const auto* other_item = dynamic_cast<const ConfigurationItem*>(&other);
if (other_item == nullptr) {
return QTreeWidgetItem::operator<(other);
}
const int column = treeWidget() != nullptr ? treeWidget()->sortColumn() : NameColumn;
switch (column) {
case NameColumn: return GetSortText(*m_info) < GetSortText(*other_item->m_info);
case StatusColumn:
return GetStatusText(m_info->game_status) <
GetStatusText(other_item->m_info->game_status);
case FirmwareVersionColumn: {
const auto& version = m_info->firmwareVer;
const auto& other_version = other_item->m_info->firmwareVer;
if (version.isEmpty() || other_version.isEmpty()) {
return version.isEmpty() && !other_version.isEmpty();
}
return QVersionNumber::compare(QVersionNumber::fromString(version),
QVersionNumber::fromString(other_version)) < 0;
}
case CommentsColumn:
return m_info->game_comment.toCaseFolded() <
other_item->m_info->game_comment.toCaseFolded();
default: return text(column).toCaseFolded() < other.text(column).toCaseFolded();
}
}
void ConfigurationItem::SetRunning(bool state) {
m_running = state;
QFont f = font(NameColumn);
f.setBold(state);
for (int column = NameColumn; column <= CommentsColumn; column++) {
setFont(column, f);
}
UpdateIcon();
}
void ConfigurationItem::SetCompatibilityEditable(bool editable) {
m_status_combo->setEnabled(editable);
m_comment_edit->setReadOnly(!editable);
m_comment_edit->setClearButtonEnabled(editable);
}
void ConfigurationItem::UpdateIcon() {
const QString icon_file = QDir(m_info->basedir).filePath(QStringLiteral("sce_sys/icon0.png"));
if (QFileInfo::exists(icon_file)) {
setIcon(NameColumn, QIcon(icon_file));
return;
}
if (m_running) {
setIcon(NameColumn, StandardIcon(QStyle::SP_MediaPlay));
} else if (m_info->custom_settings) {
setIcon(NameColumn, StandardIcon(QStyle::SP_FileIcon));
} else {
setIcon(NameColumn, StandardIcon(QStyle::SP_ComputerIcon));
}
}
void ConfigurationItem::UpdateStatusIndicator() {
m_status_indicator->setStyleSheet(
QStringLiteral(
"background-color: %1; border: 1px solid rgba(255, 255, 255, 90); border-radius: 6px;")
.arg(GetStatusColor(m_info->game_status)));
m_status_indicator->setToolTip(GetStatusText(m_info->game_status));
}
@@ -0,0 +1,908 @@
#include "configurationListWidget.h"
#include "common.h"
#include "compatibilityDatabase.h"
#include "configuration.h"
#include "configurationEditDialog.h"
#include "configurationItem.h"
#include "gameListTreeWidget.h"
#include "mainDialog.h"
#include "trophyViewerDialog.h"
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QAction>
#include <QComboBox>
#include <QCoreApplication>
#include <QCursor>
#include <QDesktopServices>
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHeaderView>
#include <QIcon>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QRegularExpression>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
#include <QStyle>
#include <QToolButton>
#include <QTreeWidget>
#include <QUrl>
#include <QtCore>
#include "ui_configuration_list_widget.h"
#include <memory>
constexpr char CONF_FILE_NAME[] = "Kyty.ini";
constexpr char CONF_ORG_NAME[] = "Kyty";
constexpr char CONF_APP_NAME[] = "Kyty";
constexpr char CONF_SECTION_NAME[] = "GameConfigurations";
constexpr char CONF_LAUNCHER[] = "Launcher";
constexpr char CONF_GAME_DIR[] = "game_dir";
constexpr char CONF_GAME_DIRS[] = "game_dirs";
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
constexpr char SAVE_DATA_DIR[] = "_SaveData";
constexpr int GAME_NAME_COLUMN = 0;
constexpr int GAME_SERIAL_COLUMN = 1;
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 2;
constexpr int GAME_PATH_COLUMN = 3;
constexpr int GAME_STATUS_COLUMN = 4;
constexpr int GAME_COMMENT_COLUMN = 5;
static QString NormalizeGameDirectory(const QString& dir) {
const auto trimmed = dir.trimmed();
if (trimmed.isEmpty()) {
return {};
}
return QDir::cleanPath(QDir(trimmed).absolutePath());
}
static QString PathKey(const QString& path) {
auto normalized = NormalizeGameDirectory(path);
if (normalized.isEmpty()) {
normalized = QDir::cleanPath(path.trimmed());
}
if (normalized.isEmpty()) {
return {};
}
auto canonical = QFileInfo(normalized).canonicalFilePath();
if (canonical.isEmpty()) {
canonical = normalized;
}
canonical = QDir::cleanPath(canonical);
#ifdef __linux__
return canonical;
#else
return canonical.toCaseFolded();
#endif
}
static QStringList NormalizeGameDirectories(const QStringList& dirs) {
QStringList dirs_ret;
QSet<QString> seen;
for (const auto& dir: dirs) {
const auto normalized = NormalizeGameDirectory(dir);
const auto key = PathKey(normalized);
if (normalized.isEmpty() || key.isEmpty() || seen.contains(key)) {
continue;
}
seen.insert(key);
dirs_ret.append(normalized);
}
return dirs_ret;
}
static QStringList SettingsStringList(const QVariant& value) {
auto list = value.toStringList();
if (!list.isEmpty()) {
return list;
}
const auto text = value.toString();
return text.isEmpty() ? QStringList() : QStringList({text});
}
static QString GetPic0Path(const Configuration& info) {
if (info.basedir.isEmpty()) {
return {};
}
const auto path = QDir(info.basedir).filePath(QStringLiteral("sce_sys/pic0.png"));
return QFileInfo::exists(path) ? QFileInfo(path).absoluteFilePath() : QString();
}
static void ApplyGameListStyle(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->setAlternatingRowColors(false);
ui->cfgs_list->setAllColumnsShowFocus(true);
ui->cfgs_list->setIndentation(0);
ui->cfgs_list->setMouseTracking(false);
ui->cfgs_list->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->cfgs_list->setSelectionMode(QAbstractItemView::SingleSelection);
ui->cfgs_list->setTextElideMode(Qt::ElideMiddle);
ui->cfgs_list->header()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->cfgs_list->header()->setHighlightSections(false);
ui->cfgs_list->header()->setStretchLastSection(true);
ui->cfgs_list->setStyleSheet(QStringLiteral(
"QTreeWidget { background: transparent; border: 1px solid rgba(255,255,255,42); "
"color: #eef4ff; outline: 0; }"
"QTreeWidget::item { background: rgba(8,10,16,92); "
"border-bottom: 1px solid rgba(255,255,255,24); padding: 4px; }"
"QTreeWidget::item:selected { background: rgba(32,118,210,150); color: #ffffff; }"
"QHeaderView::section { background: rgba(22,25,31,235); color: #dce6f6; border: 0; "
"border-right: 1px solid rgba(255,255,255,35); border-bottom: 1px solid "
"rgba(255,255,255,45); "
"padding: 5px 6px; }"
"QScrollBar:vertical { background: rgba(12,14,18,150); width: 14px; margin: 0; }"
"QScrollBar::handle:vertical { background: rgba(220,230,245,120); min-height: 36px; }"
"QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }"));
ui->search_line_edit->setStyleSheet(QStringLiteral(
"QLineEdit { background: rgba(12,14,18,210); border: 1px solid rgba(255,255,255,42); "
"border-radius: 4px; color: #eef4ff; padding: 5px 8px; selection-background-color: "
"#267bd8; }"
"QLineEdit:focus { border-color: rgba(80,160,255,180); }"));
ui->global_settings_button->setStyleSheet(QStringLiteral(
"QToolButton { background: rgba(255,255,255,18); border: 1px solid transparent; "
"border-radius: 5px; padding: 3px; }"
"QToolButton:hover { background: rgba(255,255,255,45); border-color: rgba(255,255,255,70); "
"}"
"QToolButton:disabled { background: transparent; }"));
ui->edit_button->setStyleSheet(ui->global_settings_button->styleSheet());
ui->delete_button->setStyleSheet(ui->global_settings_button->styleSheet());
}
static void AddSaveDataDir(QStringList* dirs, QSet<QString>* seen, const QString& root,
const QString& title_id) {
const auto path = QDir(root).filePath(
QStringLiteral("%1/%2").arg(QString::fromLatin1(SAVE_DATA_DIR), title_id));
QDir dir(path);
if (!dir.exists()) {
return;
}
const auto absolute = dir.absolutePath();
auto canonical = QFileInfo(absolute).canonicalFilePath();
if (canonical.isEmpty()) {
canonical = absolute;
}
canonical = QDir::cleanPath(canonical);
if (!seen->contains(canonical)) {
seen->insert(canonical);
dirs->append(absolute);
}
}
static QStringList GetSaveDataDirs(const Configuration& info) {
QStringList dirs;
if (info.title_id.trimmed().isEmpty()) {
return dirs;
}
QSet<QString> seen;
QStringList roots({QDir::currentPath(), QCoreApplication::applicationDirPath()});
QDir current_parent(QDir::currentPath());
if (current_parent.cdUp()) {
roots.append(current_parent.absolutePath());
}
QDir app_parent(QCoreApplication::applicationDirPath());
if (app_parent.cdUp()) {
roots.append(app_parent.absolutePath());
}
for (const auto& root: roots) {
AddSaveDataDir(&dirs, &seen, root, info.title_id.trimmed());
}
return dirs;
}
ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
: QWidget(parent), m_ui(new Ui::ConfigurationListWidget) {
m_compatibility = new CompatibilityDatabase(
QCoreApplication::arguments().contains(QStringLiteral("--local")), this);
m_ui->setupUi(this);
ApplyGameListStyle(m_ui);
m_ui->global_settings_button->setIcon(style()->standardIcon(QStyle::SP_FileDialogDetailedView));
m_ui->global_settings_button->setToolTip(tr("Edit global settings and game folders"));
m_ui->edit_button->setIcon(style()->standardIcon(QStyle::SP_FileIcon));
m_ui->delete_button->setIcon(style()->standardIcon(QStyle::SP_DialogDiscardButton));
m_ui->delete_button->setEnabled(false);
m_ui->edit_button->setEnabled(false);
m_ui->cfgs_list->setContextMenuPolicy(Qt::CustomContextMenu);
m_ui->cfgs_list->setIconSize(QSize(48, 48));
m_ui->cfgs_list->setRootIsDecorated(false);
m_ui->cfgs_list->setUniformRowHeights(true);
m_ui->cfgs_list->setSortingEnabled(true);
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
m_ui->cfgs_list->setColumnWidth(GAME_SERIAL_COLUMN, 110);
m_ui->cfgs_list->setColumnWidth(GAME_FIRMWARE_VERSION_COLUMN, 150);
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
m_ui->cfgs_list->setColumnWidth(GAME_COMMENT_COLUMN, 240);
connect(m_ui->global_settings_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_global_settings);
connect(m_ui->edit_button, &QToolButton::clicked, this,
&ConfigurationListWidget::edit_configuration);
connect(m_ui->delete_button, &QToolButton::clicked, this,
&ConfigurationListWidget::delete_configuartion);
connect(m_ui->cfgs_list, &QTreeWidget::currentItemChanged, this,
&ConfigurationListWidget::list_currentItemChanged);
connect(m_ui->cfgs_list, &QTreeWidget::itemDoubleClicked, this,
&ConfigurationListWidget::list_itemDoubleClicked);
connect(m_ui->cfgs_list, &QTreeWidget::customContextMenuRequested, this,
&ConfigurationListWidget::show_context_menu);
connect(m_ui->search_line_edit, &QLineEdit::textChanged, this,
&ConfigurationListWidget::filter_configurations);
connect(m_compatibility, &CompatibilityDatabase::Updated, this,
&ConfigurationListWidget::ApplyCompatibility);
m_ui->cfgs_list->setDragDropMode(QAbstractItemView::NoDragDrop);
ReadSettings();
if (m_compatibility->IsLocal()) {
m_compatibility->Load();
}
ScanGameDirectory();
if (!m_compatibility->IsLocal()) {
m_compatibility->Load();
}
}
ConfigurationListWidget::~ConfigurationListWidget() {
qDeleteAll(m_custom_infos);
delete m_ui;
}
void ConfigurationListWidget::WriteSettings() {
QFile file = QFile(QDir(".").absoluteFilePath(CONF_FILE_NAME));
std::unique_ptr<QSettings> s;
if (file.exists()) {
s = std::make_unique<QSettings>(CONF_FILE_NAME, QSettings::IniFormat);
} else {
#ifdef __linux__
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::UserScope, CONF_ORG_NAME,
CONF_APP_NAME);
#else
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::SystemScope, CONF_ORG_NAME,
CONF_APP_NAME);
#endif
}
MainDialog::WriteSettings(*s);
ConfigurationEditDialog::WriteSettings(*s);
s->beginGroup(CONF_LAUNCHER);
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
s->setValue(CONF_GAME_DIRS, m_game_dirs);
s->remove(CONF_GAME_DIR);
s->endGroup();
s->remove(CONF_GLOBAL);
s->beginGroup(CONF_GLOBAL);
m_global_info.WriteSettings(s.get());
s->endGroup();
s->remove(CONF_SECTION_NAME);
s->beginWriteArray(CONF_SECTION_NAME);
int i = 0;
for (auto it = m_custom_infos.constBegin(); it != m_custom_infos.constEnd(); ++it) {
s->setArrayIndex(i++);
it.value()->WriteSettings(s.get());
}
s->endArray();
}
void ConfigurationListWidget::ReadSettings() {
QFile file = QFile(QDir(".").absoluteFilePath(CONF_FILE_NAME));
std::unique_ptr<QSettings> s;
if (file.exists()) {
s = std::make_unique<QSettings>(CONF_FILE_NAME, QSettings::IniFormat);
} else {
#ifdef __linux__
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::UserScope, CONF_ORG_NAME,
CONF_APP_NAME);
#else
s = std::make_unique<QSettings>(QSettings::IniFormat, QSettings::SystemScope, CONF_ORG_NAME,
CONF_APP_NAME);
#endif
}
m_settings_file = s->fileName();
MainDialog::ReadSettings(*s);
ConfigurationEditDialog::ReadSettings(*s);
s->beginGroup(CONF_LAUNCHER);
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIRS)));
if (m_game_dirs.isEmpty()) {
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
}
s->endGroup();
s->beginGroup(CONF_GLOBAL);
if (!s->childKeys().isEmpty()) {
m_global_info.ReadSettings(s.get());
}
s->endGroup();
qDeleteAll(m_custom_infos);
m_custom_infos.clear();
int size = s->beginReadArray(CONF_SECTION_NAME);
for (int i = 0; i < size; i++) {
s->setArrayIndex(i);
auto* info = new Configuration;
info->ReadSettings(s.get());
info->custom_settings = true;
if (!info->game_path.isEmpty()) {
m_custom_infos.insert(info->game_path, info);
} else {
delete info;
}
}
s->endArray();
}
void ConfigurationListWidget::ApplyCompatibility() {
const bool sorting_enabled = m_ui->cfgs_list->isSortingEnabled();
const int sort_column = m_ui->cfgs_list->sortColumn();
const auto sort_order = m_ui->cfgs_list->header()->sortIndicatorOrder();
m_ui->cfgs_list->setSortingEnabled(false);
for (int index = 0; index < m_ui->cfgs_list->topLevelItemCount(); index++) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->topLevelItem(index));
const auto& title_id = item->GetInfo().title_id;
const auto* entry = m_compatibility->Find(title_id);
item->GetInfo().game_status =
entry != nullptr ? entry->status : Configuration::GameStatus::Unknown;
item->GetInfo().game_comment = entry != nullptr ? entry->comment : QString();
const QSignalBlocker blocker(item->GetStatusCombo());
item->Update();
item->SetCompatibilityEditable(m_compatibility->IsLocal() && !title_id.trimmed().isEmpty());
}
m_ui->cfgs_list->setSortingEnabled(sorting_enabled);
if (sorting_enabled) {
m_ui->cfgs_list->sortItems(sort_column, sort_order);
}
}
static Configuration* CloneConfiguration(const Configuration& source) {
auto* ret = new Configuration;
ret->CopyFrom(source);
return ret;
}
struct GameMetadata {
QString title_name;
QString title_id;
QString firmwareVer;
};
static QString GetJsonString(const QJsonObject& obj, const QString& key) {
return obj.value(key).toString().trimmed();
}
static QString GetLocalizedTitleName(const QJsonObject& root) {
const auto localized = root.value(QStringLiteral("localizedParameters")).toObject();
if (localized.isEmpty()) {
return {};
}
const auto default_language = GetJsonString(localized, QStringLiteral("defaultLanguage"));
if (!default_language.isEmpty()) {
const auto title = GetJsonString(localized.value(default_language).toObject(),
QStringLiteral("titleName"));
if (!title.isEmpty()) {
return title;
}
}
const auto english_title = GetJsonString(localized.value(QStringLiteral("en-US")).toObject(),
QStringLiteral("titleName"));
if (!english_title.isEmpty()) {
return english_title;
}
for (auto it = localized.constBegin(); it != localized.constEnd(); ++it) {
const auto title = GetJsonString(it.value().toObject(), QStringLiteral("titleName"));
if (!title.isEmpty()) {
return title;
}
}
return {};
}
static QString GetFirmwareVersion(const QJsonObject& root) {
const auto encoded = GetJsonString(root, QStringLiteral("requiredSystemSoftwareVersion"));
static const QRegularExpression version_pattern(
QStringLiteral("^0[xX]([0-9]{6})[0-9A-Fa-f]{10}$"));
const auto match = version_pattern.match(encoded);
if (!match.hasMatch()) {
return {};
}
const auto digits = match.captured(1);
bool valid = false;
const int major = digits.left(2).toInt(&valid, 10);
if (!valid) {
return {};
}
QString version = QStringLiteral("%1.%2").arg(major).arg(digits.mid(2, 2));
const auto patch = digits.mid(4, 2);
if (patch != QStringLiteral("00")) {
version += QStringLiteral(".") + patch;
}
return version;
}
static GameMetadata GetGameMetadata(const QString& param_file, const QString& fallback) {
GameMetadata ret;
ret.title_name = fallback;
QFile param(param_file);
if (!param.open(QIODevice::ReadOnly)) {
return ret;
}
QJsonParseError parse_error;
const auto doc = QJsonDocument::fromJson(param.readAll(), &parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
return ret;
}
const auto root = doc.object();
const auto title = GetLocalizedTitleName(root);
if (!title.isEmpty()) {
ret.title_name = title;
}
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
ret.firmwareVer = GetFirmwareVersion(root);
return ret;
}
static void SetGameFiles(Configuration& info, const QString& game_dir, const QString& game_path,
const GameMetadata& metadata) {
QDir game(game_dir);
info.game_path = game_path;
info.basedir = game.absolutePath();
info.name = metadata.title_name;
info.title_id = metadata.title_id;
info.firmwareVer = metadata.firmwareVer;
if (info.name.isEmpty()) {
info.name = game.dirName();
}
if (info.elf.isEmpty()) {
info.elf = QStringLiteral("eboot.bin");
}
}
static Configuration* FindCustomInfo(QMap<QString, Configuration*>* custom_infos,
const QString& game_path, const QString& legacy_game_path) {
auto custom = custom_infos->find(game_path);
if (custom != custom_infos->end()) {
return custom.value();
}
custom = custom_infos->find(legacy_game_path);
if (custom == custom_infos->end()) {
return nullptr;
}
auto* info = custom.value();
custom_infos->erase(custom);
info->game_path = game_path;
custom_infos->insert(game_path, info);
return info;
}
bool ConfigurationListWidget::EnsureGameDirectory() {
if (HasValidGameDirectory()) {
return true;
}
QMessageBox::information(this, tr("Game folders"),
tr("Add at least one game folder in global settings."));
edit_global_settings();
return HasValidGameDirectory();
}
bool ConfigurationListWidget::HasValidGameDirectory() const {
for (const auto& dir: m_game_dirs) {
if (!dir.isEmpty() && QDir(dir).exists()) {
return true;
}
}
return false;
}
void ConfigurationListWidget::ScanGameDirectory() {
m_selected_item = nullptr;
m_ui->cfgs_list->clear();
m_ui->cfgs_list->SetBackgroundImage({});
m_ui->edit_button->setEnabled(false);
m_ui->delete_button->setEnabled(false);
const QString eboot_name = QStringLiteral("eboot.bin");
QSet<QString> found_games;
for (const auto& root_path: m_game_dirs) {
QDir root(root_path);
if (root_path.isEmpty() || !root.exists()) {
continue;
}
QList<QDir> pending_dirs;
const auto root_subdirs =
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: root_subdirs) {
pending_dirs.append(QDir(subdir.absoluteFilePath()));
}
while (!pending_dirs.isEmpty()) {
QDir game_dir = pending_dirs.takeFirst();
if (game_dir.exists(eboot_name)) {
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
const QString game_key = PathKey(game_path);
if (game_key.isEmpty() || found_games.contains(game_key)) {
continue;
}
found_games.insert(game_key);
auto metadata = GetGameMetadata(
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
auto info = std::make_unique<Configuration>();
auto* custom = FindCustomInfo(&m_custom_infos, game_path, legacy_game_path);
if (custom != nullptr) {
info->CopyFrom(*custom);
info->custom_settings = true;
} else {
info->CopyEmulatorSettingsFrom(m_global_info);
info->custom_settings = false;
}
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
const auto* compatibility = m_compatibility->Find(info->title_id);
if (compatibility != nullptr) {
info->game_status = compatibility->status;
info->game_comment = compatibility->comment;
}
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, this,
[this, item](int /*index*/) {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
item->GetStatusCombo()->currentData().toInt());
item->Update();
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, this, [this, item]() {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_comment = item->GetCommentEdit()->text();
item->Update();
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
continue;
}
const auto subdirs =
game_dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
for (const auto& subdir: subdirs) {
pending_dirs.append(QDir(subdir.absoluteFilePath()));
}
}
}
m_ui->cfgs_list->sortItems(GAME_NAME_COLUMN, Qt::AscendingOrder);
filter_configurations(m_ui->search_line_edit->text());
}
void ConfigurationListWidget::edit_configuration() {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
if (item == nullptr) {
return;
}
ConfigurationEditDialog dlg(item->GetInfo(), this);
dlg.SetTitle(tr("Edit game settings"));
if (dlg.exec() == QDialog::Accepted) {
item->GetInfo().custom_settings = true;
auto game_path = item->GetInfo().game_path;
delete m_custom_infos.take(game_path);
m_custom_infos.insert(game_path, CloneConfiguration(item->GetInfo()));
WriteSettings();
ScanGameDirectory();
}
}
void ConfigurationListWidget::delete_configuartion() {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
if (item == nullptr || !item->GetInfo().custom_settings) {
return;
}
if (QMessageBox::Yes == QMessageBox::question(this, tr("Clear custom settings"),
tr("Do you want to clear custom settings?"))) {
ClearCustomSettings(item);
WriteSettings();
ScanGameDirectory();
}
}
void ConfigurationListWidget::edit_global_settings() {
Configuration info;
info.CopyEmulatorSettingsFrom(m_global_info);
info.name = tr("Global settings");
ConfigurationEditDialog dlg(info, this);
dlg.SetTitle(tr("Global settings"));
dlg.SetGameDirectories(m_game_dirs);
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
m_game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
WriteSettings();
ScanGameDirectory();
}
}
void ConfigurationListWidget::ClearCustomSettings(ConfigurationItem* item) {
delete m_custom_infos.take(item->GetInfo().game_path);
}
void ConfigurationListWidget::run_configuration() {
emit Run();
}
bool ConfigurationListWidget::CanViewSelectedTrophies() const {
return m_selected_item != nullptr &&
TrophyViewerDialog::HasTrophyData(&m_selected_item->GetInfo());
}
void ConfigurationListWidget::ViewTrophies() {
auto* current = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
auto* item = current != nullptr ? current : m_selected_item;
if (item == nullptr) {
return;
}
TrophyViewerDialog::ShowForGame(&item->GetInfo(), this);
}
void ConfigurationListWidget::open_game_folder() {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
if (item == nullptr) {
return;
}
const QDir game_dir(item->GetInfo().basedir);
if (!game_dir.exists()) {
QMessageBox::warning(this, tr("Open game folder"), tr("Game folder does not exist."));
return;
}
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(game_dir.absolutePath()))) {
QMessageBox::warning(this, tr("Open game folder"), tr("Could not open game folder."));
}
}
void ConfigurationListWidget::remove_save_data() {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->currentItem());
if (item == nullptr) {
return;
}
const auto save_data_dirs = GetSaveDataDirs(item->GetInfo());
if (save_data_dirs.isEmpty()) {
QMessageBox::information(this, tr("Remove save data"),
tr("No save data folder found for this game."));
return;
}
const auto title =
!item->GetInfo().name.isEmpty() ? item->GetInfo().name : item->GetInfo().title_id;
const auto text =
tr("Remove save data for \"%1\"?\n\nThis will delete:\n%2\n\nThis cannot be undone.")
.arg(title, save_data_dirs.join(QLatin1Char('\n')));
if (QMessageBox::Yes != QMessageBox::question(this, tr("Remove save data"), text)) {
return;
}
QStringList failed_dirs;
for (const auto& path: save_data_dirs) {
QDir dir(path);
if (dir.exists() && !dir.removeRecursively()) {
failed_dirs.append(path);
}
}
if (!failed_dirs.isEmpty()) {
QMessageBox::warning(this, tr("Remove save data"),
tr("Could not remove:\n%1").arg(failed_dirs.join(QLatin1Char('\n'))));
}
}
void ConfigurationListWidget::filter_configurations(const QString& text) {
const auto query = text.trimmed();
const bool has_query = !query.isEmpty();
bool selection_is_shown = false;
for (int item_index = 0; item_index < m_ui->cfgs_list->topLevelItemCount(); item_index++) {
auto* item = m_ui->cfgs_list->topLevelItem(item_index);
if (item == nullptr) {
continue;
}
const bool match = !has_query ||
item->text(GAME_NAME_COLUMN).contains(query, Qt::CaseInsensitive) ||
item->text(GAME_SERIAL_COLUMN).contains(query, Qt::CaseInsensitive);
item->setHidden(!match);
if (match && item == m_selected_item) {
selection_is_shown = true;
}
}
if (m_selected_item != nullptr && !selection_is_shown) {
m_ui->cfgs_list->clearSelection();
m_ui->cfgs_list->setCurrentItem(nullptr);
SelectItem(nullptr);
}
}
void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
auto* item = static_cast<ConfigurationItem*>(witem);
if (item == nullptr) {
m_selected_item = nullptr;
m_ui->cfgs_list->SetBackgroundImage({});
m_ui->edit_button->setEnabled(false);
m_ui->delete_button->setEnabled(false);
emit Select();
return;
}
m_ui->delete_button->setEnabled(!item->IsRunning() && item->GetInfo().custom_settings);
m_ui->edit_button->setEnabled(!item->IsRunning());
m_selected_item = item;
m_ui->cfgs_list->SetBackgroundImage(GetPic0Path(item->GetInfo()));
emit Select();
}
void ConfigurationListWidget::list_currentItemChanged(QTreeWidgetItem* current,
QTreeWidgetItem* /*previous*/) {
SelectItem(current);
}
void ConfigurationListWidget::list_itemDoubleClicked(QTreeWidgetItem* witem, int /*column*/) {
SelectItem(witem);
if (m_run_enabled) {
emit Run();
}
}
void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
auto* item = static_cast<ConfigurationItem*>(m_ui->cfgs_list->itemAt(pos));
if (item != nullptr) {
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
}
QMenu menu;
const auto save_data_dirs = item != nullptr ? GetSaveDataDirs(item->GetInfo()) : QStringList();
const bool has_trophy_data =
item != nullptr && TrophyViewerDialog::HasTrophyData(&item->GetInfo());
QAction* action_run = menu.addAction(tr("Run"), this, SLOT(run_configuration()));
QAction* action_open_folder =
menu.addAction(style()->standardIcon(QStyle::SP_DirOpenIcon), tr("Open game folder"), this,
SLOT(open_game_folder()));
QAction* action_view_trophies = menu.addAction(
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
connect(action_view_trophies, &QAction::triggered, this,
&ConfigurationListWidget::ViewTrophies);
QAction* action_remove_save_data =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
tr("Remove save data..."), this, SLOT(remove_save_data()));
menu.addSeparator();
QAction* action_edit =
menu.addAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Edit game settings..."),
this, SLOT(edit_configuration()));
QAction* action_delete =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
tr("Clear custom settings"), this, SLOT(delete_configuartion()));
if (item == nullptr) {
menu.addSeparator();
/*QAction *action_global = */ menu.addAction(
style()->standardIcon(QStyle::SP_FileDialogDetailedView), tr("Global settings..."),
this, SLOT(edit_global_settings()));
}
if (item != nullptr) {
action_run->setDisabled(item->IsRunning());
action_open_folder->setDisabled(!QDir(item->GetInfo().basedir).exists());
action_view_trophies->setDisabled(!has_trophy_data);
action_remove_save_data->setDisabled(item->IsRunning() || save_data_dirs.isEmpty());
action_edit->setDisabled(item->IsRunning());
action_delete->setDisabled(item->IsRunning() || !item->GetInfo().custom_settings);
} else {
action_run->setDisabled(true);
action_open_folder->setDisabled(true);
action_view_trophies->setDisabled(true);
action_remove_save_data->setDisabled(true);
action_edit->setDisabled(true);
action_delete->setDisabled(true);
}
if (!m_run_enabled) {
action_run->setDisabled(true);
}
menu.exec(QCursor::pos());
}
+24
View File
@@ -0,0 +1,24 @@
#include "mainDialog.h"
#include <QApplication>
#include <QArgument>
#include <QObject>
#include <QStyleFactory>
class QStyle;
int main(int argc, char* argv[]) {
QApplication a(argc, argv);
MainDialog w;
QStyle* s = QStyleFactory::create("fusion");
QApplication::setStyle(s);
QObject::connect(&a, &QApplication::aboutToQuit, &w, &MainDialog::Quit);
w.emit Start();
w.show();
return QApplication::exec();
}
+374
View File
@@ -0,0 +1,374 @@
#include "mainDialog.h"
#include "common.h"
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include <QApplication>
#include <QByteArray>
#include <QDialog>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QLabel>
#include <QMessageBox>
#include <QProcess>
#include <QRadioButton>
#include <QRegularExpression>
#include <QSettings>
#include <QStringList>
#include <QTextStream>
#include <QVariant>
#include <QtCore>
#include "ui_main_dialog.h"
#ifndef __linux__
#include <windows.h> // IWYU pragma: keep
#endif
// IWYU pragma: no_include <minwindef.h>
// IWYU pragma: no_include <processthreadsapi.h>
// IWYU pragma: no_include <winbase.h>
class QWidget;
#ifdef __linux__
constexpr char EMULATOR_EXE[] = "kyty_emulator";
#else
constexpr char EMULATOR_EXE[] = "kyty_emulator.exe";
#endif
#ifndef __linux__
constexpr char CMD_EXE[] = "cmd.exe";
#else
constexpr char GNOME[] = "gnome-terminal";
constexpr char XTERM[] = "xterm";
constexpr char KYTY_BASH_FILE[] = "kyty_run.sh";
#endif
#ifndef __linux__
constexpr DWORD CMD_X_CHARS = 175;
constexpr DWORD CMD_Y_CHARS = 1000;
#endif
constexpr char SETTINGS_MAIN_DIALOG[] = "MainDialog";
constexpr char SETTINGS_MAIN_LAST_GEOMETRY[] = "geometry";
class DetachableProcess: public QProcess {
Q_OBJECT;
public:
explicit DetachableProcess(QObject* parent = nullptr): QProcess(parent) {}
void Detach() {
this->waitForStarted();
setProcessState(QProcess::NotRunning);
}
};
class MainDialogPrivate: public QObject {
Q_OBJECT
KYTY_QT_CLASS_NO_COPY(MainDialogPrivate);
public:
explicit MainDialogPrivate(QObject* parent = nullptr): QObject(parent) {}
~MainDialogPrivate() override;
void Setup(MainDialog* main_dialog);
/*slots:*/
void Update();
void FindInterpreter();
void Run();
[[nodiscard]] const QString& GetInterpreter() const { return m_interpreter; }
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
private:
static QByteArray g_last_geometry;
Ui::MainDialog* m_ui = {nullptr};
MainDialog* m_main_dialog = nullptr;
QString m_interpreter;
/*DetachableProcess*/ QProcess m_process;
ConfigurationItem* m_running_item = nullptr;
};
QByteArray MainDialogPrivate::g_last_geometry;
MainDialog::MainDialog(QWidget* parent): QDialog(parent), m_p(new MainDialogPrivate(this)) {
m_p->Setup(this);
}
MainDialogPrivate::~MainDialogPrivate() {
delete m_ui;
}
void MainDialogPrivate::Setup(MainDialog* main_dialog) {
m_ui = new Ui::MainDialog;
m_ui->setupUi(main_dialog);
m_main_dialog = main_dialog;
m_ui->widget->SetMainDialog(main_dialog);
main_dialog->setWindowFlags(Qt::Dialog /*| Qt::MSWindowsFixedSizeDialogHint*/);
connect(main_dialog, &MainDialog::Start, this, &MainDialogPrivate::FindInterpreter,
Qt::QueuedConnection);
connect(m_ui->widget, &ConfigurationListWidget::Select, this, &MainDialogPrivate::Update);
connect(m_ui->widget, &ConfigurationListWidget::Run, this, &MainDialogPrivate::Run);
connect(main_dialog, &MainDialog::Resize, [this]() {
g_last_geometry = m_main_dialog->saveGeometry();
m_ui->widget->WriteSettings();
});
connect(&m_process,
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
[this](int /*exitCode*/, QProcess::ExitStatus /*exitStatus*/) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
}
Update();
});
// connect(main_dialog, &MainDialog::Quit, [=]() { m_process.Detach(); });
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
m_main_dialog->restoreGeometry(g_last_geometry);
Update();
}
void MainDialogPrivate::FindInterpreter() {
QDir search_dir(QApplication::applicationDirPath());
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
if (!QFile::exists(m_interpreter)) {
search_dir.cdUp();
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
}
bool found = QFile::exists(m_interpreter);
if (found) {
m_ui->label_Interpreter->setText(tr("Emulator: ") + m_interpreter);
QProcess test;
test.setProgram(m_interpreter);
test.start();
test.waitForFinished();
auto output = QString(test.readAllStandardOutput());
auto lines = output.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
if (lines.count() >= 2) {
m_ui->label_Version->setText(
tr("Version: ") + (lines.at(0).startsWith("exe_name") ? lines.at(1) : lines.at(0)));
} else {
found = false;
}
}
if (!found) {
QMessageBox::critical(m_main_dialog, tr("Error"), tr("Can't find emulator"));
QApplication::quit();
return;
}
if (!m_ui->widget->EnsureGameDirectory()) {
QApplication::quit();
return;
}
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
Update();
}
static QString BoolArg(bool value) {
return value ? QStringLiteral("true") : QStringLiteral("false");
}
static QStringList CreateEmulatorArgs(const Configuration& info) {
QStringList args;
auto r = EnumToText(info.screen_resolution).split('x');
if (r.size() != 2) {
return {};
}
args << "--screen-width" << r.at(0);
args << "--screen-height" << r.at(1);
args << "--vblank-frequency" << QString::number(info.vblank_frequency);
args << "--vulkan-validation" << BoolArg(info.vulkan_validation_enabled);
args << "--shader-validation" << BoolArg(info.shader_validation_enabled);
args << "--shader-optimization-type" << EnumToText(info.shader_optimization_type);
args << "--shader-log-direction" << EnumToText(info.shader_log_direction);
args << "--shader-log-folder" << info.shader_log_folder;
args << "--command-buffer-dump" << BoolArg(info.command_buffer_dump_enabled);
args << "--command-buffer-dump-folder" << info.command_buffer_dump_folder;
args << "--printf-direction" << EnumToText(info.printf_direction);
args << "--printf-output-file" << info.printf_output_file;
args << "--profiler-direction" << EnumToText(info.profiler_direction);
args << "--spirv-debug-printf" << "false";
args << "--ngg-rectlist-draw" << BoolArg(info.ngg_rectlist_draw_enabled);
if (info.renderdoc_enabled) {
args << "--rd";
}
QString game = info.basedir;
if (!info.elf.isEmpty()) {
game = QDir(info.basedir).filePath(info.elf);
}
args << "--game" << game;
return args;
}
#ifdef __linux__
static QString BashQuote(QString value) {
value.replace('\'', "'\\''");
return QStringLiteral("'") + value + QStringLiteral("'");
}
static bool CreateBashScript(const QString& interpreter, const QStringList& args,
const QString& file_name) {
QFile file(file_name);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream s(&file);
s << "#!/bin/bash\n";
s << BashQuote(interpreter);
for (const auto& arg: args) {
s << " " << BashQuote(arg);
}
s << "\n";
s << "echo Press any key...\n";
s << "read -n1\n";
file.close();
return file.setPermissions(file.permissions() | QFile::ExeUser | QFile::ExeOwner |
QFile::ExeGroup);
}
return false;
}
#endif
void MainDialog::RunInterpreter(QProcess* process, const Configuration& info) {
const auto& interpreter = m_p->GetInterpreter();
QFileInfo f(interpreter);
auto dir = f.absoluteDir();
auto args = CreateEmulatorArgs(info);
if (args.isEmpty()) {
QMessageBox::critical(this, tr("Error"), tr("Invalid emulator configuration"));
QApplication::quit();
return;
}
#ifdef __linux__
auto bash_file_name = dir.filePath(KYTY_BASH_FILE);
if (!CreateBashScript(interpreter, args, bash_file_name)) {
QMessageBox::critical(this, tr("Error"), tr("Can't create file:\n") + bash_file_name);
QApplication::quit();
return;
}
{
process->setProgram(GNOME);
process->setArguments({"--", "bash", "-c", bash_file_name});
}
#else
{
process->setProgram(CMD_EXE);
QStringList process_args;
process_args << QStringLiteral("/K") << interpreter;
process_args += args;
process->setArguments(process_args);
}
#endif
process->setWorkingDirectory(dir.path());
#ifndef __linux__
process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments* args) {
args->flags |= static_cast<uint32_t>(CREATE_NEW_CONSOLE);
args->startupInfo->dwFlags &= ~static_cast<DWORD>(STARTF_USESTDHANDLES);
args->startupInfo->dwFlags |= static_cast<DWORD>(STARTF_USECOUNTCHARS);
args->startupInfo->dwXCountChars = CMD_X_CHARS;
args->startupInfo->dwYCountChars = CMD_Y_CHARS;
// args->startupInfo->dwFlags |= static_cast<DWORD>(STARTF_USEFILLATTRIBUTE);
// args->startupInfo->dwFillAttribute =
// static_cast<DWORD>(BACKGROUND_BLUE) | static_cast<DWORD>(FOREGROUND_RED) |
// static_cast<DWORD>(FOREGROUND_INTENSITY);
});
#endif
process->start();
process->waitForFinished(100);
}
void MainDialog::WriteSettings(QSettings& s) {
MainDialogPrivate::WriteSettings(s);
}
void MainDialog::ReadSettings(QSettings& s) {
MainDialogPrivate::ReadSettings(s);
}
void MainDialog::resizeEvent(QResizeEvent* event) {
emit Resize();
QDialog::resizeEvent(event);
}
void MainDialogPrivate::WriteSettings(QSettings& s) {
s.beginGroup(SETTINGS_MAIN_DIALOG);
if (!g_last_geometry.isEmpty()) {
s.setValue(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry);
}
s.endGroup();
}
void MainDialogPrivate::ReadSettings(QSettings& s) {
s.beginGroup(SETTINGS_MAIN_DIALOG);
g_last_geometry = s.value(SETTINGS_MAIN_LAST_GEOMETRY, g_last_geometry).toByteArray();
s.endGroup();
}
void MainDialogPrivate::Run() {
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
return;
}
m_running_item->SetRunning(true);
m_main_dialog->RunInterpreter(&m_process, m_running_item->GetInfo());
Update();
}
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
if (run_enabled) {
const auto& info = item->GetInfo();
auto dir = info.basedir;
run_enabled = !dir.isEmpty() && QDir(dir).exists();
}
m_ui->widget->SetRunEnabled(run_enabled);
}
#include "mainDialog.moc"
+44
View File
@@ -0,0 +1,44 @@
#include "mandatoryLineEdit.h"
#include <QColor>
#include <QLine>
#include <QPainter>
#include <QPen>
#include <QVariant>
#include <QtCore>
class QPaintEvent;
class QWidget;
MandatoryLineEdit::MandatoryLineEdit(QWidget* parent): QLineEdit(parent) {
this->setProperty(MANDATORY_AND_EMPTY, true);
connect(this, &QLineEdit::textChanged, this, &MandatoryLineEdit::changed);
}
void MandatoryLineEdit::paintEvent(QPaintEvent* event) {
QLineEdit::paintEvent(event);
if (this->property(MANDATORY_AND_EMPTY).toBool() && this->isEnabled()) {
QPainter painter(this);
QLine line(4, this->height() - 4, this->width() - 4, this->height() - 4);
painter.setPen(QPen(QColor(255, 0, 0), 1.0, Qt::DotLine));
painter.drawLine(line);
}
}
bool MandatoryLineEdit::FindEmpty(QObject* p) {
const QList<QLineEdit*>& ch_list = p->findChildren<QLineEdit*>();
foreach (QLineEdit* ch, ch_list) {
if (ch->isVisible() && ch->property(MANDATORY_AND_EMPTY).toBool() && ch->isEnabled()) {
return true;
}
}
return false;
}
void MandatoryLineEdit::changed(const QString& text) {
this->setProperty(MANDATORY_AND_EMPTY, text.isEmpty());
}
+582
View File
@@ -0,0 +1,582 @@
#include "trophyViewerDialog.h"
#include "configuration.h"
#include <QAbstractItemView>
#include <QBrush>
#include <QByteArray>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFont>
#include <QHeaderView>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QLabel>
#include <QMap>
#include <QMessageBox>
#include <QPixmap>
#include <QRegularExpression>
#include <QSet>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QTabWidget>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QVBoxLayout>
#include <QVariant>
#include <QWidget>
#include <limits>
namespace {
constexpr quint32 UCP_MAGIC = 0xb228c60a;
constexpr quint32 UCP_VERSION = 1;
constexpr int UCP_HEADER_LEN = 0x40;
constexpr int UCP_TOC_SKIP = 0x20;
constexpr int UCP_ENTRY_LEN = 0x40;
constexpr int UCP_NAME_LEN = 0x20;
struct UcpEntry {
QString name;
quint64 offset = 0;
quint64 size = 0;
};
struct TrophyDefinition {
QString id;
QString grade;
bool hidden = false;
bool has_reward = false;
};
struct TrophyText {
QString name;
QString detail;
QString reward;
};
struct TrophyRow {
QString id;
QString name;
QString detail;
QString grade;
QString reward;
bool hidden = false;
bool has_reward = false;
QPixmap icon;
};
struct TrophySet {
QString tab_title;
QList<TrophyRow> trophies;
};
static bool CanRead(const QByteArray& data, qsizetype offset, qsizetype size) {
return offset >= 0 && size >= 0 && offset <= data.size() && size <= data.size() - offset;
}
static quint32 ReadBe32(const QByteArray& data, qsizetype offset) {
const auto* p = reinterpret_cast<const uchar*>(data.constData() + offset);
return (static_cast<quint32>(p[0]) << 24u) | (static_cast<quint32>(p[1]) << 16u) |
(static_cast<quint32>(p[2]) << 8u) | static_cast<quint32>(p[3]);
}
static quint64 ReadBe64(const QByteArray& data, qsizetype offset) {
return (static_cast<quint64>(ReadBe32(data, offset)) << 32u) |
static_cast<quint64>(ReadBe32(data, offset + 4));
}
static bool FitsQSizeType(quint64 value) {
return value <= static_cast<quint64>(std::numeric_limits<qsizetype>::max());
}
static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizetype max_size) {
if (!CanRead(data, offset, max_size)) {
return {};
}
qsizetype len = 0;
while (len < max_size && data.at(offset + len) != '\0') {
len++;
}
return QString::fromLatin1(data.constData() + offset, len);
}
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
QFile file(file_name);
if (!file.open(QIODevice::ReadOnly)) {
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
return false;
}
const QByteArray data = file.readAll();
if (data.size() < UCP_HEADER_LEN) {
error = QObject::tr("%1 is too small to be a trophy package.")
.arg(QFileInfo(file_name).fileName());
return false;
}
const auto magic = ReadBe32(data, 0x00);
if (magic != UCP_MAGIC) {
error = QObject::tr("%1 has an invalid trophy package magic.")
.arg(QFileInfo(file_name).fileName());
return false;
}
const auto version = ReadBe32(data, 0x04);
if (version != UCP_VERSION) {
error = QObject::tr("%1 uses unsupported trophy package version %2.")
.arg(QFileInfo(file_name).fileName(), QString::number(version));
return false;
}
const auto declared_size = ReadBe64(data, 0x08);
if (declared_size > static_cast<quint64>(data.size())) {
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
return false;
}
const auto file_count = ReadBe32(data, 0x10);
const auto toc_offset = static_cast<quint64>(ReadBe32(data, 0x14));
const auto data_size = static_cast<quint64>(data.size());
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
if (toc_offset > data_size || table_size > data_size - toc_offset ||
!FitsQSizeType(toc_offset + table_size)) {
error = QObject::tr("%1 has an invalid table of contents.")
.arg(QFileInfo(file_name).fileName());
return false;
}
for (quint32 i = 0; i < file_count; i++) {
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
static_cast<quint64>(i) * UCP_ENTRY_LEN);
UcpEntry entry;
entry.name = ReadFixedString(data, entry_offset, UCP_NAME_LEN).trimmed();
entry.offset = ReadBe64(data, entry_offset + 0x20);
entry.size = ReadBe64(data, entry_offset + 0x28);
if (entry.name.isEmpty()) {
continue;
}
if (entry.offset > data_size || entry.size > data_size - entry.offset ||
!FitsQSizeType(entry.offset) || !FitsQSizeType(entry.size)) {
error = QObject::tr("%1 has an invalid entry for %2.")
.arg(QFileInfo(file_name).fileName(), entry.name);
return false;
}
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
static_cast<qsizetype>(entry.size)));
}
return true;
}
static const QByteArray* FindFile(const QMap<QString, QByteArray>& files, const QString& name) {
const auto it = files.constFind(name.toCaseFolded());
return it == files.constEnd() ? nullptr : &it.value();
}
static const QByteArray* FindFirstTrophyMetadataFile(const QMap<QString, QByteArray>& files) {
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
if (it.key().startsWith(QStringLiteral("tropmeta_")) &&
it.key().endsWith(QStringLiteral(".json"))) {
return &it.value();
}
}
return FindFile(files, QStringLiteral("tropmeta.json"));
}
static bool ReadJsonObject(const QByteArray& data, const QString& file_name, QJsonObject& object,
QString& error) {
QJsonParseError parse_error;
const auto doc = QJsonDocument::fromJson(data, &parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
error = QObject::tr("Could not read %1: %2").arg(file_name, parse_error.errorString());
return false;
}
object = doc.object();
return true;
}
static QString JsonString(const QJsonValue& value) {
if (value.isString()) {
return value.toString();
}
if (value.isDouble()) {
const double number = value.toDouble();
if (number == static_cast<int>(number)) {
return QString::number(static_cast<int>(number));
}
return QString::number(number);
}
if (value.isBool()) {
return value.toBool() ? QStringLiteral("true") : QStringLiteral("false");
}
return {};
}
static QList<TrophyDefinition> ReadDefinitions(const QJsonObject& tropconf) {
QList<TrophyDefinition> ret;
const auto trophies = tropconf.value(QStringLiteral("trophies")).toArray();
for (const auto& value: trophies) {
const auto obj = value.toObject();
TrophyDefinition def;
def.id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
def.grade = JsonString(obj.value(QStringLiteral("grade"))).trimmed();
def.hidden = obj.value(QStringLiteral("hidden")).toBool(false);
def.has_reward = obj.value(QStringLiteral("hasReward")).toBool(false);
if (!def.id.isEmpty()) {
ret.append(def);
}
}
return ret;
}
static QMap<QString, TrophyText> ReadMetadata(const QJsonObject& tropmeta) {
QMap<QString, TrophyText> ret;
const auto metadata = tropmeta.value(QStringLiteral("metadata")).toObject();
const auto trophies = metadata.value(QStringLiteral("trophyMetadata")).toArray();
for (const auto& value: trophies) {
const auto obj = value.toObject();
const auto id = JsonString(obj.value(QStringLiteral("id"))).trimmed();
if (id.isEmpty()) {
continue;
}
TrophyText text;
text.name = JsonString(obj.value(QStringLiteral("name"))).trimmed();
text.detail = JsonString(obj.value(QStringLiteral("detail"))).trimmed();
text.reward = JsonString(obj.value(QStringLiteral("reward"))).trimmed();
ret.insert(id, text);
}
return ret;
}
static QString GradeToText(const QString& grade) {
if (grade == QStringLiteral("P")) {
return QObject::tr("Platinum");
}
if (grade == QStringLiteral("G")) {
return QObject::tr("Gold");
}
if (grade == QStringLiteral("S")) {
return QObject::tr("Silver");
}
if (grade == QStringLiteral("B")) {
return QObject::tr("Bronze");
}
return grade;
}
static QPixmap LoadTrophyIcon(const QMap<QString, QByteArray>& files, const QString& id) {
QStringList names;
names.append(QStringLiteral("trop%1.png").arg(id));
bool id_is_number = false;
const int id_num = id.toInt(&id_is_number);
if (id_is_number) {
names.append(QStringLiteral("trop%1.png").arg(id_num, 4, 10, QLatin1Char('0')));
}
for (const auto& name: names) {
const auto* data = FindFile(files, name);
if (data == nullptr) {
continue;
}
QPixmap pixmap;
if (pixmap.loadFromData(*data)) {
return pixmap;
}
}
return {};
}
static QString TrophyTabTitle(const QString& file_name) {
static const QRegularExpression trophy_file_re(QStringLiteral("^trophy(\\d+)\\.ucp$"),
QRegularExpression::CaseInsensitiveOption);
const auto name = QFileInfo(file_name).fileName();
const auto match = trophy_file_re.match(name);
if (match.hasMatch()) {
return QObject::tr("Trophy %1").arg(match.captured(1));
}
return QFileInfo(file_name).completeBaseName();
}
static bool BuildTrophySet(const QString& ucp_file, TrophySet& set, QString& error) {
QMap<QString, QByteArray> files;
if (!ReadUcp(ucp_file, files, error)) {
return false;
}
const auto* conf_data = FindFile(files, QStringLiteral("tropconf.json"));
if (conf_data == nullptr) {
error =
QObject::tr("%1 does not contain tropconf.json.").arg(QFileInfo(ucp_file).fileName());
return false;
}
QJsonObject tropconf;
if (!ReadJsonObject(*conf_data, QStringLiteral("tropconf.json"), tropconf, error)) {
return false;
}
const auto default_language =
JsonString(tropconf.value(QStringLiteral("defaultLanguage"))).trimmed();
const QByteArray* meta_data = nullptr;
if (!default_language.isEmpty()) {
meta_data = FindFile(files, QStringLiteral("tropmeta_%1.json").arg(default_language));
}
if (meta_data == nullptr) {
meta_data = FindFirstTrophyMetadataFile(files);
}
if (meta_data == nullptr) {
error = QObject::tr("%1 does not contain readable trophy metadata.")
.arg(QFileInfo(ucp_file).fileName());
return false;
}
QJsonObject tropmeta;
if (!ReadJsonObject(*meta_data, QStringLiteral("tropmeta.json"), tropmeta, error)) {
return false;
}
const auto definitions = ReadDefinitions(tropconf);
if (definitions.isEmpty()) {
error = QObject::tr("%1 does not define any trophies.").arg(QFileInfo(ucp_file).fileName());
return false;
}
const auto texts = ReadMetadata(tropmeta);
set.tab_title = TrophyTabTitle(ucp_file);
for (const auto& def: definitions) {
const auto text = texts.value(def.id);
TrophyRow row;
row.id = def.id;
row.grade = def.grade;
row.hidden = def.hidden;
row.has_reward = def.has_reward;
row.reward = text.reward;
row.name = text.name;
row.detail = text.detail;
row.icon = LoadTrophyIcon(files, def.id);
if (row.name.isEmpty()) {
row.name =
row.hidden ? QObject::tr("Hidden Trophy") : QObject::tr("Trophy %1").arg(def.id);
}
if (row.detail.isEmpty() && row.hidden) {
row.detail = QObject::tr("This trophy is hidden.");
}
set.trophies.append(row);
}
return true;
}
static QStringList FindTrophyFiles(const Configuration* info) {
if (info == nullptr || info->basedir.isEmpty()) {
return {};
}
const QDir trophy_dir(QDir(info->basedir).filePath(QStringLiteral("sce_sys/trophy2")));
if (!trophy_dir.exists()) {
return {};
}
const auto files =
trophy_dir.entryInfoList({QStringLiteral("Trophy*.ucp"), QStringLiteral("trophy*.ucp")},
QDir::Files | QDir::NoSymLinks, QDir::Name | QDir::IgnoreCase);
QStringList trophy_files;
QSet<QString> seen;
for (const auto& file: files) {
auto key = file.canonicalFilePath();
if (key.isEmpty()) {
key = file.absoluteFilePath();
}
key = QDir::cleanPath(key).toCaseFolded();
if (!seen.contains(key)) {
seen.insert(key);
trophy_files.append(file.absoluteFilePath());
}
}
return trophy_files;
}
static void PrepareTable(QTableWidget* table) {
table->setColumnCount(4);
table->setHorizontalHeaderLabels({QObject::tr("Unlocked"), QObject::tr("Trophy"),
QObject::tr("Name"), QObject::tr("Description")});
table->setAlternatingRowColors(true);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setShowGrid(false);
table->setWordWrap(true);
table->verticalHeader()->setVisible(false);
table->verticalHeader()->setDefaultSectionSize(112);
table->horizontalHeader()->setStretchLastSection(true);
table->horizontalHeader()->setHighlightSections(false);
table->setColumnWidth(0, 100);
table->setColumnWidth(1, 132);
table->setColumnWidth(2, 260);
table->setColumnWidth(3, 480);
}
static QTableWidgetItem* CreateItem(const QString& text) {
auto* item = new QTableWidgetItem(text);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
return item;
}
static QString TrophyTooltip(const TrophyRow& row) {
QStringList lines;
lines.append(row.name);
if (!row.detail.isEmpty()) {
lines.append(row.detail);
}
const auto grade = GradeToText(row.grade);
if (!grade.isEmpty()) {
lines.append(QObject::tr("Grade: %1").arg(grade));
}
if (row.hidden) {
lines.append(QObject::tr("Hidden trophy"));
}
if (row.has_reward && !row.reward.isEmpty()) {
lines.append(QObject::tr("Reward: %1").arg(row.reward));
}
return lines.join(QLatin1Char('\n'));
}
} // namespace
TrophyViewerDialog::TrophyViewerDialog(QWidget* parent): QDialog(parent) {
setWindowTitle(tr("Trophy Viewer"));
resize(1000, 640);
auto* layout = new QVBoxLayout(this);
m_tabs = new QTabWidget(this);
layout->addWidget(m_tabs, 1);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttons);
}
bool TrophyViewerDialog::HasTrophyData(const Configuration* info) {
return !FindTrophyFiles(info).isEmpty();
}
void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent) {
if (info == nullptr) {
return;
}
TrophyViewerDialog dlg(parent);
if (!info->name.isEmpty()) {
dlg.setWindowTitle(tr("Trophy Viewer - %1").arg(info->name));
}
QString error;
if (!dlg.LoadGame(*info, error)) {
QMessageBox::warning(parent, tr("Trophy Viewer"), error);
return;
}
dlg.exec();
}
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
const auto trophy_files = FindTrophyFiles(&info);
if (trophy_files.isEmpty()) {
error = tr("No trophy package found in sce_sys/trophy2.");
return false;
}
QStringList errors;
for (const auto& file: trophy_files) {
TrophySet set;
QString set_error;
if (!BuildTrophySet(file, set, set_error)) {
errors.append(set_error);
continue;
}
auto* table = new QTableWidget(set.trophies.size(), 4, m_tabs);
PrepareTable(table);
for (int row_index = 0; row_index < set.trophies.size(); row_index++) {
const auto& row = set.trophies.at(row_index);
table->setRowHeight(row_index, 112);
auto* status = CreateItem(tr("locked"));
status->setTextAlignment(Qt::AlignCenter);
if (row.hidden) {
status->setForeground(QBrush(Qt::gray));
}
table->setItem(row_index, 0, status);
auto* icon_label = new QLabel(table);
icon_label->setAlignment(Qt::AlignCenter);
icon_label->setMinimumSize(QSize(108, 108));
if (!row.icon.isNull()) {
icon_label->setPixmap(
row.icon.scaled(QSize(96, 96), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
table->setCellWidget(row_index, 1, icon_label);
auto* name = CreateItem(row.name);
auto font = name->font();
font.setBold(true);
name->setFont(font);
name->setToolTip(TrophyTooltip(row));
table->setItem(row_index, 2, name);
auto* detail = CreateItem(row.detail);
detail->setToolTip(TrophyTooltip(row));
table->setItem(row_index, 3, detail);
}
m_tabs->addTab(table, set.tab_title);
}
if (m_tabs->count() == 0) {
error = errors.isEmpty() ? tr("No readable trophy data found.")
: errors.join(QLatin1Char('\n'));
return false;
}
return true;
}