From 4cfeb5022cf1293a780e560eb1dad6b138ee8dde Mon Sep 17 00:00:00 2001 From: Ysm-04 Date: Thu, 28 May 2026 10:38:26 +0800 Subject: [PATCH] Add minimal character package loading --- CMakeLists.txt | 4 + src/character/CharacterPackage.cpp | 18 +++ src/character/CharacterPackage.h | 33 +++++ src/character/CharacterPackageLoader.cpp | 174 +++++++++++++++++++++++ src/character/CharacterPackageLoader.h | 16 +++ src/ui/PetWindow.cpp | 27 +++- src/ui/PetWindow.h | 3 +- 7 files changed, 270 insertions(+), 5 deletions(-) create mode 100644 src/character/CharacterPackage.cpp create mode 100644 src/character/CharacterPackage.h create mode 100644 src/character/CharacterPackageLoader.cpp create mode 100644 src/character/CharacterPackageLoader.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2754048..ed500b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,10 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_add_executable(QtDesktopPet main.cpp + src/character/CharacterPackage.h + src/character/CharacterPackage.cpp + src/character/CharacterPackageLoader.h + src/character/CharacterPackageLoader.cpp src/ui/PetWindow.h src/ui/PetWindow.cpp ) diff --git a/src/character/CharacterPackage.cpp b/src/character/CharacterPackage.cpp new file mode 100644 index 0000000..00639e1 --- /dev/null +++ b/src/character/CharacterPackage.cpp @@ -0,0 +1,18 @@ +#include "CharacterPackage.h" + +bool CharacterPackage::hasState(const QString &stateName) const +{ + return states.contains(stateName); +} + +const CharacterState *CharacterPackage::state(const QString &stateName) const +{ + auto iterator = states.constFind(stateName); + if (iterator == states.constEnd()) + { + return nullptr; + } + + return &iterator.value(); +} + diff --git a/src/character/CharacterPackage.h b/src/character/CharacterPackage.h new file mode 100644 index 0000000..53f70c8 --- /dev/null +++ b/src/character/CharacterPackage.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +struct CharacterState +{ + QString name; + QString relativePath; + QStringList framePaths; + int fps = 0; + bool loop = true; + QString nextState; +}; + +class CharacterPackage +{ +public: + int schemaVersion = 0; + QString id; + QString displayName; + QString author; + QString version; + QString packagePath; + QString previewPath; + QString defaultState; + QMap states; + + bool hasState(const QString &stateName) const; + const CharacterState *state(const QString &stateName) const; +}; + diff --git a/src/character/CharacterPackageLoader.cpp b/src/character/CharacterPackageLoader.cpp new file mode 100644 index 0000000..abd82e3 --- /dev/null +++ b/src/character/CharacterPackageLoader.cpp @@ -0,0 +1,174 @@ +#include "CharacterPackageLoader.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr int SupportedSchemaVersion = 1; + +QString requiredString(const QJsonObject &object, const QString &key) +{ + const QJsonValue value = object.value(key); + if (!value.isString()) + { + return QString(); + } + + return value.toString().trimmed(); +} +} + +CharacterPackage CharacterPackageLoader::load(const QString &packagePath, QString *errorMessage) +{ + CharacterPackage package; + package.packagePath = QDir::cleanPath(packagePath); + + const QDir packageDirectory(package.packagePath); + if (!packageDirectory.exists()) + { + setError(errorMessage, QStringLiteral("Character package directory does not exist.")); + return package; + } + + QFile file(packageDirectory.filePath(QStringLiteral("character.json"))); + if (!file.exists()) + { + setError(errorMessage, QStringLiteral("character.json does not exist.")); + return package; + } + + if (!file.open(QIODevice::ReadOnly)) + { + setError(errorMessage, QStringLiteral("Unable to read character.json.")); + return package; + } + + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) + { + setError(errorMessage, QStringLiteral("character.json has invalid format.")); + return package; + } + + const QJsonObject root = document.object(); + package.schemaVersion = root.value(QStringLiteral("schemaVersion")).toInt(0); + if (package.schemaVersion != SupportedSchemaVersion) + { + setError(errorMessage, QStringLiteral("Character package schemaVersion is not supported.")); + return package; + } + + package.id = requiredString(root, QStringLiteral("id")); + package.displayName = requiredString(root, QStringLiteral("displayName")); + package.author = requiredString(root, QStringLiteral("author")); + package.version = requiredString(root, QStringLiteral("version")); + package.defaultState = requiredString(root, QStringLiteral("defaultState")); + + if (package.id.isEmpty()) + { + setError(errorMessage, QStringLiteral("Character package id is empty.")); + return CharacterPackage(); + } + + if (package.defaultState.isEmpty()) + { + setError(errorMessage, QStringLiteral("Character package defaultState is empty.")); + return CharacterPackage(); + } + + const QString previewRelativePath = requiredString(root, QStringLiteral("preview")); + if (!previewRelativePath.isEmpty()) + { + package.previewPath = packageDirectory.filePath(previewRelativePath); + } + + const QJsonValue statesValue = root.value(QStringLiteral("states")); + if (!statesValue.isObject() || statesValue.toObject().isEmpty()) + { + setError(errorMessage, QStringLiteral("Character package states is empty.")); + return CharacterPackage(); + } + + const QJsonObject statesObject = statesValue.toObject(); + for (auto iterator = statesObject.constBegin(); iterator != statesObject.constEnd(); ++iterator) + { + if (!iterator.value().isObject()) + { + continue; + } + + const QString stateName = iterator.key(); + const QJsonObject stateObject = iterator.value().toObject(); + CharacterState state; + state.name = stateName; + state.relativePath = requiredString(stateObject, QStringLiteral("path")); + state.fps = stateObject.value(QStringLiteral("fps")).toInt(0); + state.loop = stateObject.value(QStringLiteral("loop")).toBool(true); + state.nextState = requiredString(stateObject, QStringLiteral("next")); + + if (state.relativePath.isEmpty() || state.fps <= 0) + { + continue; + } + + state.framePaths = collectPngFrames(packageDirectory.filePath(state.relativePath)); + if (state.framePaths.isEmpty()) + { + continue; + } + + package.states.insert(state.name, state); + } + + if (!package.hasState(package.defaultState)) + { + setError(errorMessage, QStringLiteral("defaultState does not exist or has no usable frames.")); + return CharacterPackage(); + } + + if (!package.hasState(QStringLiteral("idle"))) + { + setError(errorMessage, QStringLiteral("idle state does not exist or has no usable frames.")); + return CharacterPackage(); + } + + return package; +} + +QStringList CharacterPackageLoader::collectPngFrames(const QString &stateDirectoryPath) +{ + const QDir stateDirectory(stateDirectoryPath); + if (!stateDirectory.exists()) + { + return {}; + } + + const QFileInfoList frameFiles = stateDirectory.entryInfoList( + {QStringLiteral("*.png"), QStringLiteral("*.PNG")}, + QDir::Files, + QDir::Name); + + QStringList framePaths; + for (const QFileInfo &frameFile : frameFiles) + { + framePaths.append(frameFile.absoluteFilePath()); + } + + return framePaths; +} + +bool CharacterPackageLoader::setError(QString *errorMessage, const QString &message) +{ + if (errorMessage != nullptr) + { + *errorMessage = message; + } + + return false; +} diff --git a/src/character/CharacterPackageLoader.h b/src/character/CharacterPackageLoader.h new file mode 100644 index 0000000..65167f5 --- /dev/null +++ b/src/character/CharacterPackageLoader.h @@ -0,0 +1,16 @@ +#pragma once + +#include "CharacterPackage.h" + +#include + +class CharacterPackageLoader +{ +public: + static CharacterPackage load(const QString &packagePath, QString *errorMessage = nullptr); + +private: + static QStringList collectPngFrames(const QString &stateDirectoryPath); + static bool setError(QString *errorMessage, const QString &message); +}; + diff --git a/src/ui/PetWindow.cpp b/src/ui/PetWindow.cpp index 900ce97..cefdab0 100644 --- a/src/ui/PetWindow.cpp +++ b/src/ui/PetWindow.cpp @@ -1,5 +1,7 @@ #include "PetWindow.h" +#include "../character/CharacterPackageLoader.h" + #include #include #include @@ -13,6 +15,11 @@ namespace { +QString characterPackagePath() +{ + return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko"); +} + QString previewImagePath() { return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko/preview.png"); @@ -36,7 +43,7 @@ PetWindow::PetWindow(QWidget *parent) layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_imageLabel); - loadPreviewImage(); + loadInitialImage(); } void PetWindow::contextMenuEvent(QContextMenuEvent *event) @@ -98,9 +105,22 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event) QWidget::mouseReleaseEvent(event); } -void PetWindow::loadPreviewImage() +void PetWindow::loadInitialImage() +{ + QString loadError; + const CharacterPackage package = CharacterPackageLoader::load(characterPackagePath(), &loadError); + const CharacterState *idleState = package.state(QStringLiteral("idle")); + if (idleState != nullptr && !idleState->framePaths.isEmpty()) + { + setDisplayImage(idleState->framePaths.first()); + return; + } + + setDisplayImage(previewImagePath()); +} + +void PetWindow::setDisplayImage(const QString &imagePath) { - const QString imagePath = previewImagePath(); QPixmap pixmap(imagePath); if (pixmap.isNull()) { @@ -138,4 +158,3 @@ void PetWindow::setAlwaysOnTop(bool enabled) setWindowFlags(flags); show(); } - diff --git a/src/ui/PetWindow.h b/src/ui/PetWindow.h index 1c58c67..77b7413 100644 --- a/src/ui/PetWindow.h +++ b/src/ui/PetWindow.h @@ -16,7 +16,8 @@ protected: void mouseReleaseEvent(QMouseEvent *event) override; private: - void loadPreviewImage(); + void loadInitialImage(); + void setDisplayImage(const QString &imagePath); void setAlwaysOnTop(bool enabled); QLabel *m_imageLabel;