Add minimal character package loading

This commit is contained in:
2026-05-28 10:38:26 +08:00
parent ec7a18f7cf
commit 4cfeb5022c
7 changed files with 270 additions and 5 deletions
+4
View File
@@ -14,6 +14,10 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(QtDesktopPet qt_add_executable(QtDesktopPet
main.cpp 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.h
src/ui/PetWindow.cpp src/ui/PetWindow.cpp
) )
+18
View File
@@ -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();
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <QMap>
#include <QString>
#include <QStringList>
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<QString, CharacterState> states;
bool hasState(const QString &stateName) const;
const CharacterState *state(const QString &stateName) const;
};
+174
View File
@@ -0,0 +1,174 @@
#include "CharacterPackageLoader.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
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;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "CharacterPackage.h"
#include <QString>
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);
};
+23 -4
View File
@@ -1,5 +1,7 @@
#include "PetWindow.h" #include "PetWindow.h"
#include "../character/CharacterPackageLoader.h"
#include <QAction> #include <QAction>
#include <QContextMenuEvent> #include <QContextMenuEvent>
#include <QCursor> #include <QCursor>
@@ -13,6 +15,11 @@
namespace namespace
{ {
QString characterPackagePath()
{
return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko");
}
QString previewImagePath() QString previewImagePath()
{ {
return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko/preview.png"); return QStringLiteral(PET_SOURCE_DIR) + QStringLiteral("/shiroko/preview.png");
@@ -36,7 +43,7 @@ PetWindow::PetWindow(QWidget *parent)
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_imageLabel); layout->addWidget(m_imageLabel);
loadPreviewImage(); loadInitialImage();
} }
void PetWindow::contextMenuEvent(QContextMenuEvent *event) void PetWindow::contextMenuEvent(QContextMenuEvent *event)
@@ -98,9 +105,22 @@ void PetWindow::mouseReleaseEvent(QMouseEvent *event)
QWidget::mouseReleaseEvent(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); QPixmap pixmap(imagePath);
if (pixmap.isNull()) if (pixmap.isNull())
{ {
@@ -138,4 +158,3 @@ void PetWindow::setAlwaysOnTop(bool enabled)
setWindowFlags(flags); setWindowFlags(flags);
show(); show();
} }
+2 -1
View File
@@ -16,7 +16,8 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
private: private:
void loadPreviewImage(); void loadInitialImage();
void setDisplayImage(const QString &imagePath);
void setAlwaysOnTop(bool enabled); void setAlwaysOnTop(bool enabled);
QLabel *m_imageLabel; QLabel *m_imageLabel;