├── resources ├── D1GraphicsTool.rc ├── icon.ico ├── null.trn ├── default.pal ├── demo001.gif ├── D1GraphicsTool.qss ├── d1files.qrc └── black.trn ├── source ├── .clang-format ├── d1formats │ ├── d1image.h │ ├── d1cel.h │ ├── d1celtileset.h │ ├── d1celframe.h │ ├── d1cl2.h │ ├── d1amp.h │ ├── d1sol.h │ ├── d1til.h │ ├── d1trn.h │ ├── d1min.h │ ├── d1image.cpp │ ├── d1celtilesetframe.h │ ├── d1trn.cpp │ ├── d1gfx.h │ ├── d1sol.cpp │ ├── d1amp.cpp │ ├── d1til.cpp │ ├── d1celtileset.cpp │ ├── d1celframe.cpp │ ├── d1min.cpp │ └── d1gfx.cpp ├── config │ ├── config.h │ └── config.cpp ├── views │ ├── view.h │ ├── view.cpp │ ├── celview.h │ └── levelcelview.h ├── undostack │ ├── command.h │ ├── command.cpp │ ├── undostack.h │ ├── framecmds.cpp │ ├── framecmds.h │ ├── undomacro.cpp │ ├── undomacro.h │ └── undostack.cpp ├── dialogs │ ├── settingsdialog.h │ ├── importdialog.h │ ├── openasdialog.h │ ├── exportdialog.h │ ├── settingsdialog.cpp │ ├── importdialog.cpp │ ├── settingsdialog.ui │ └── openasdialog.cpp ├── widgets │ ├── leveltabframewidget.h │ ├── leveltabsubtilewidget.h │ ├── leveltabtilewidget.h │ ├── leveltabframewidget.ui │ ├── leveltabsubtilewidget.ui │ ├── leveltabsubtilewidget.cpp │ ├── leveltabtilewidget.cpp │ ├── leveltabtilewidget.ui │ └── palettewidget.h ├── main.cpp ├── palette │ ├── d1palhits.h │ ├── d1pal.h │ ├── d1palhits.cpp │ └── d1pal.cpp ├── .clang-tidy └── mainwindow.h ├── debian └── usr │ └── share │ └── applications │ └── D1GraphicsTool.desktop ├── .github └── workflows │ ├── linux-qt5.yml │ ├── linux.yml │ ├── clang-format-check.yml │ ├── windows.yml │ └── windows-32.yml ├── TODO.md ├── .gitignore ├── .editorconfig ├── README.md ├── LICENSE.md ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── CHANGELOG.md /resources/D1GraphicsTool.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON DISCARDABLE "icon.ico" 2 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasurgical/d1-graphics-tool/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/null.trn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasurgical/d1-graphics-tool/HEAD/resources/null.trn -------------------------------------------------------------------------------- /resources/default.pal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasurgical/d1-graphics-tool/HEAD/resources/default.pal -------------------------------------------------------------------------------- /resources/demo001.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diasurgical/d1-graphics-tool/HEAD/resources/demo001.gif -------------------------------------------------------------------------------- /resources/D1GraphicsTool.qss: -------------------------------------------------------------------------------- 1 | QLineEdit[readOnly="true"] { 2 | color: #808080; 3 | background-color: #F0F0F0; 4 | } 5 | 6 | QMessageBox { 7 | messagebox-text-interaction-flags: 5; 8 | } 9 | -------------------------------------------------------------------------------- /resources/d1files.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | null.trn 4 | default.pal 5 | D1GraphicsTool.qss 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/black.trn: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: webkit 2 | AlignTrailingComments: true 3 | AllowShortBlocksOnASingleLine: true 4 | AllowShortFunctionsOnASingleLine: None 5 | PointerAlignment: Right 6 | TabWidth: 4 7 | UseTab: Never 8 | SortIncludes: true 9 | NamespaceIndentation: None 10 | FixNamespaceComments: true 11 | -------------------------------------------------------------------------------- /debian/usr/share/applications/D1GraphicsTool.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Diablo 1 Graphics Tool 4 | Comment=Work with Diablo 1 graphics files 5 | Keywords=Diablo;graphic; 6 | Exec=D1GraphicsTool 7 | Icon=/opt/d1-graphics-tool/icon.svg 8 | Terminal=false 9 | Categories=Graphics;2DGraphics;RasterGraphics; 10 | StartupNotify=true 11 | -------------------------------------------------------------------------------- /source/d1formats/d1image.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "d1gfx.h" 7 | #include "palette/d1pal.h" 8 | 9 | // alpha value under which the color is considered as transparent 10 | #define COLOR_ALPHA_LIMIT 128 11 | 12 | class D1ImageFrame { 13 | public: 14 | static bool load(D1GfxFrame &frame, const QImage &image, D1Pal *pal); 15 | }; 16 | -------------------------------------------------------------------------------- /source/config/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Config { 6 | public: 7 | static void loadConfiguration(); 8 | static void storeConfiguration(); 9 | static QJsonValue value(const QString &name); 10 | static void insert(const QString &key, const QJsonValue &value); 11 | 12 | private: 13 | static bool createDirectoriesOnPath(); 14 | 15 | static QString dirPath; 16 | }; 17 | -------------------------------------------------------------------------------- /source/views/view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class View : public QGraphicsView { 8 | Q_OBJECT 9 | 10 | public: 11 | View(QWidget *parent = nullptr); 12 | 13 | private slots: 14 | void mouseReleaseEvent(QMouseEvent *event); 15 | void mousePressEvent(QMouseEvent *event); 16 | void leaveEvent(QEvent *event) override; 17 | }; 18 | -------------------------------------------------------------------------------- /source/undostack/command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Command { 4 | public: 5 | virtual void undo() = 0; 6 | virtual void redo() = 0; 7 | 8 | void setObsolete(bool isObsolete); 9 | bool isObsolete() const; 10 | void setMacroID(unsigned int macroID); 11 | unsigned int macroID() const; 12 | 13 | virtual ~Command() = default; 14 | 15 | private: 16 | unsigned int m_macroID { 0 }; 17 | bool m_isObsolete = false; 18 | }; 19 | -------------------------------------------------------------------------------- /source/d1formats/d1cel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "d1gfx.h" 7 | #include "dialogs/openasdialog.h" 8 | 9 | class D1Cel { 10 | public: 11 | static bool load(D1Gfx &gfx, QString celFilePath, const OpenAsParam ¶ms); 12 | static bool save(D1Gfx &gfx, const QString &gfxPath); 13 | 14 | private: 15 | static bool writeFileData(D1Gfx &gfx, QFile &outFile); 16 | static bool writeCompFileData(D1Gfx &gfx, QFile &outFile); 17 | }; 18 | -------------------------------------------------------------------------------- /source/d1formats/d1celtileset.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "d1celtilesetframe.h" 9 | #include "d1gfx.h" 10 | #include "dialogs/openasdialog.h" 11 | 12 | class D1CelTileset { 13 | public: 14 | static bool load(D1Gfx &gfx, std::map &celFrameTypes, QString celFilePath, const OpenAsParam ¶ms); 15 | static bool save(D1Gfx &gfx, const QString &gfxPath); 16 | 17 | private: 18 | static bool writeFileData(D1Gfx &gfx, QFile &outFile); 19 | }; 20 | -------------------------------------------------------------------------------- /source/dialogs/settingsdialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Ui { 6 | class SettingsDialog; 7 | } 8 | 9 | class SettingsDialog : public QDialog { 10 | Q_OBJECT 11 | 12 | public: 13 | explicit SettingsDialog(QWidget *parent = nullptr); 14 | ~SettingsDialog(); 15 | 16 | void initialize(); 17 | 18 | signals: 19 | void configurationSaved(); 20 | 21 | private slots: 22 | void on_defaultPaletteColorPushButton_clicked(); 23 | void on_paletteSelectionBorderColorPushButton_clicked(); 24 | void on_settingsOkButton_clicked(); 25 | void on_settingsCancelButton_clicked(); 26 | 27 | private: 28 | Ui::SettingsDialog *ui; 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/linux-qt5.yml: -------------------------------------------------------------------------------- 1 | name: Linux Qt5 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: dependencies 17 | run: sudo apt-get update && sudo apt-get install cmake qtbase5-dev 18 | - name: configure 19 | run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo 20 | - name: make 21 | run: cmake --build build -j $(nproc) 22 | - name: Upload-Package 23 | if: ${{ !env.ACT }} 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: D1GraphicsTool-Linux-x64-qt5 27 | path: build/D1GraphicsTool 28 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux Qt6 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: dependencies 17 | run: sudo apt-get update && sudo apt-get install cmake qt6-base-dev libgl1-mesa-dev 18 | - name: configure 19 | run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo 20 | - name: make 21 | run: cmake --build build -j $(nproc) 22 | - name: Upload-Package 23 | if: ${{ !env.ACT }} 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: D1GraphicsTool-Linux-x64 27 | path: build/D1GraphicsTool 28 | -------------------------------------------------------------------------------- /source/d1formats/d1celframe.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "d1gfx.h" 6 | #include "dialogs/openasdialog.h" 7 | 8 | // Class used only for CEL frame width calculation 9 | class D1CelPixelGroup { 10 | public: 11 | D1CelPixelGroup() = default; 12 | D1CelPixelGroup(bool, quint16); 13 | 14 | bool isTransparent() const; 15 | quint16 getPixelCount(); 16 | 17 | private: 18 | bool transparent = false; 19 | quint16 pixelCount = 0; 20 | }; 21 | 22 | class D1CelFrame { 23 | public: 24 | static bool load(D1GfxFrame &frame, QByteArray rawData, const OpenAsParam ¶ms); 25 | 26 | private: 27 | static quint16 computeWidthFromHeader(QByteArray &); 28 | static quint16 computeWidthFromData(QByteArray &); 29 | }; 30 | -------------------------------------------------------------------------------- /source/d1formats/d1cl2.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "d1gfx.h" 7 | #include "dialogs/openasdialog.h" 8 | 9 | class D1Cl2Frame { 10 | friend class D1Cl2; 11 | 12 | public: 13 | static bool load(D1GfxFrame &frame, QByteArray rawFrameData, bool isClx, const OpenAsParam ¶ms); 14 | 15 | private: 16 | static quint16 computeWidthFromHeader(QByteArray &rawFrameData, bool isClx); 17 | }; 18 | 19 | class D1Cl2 { 20 | public: 21 | static bool load(D1Gfx &gfx, QString cl2FilePath, bool isClx, const OpenAsParam ¶ms); 22 | static bool save(D1Gfx &gfx, bool isClx, const QString &gfxPath); 23 | 24 | protected: 25 | static bool writeFileData(D1Gfx &gfx, QFile &outFile, bool isClx, const QString &gfxPath); 26 | }; 27 | -------------------------------------------------------------------------------- /source/dialogs/importdialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Ui { 6 | class ImportDialog; 7 | } 8 | 9 | class ImportDialog : public QDialog { 10 | Q_OBJECT 11 | 12 | public: 13 | explicit ImportDialog(QWidget *parent = nullptr); 14 | ~ImportDialog(); 15 | 16 | void initialize(); 17 | 18 | private slots: 19 | void on_inputFileBrowseButton_clicked(); 20 | void on_fontSymbolsEdit_textChanged(const QString &text); 21 | void on_fontColorButton_clicked(); 22 | void on_importButton_clicked(); 23 | void on_importCancelButton_clicked(); 24 | 25 | private: 26 | void setRenderColor(QColor color); 27 | QString getFileFormatExtension(); 28 | 29 | Ui::ImportDialog *ui; 30 | QColor renderColor = QColor::fromRgb(204, 183, 117); 31 | }; 32 | -------------------------------------------------------------------------------- /source/undostack/command.cpp: -------------------------------------------------------------------------------- 1 | #include "command.h" 2 | 3 | /** 4 | * @brief Sets if a command is obsolete or not 5 | * 6 | * This function sets if a command is obsolete or not. 7 | * This flag will be used in UndoStack to check if a 8 | * command should be popped whenever there will be undo/redo 9 | * operation used, or user pushes any command onto the stack. 10 | */ 11 | void Command::setObsolete(bool isObsolete) 12 | { 13 | m_isObsolete = isObsolete; 14 | } 15 | 16 | /** 17 | * @brief Returns if a command is obsolete or not 18 | */ 19 | bool Command::isObsolete() const 20 | { 21 | return m_isObsolete; 22 | } 23 | 24 | void Command::setMacroID(unsigned int macroID) 25 | { 26 | m_macroID = macroID; 27 | } 28 | 29 | unsigned int Command::macroID() const 30 | { 31 | return m_macroID; 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/clang-format-check.yml: -------------------------------------------------------------------------------- 1 | name: clang-format check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '*.md' 9 | - 'docs/**' 10 | pull_request: 11 | types: [ opened, synchronize ] 12 | paths-ignore: 13 | - '*.md' 14 | - 'docs/**' 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | formatting-check: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Formatting Check (Source) 30 | uses: jidicula/clang-format-action@v4.11.0 31 | with: 32 | clang-format-version: '13' 33 | check-path: 'source' 34 | fallback-style: 'webkit' 35 | -------------------------------------------------------------------------------- /source/d1formats/d1amp.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "dialogs/openasdialog.h" 7 | 8 | class D1Amp : public QObject { 9 | Q_OBJECT 10 | 11 | public: 12 | D1Amp() = default; 13 | ~D1Amp() = default; 14 | 15 | bool load(QString filePath, int tileCount, const OpenAsParam ¶ms); 16 | bool save(const QString &gfxPath); 17 | 18 | bool isModified() const; 19 | QString getFilePath(); 20 | quint8 getTileType(quint16); 21 | quint8 getTileProperties(quint16); 22 | void setTileType(quint16 tileIndex, quint8 value); 23 | void setTileProperties(quint16 tileIndex, quint8 value); 24 | void createTile(); 25 | void removeTile(int tileIndex); 26 | 27 | private: 28 | bool modified; 29 | QString ampFilePath; 30 | QList types; 31 | QList properties; 32 | }; 33 | -------------------------------------------------------------------------------- /source/widgets/leveltabframewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Ui { 6 | class LevelTabFrameWidget; 7 | } // namespace Ui 8 | 9 | class LevelCelView; 10 | class D1Gfx; 11 | class D1GfxFrame; 12 | enum class D1CEL_FRAME_TYPE; 13 | 14 | class LevelTabFrameWidget : public QWidget { 15 | Q_OBJECT 16 | 17 | public: 18 | explicit LevelTabFrameWidget(); 19 | ~LevelTabFrameWidget(); 20 | 21 | void initialize(LevelCelView *v, D1Gfx *g); 22 | void update(); 23 | 24 | static D1CEL_FRAME_TYPE altFrameType(D1GfxFrame *frame, int *limit); 25 | static void selectFrameType(D1GfxFrame *frame); 26 | 27 | private slots: 28 | void on_frameTypeComboBox_activated(int index); 29 | 30 | private: 31 | void validate(); 32 | 33 | Ui::LevelTabFrameWidget *ui; 34 | LevelCelView *levelCelView; 35 | D1Gfx *gfx; 36 | }; 37 | -------------------------------------------------------------------------------- /source/d1formats/d1sol.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class D1Sol : public QObject { 9 | Q_OBJECT 10 | 11 | public: 12 | D1Sol() = default; 13 | ~D1Sol() = default; 14 | 15 | bool load(QString filePath); 16 | bool save(const QString &gfxPath); 17 | 18 | void insertSubtile(int subtileIndex, quint8 value); 19 | void createSubtile(); 20 | void removeSubtile(int subtileIndex); 21 | void remapSubtiles(const QMap &remap); 22 | 23 | bool isModified() const; 24 | QString getFilePath(); 25 | quint16 getSubtileCount(); 26 | quint8 getSubtileProperties(int subtileIndex); 27 | void setSubtileProperties(int subtileIndex, quint8 value); 28 | 29 | private: 30 | bool modified; 31 | QString solFilePath; 32 | QList subProperties; 33 | }; 34 | -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "config/config.h" 6 | #include "mainwindow.h" 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | QApplication a(argc, argv); 11 | 12 | Config::loadConfiguration(); 13 | 14 | { // load style-sheet 15 | const char *qssName = ":/D1GraphicsTool.qss"; 16 | QFile file(qssName); 17 | if (!file.open(QIODevice::ReadOnly)) { 18 | qDebug() << "Failed to open " << qssName; 19 | return -1; 20 | } 21 | QString styleSheet = QTextStream(&file).readAll(); 22 | a.setStyleSheet(styleSheet); 23 | } 24 | 25 | int result; 26 | { // run the application 27 | MainWindow w; 28 | w.show(); 29 | 30 | result = a.exec(); 31 | } 32 | 33 | Config::storeConfiguration(); 34 | 35 | return result; 36 | } 37 | -------------------------------------------------------------------------------- /source/d1formats/d1til.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "d1min.h" 8 | 9 | #define TILE_WIDTH 2 10 | #define TILE_HEIGHT 2 11 | 12 | class D1Til : public QObject { 13 | Q_OBJECT 14 | 15 | public: 16 | D1Til() = default; 17 | ~D1Til() = default; 18 | 19 | bool load(QString filePath, D1Min *min); 20 | bool save(const QString &gfxPath); 21 | 22 | QImage getTileImage(int tileIndex); 23 | QImage getFlatTileImage(int tileIndex); 24 | void insertTile(int tileIndex, const QList &subtileIndices); 25 | void createTile(); 26 | void removeTile(int tileIndex); 27 | 28 | bool isModified() const; 29 | QString getFilePath(); 30 | int getTileCount(); 31 | QList &getSubtileIndices(int tileIndex); 32 | 33 | private: 34 | bool modified; 35 | QString tilFilePath; 36 | D1Min *min = nullptr; 37 | QList> subtileIndices; 38 | }; 39 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | ### 0.5.0 4 | 1. Implement multi-color selection in palette widgets 5 | 2. Implement undo/redo stack 6 | 3. Show all colors when clicking "Pick" button 7 | 4. Warn about modified files before closing them 8 | 9 | ### 0.6.0 10 | 11 | 12 | ### Add 13 | - Add settings 14 | - Background color? (grey, green, magenta, cyan?) 15 | - Default zoom level (depending on CEL/CL2 type?) 16 | - By default: x2 17 | - Automatic dezoom when opening a bigger image? 18 | - PowerShell release script which takes a Qt build folder as parameter 19 | - Application icon 20 | - PCX support 21 | - GIF support 22 | 23 | ### Change 24 | - Include CEL/CL2 to TRN mapping in the program, especially for monsters. 25 | - Remove CelView and LevelCelView dependencies from PalView by leveraging signals/slots. 26 | - Rewrite level CEL frame type detection to leverage associated MIN file when available. 27 | 28 | ### Fix 29 | - Last color of PAL/TRN not displayed as transparent?? 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | *.slo 3 | *.lo 4 | *.o 5 | *.a 6 | *.la 7 | *.lai 8 | *.so 9 | *.so.* 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | object_script.*.Release 15 | object_script.*.Debug 16 | *_plugin_import.cpp 17 | /.qmake.cache 18 | /.qmake.stash 19 | *.pro.user 20 | *.pro.user.* 21 | *.qbs.user 22 | *.qbs.user.* 23 | *.moc 24 | moc_*.cpp 25 | moc_*.h 26 | qrc_*.cpp 27 | ui_*.h 28 | *.qmlc 29 | *.jsc 30 | Makefile* 31 | *build-* 32 | *.qm 33 | *.prl 34 | 35 | # Qt unit tests 36 | target_wrapper.* 37 | 38 | # QtCreator 39 | *.autosave 40 | 41 | # QtCreator Qml 42 | *.qmlproject.user 43 | *.qmlproject.user.* 44 | 45 | # QtCreator CMake 46 | CMakeLists.txt.user* 47 | 48 | # QtCreator 4.8< compilation database 49 | compile_commands.json 50 | 51 | # QtCreator local machine specific files for imported projects 52 | *creator.user* 53 | *pro.user 54 | 55 | # Temp release folder 56 | releases/temp/* 57 | *.zip 58 | /debian/opt/d1-graphics-tool/icon.svg 59 | /debian/usr/bin/D1GraphicsTool 60 | /source/D1GraphicsTool 61 | /source/D1GraphicsTool.config.json 62 | 63 | /build 64 | -------------------------------------------------------------------------------- /source/widgets/leveltabsubtilewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Ui { 6 | class LevelTabSubTileWidget; 7 | } // namespace Ui 8 | 9 | class LevelCelView; 10 | class D1Gfx; 11 | class D1Min; 12 | class D1Sol; 13 | 14 | class LevelTabSubTileWidget : public QWidget { 15 | Q_OBJECT 16 | 17 | public: 18 | explicit LevelTabSubTileWidget(); 19 | ~LevelTabSubTileWidget(); 20 | 21 | void initialize(LevelCelView *v, D1Gfx *g, D1Min *m, D1Sol *s); 22 | void update(); 23 | 24 | private slots: 25 | void on_sol0_clicked(); 26 | void on_sol1_clicked(); 27 | void on_sol2_clicked(); 28 | void on_sol3_clicked(); 29 | void on_sol4_clicked(); 30 | void on_sol5_clicked(); 31 | void on_sol7_clicked(); 32 | 33 | private: 34 | void updateSolProperty(); 35 | quint8 readSol(); 36 | 37 | Ui::LevelTabSubTileWidget *ui; 38 | LevelCelView *levelCelView; 39 | D1Gfx *gfx; 40 | D1Min *min; 41 | D1Sol *sol; 42 | 43 | bool onUpdate = false; 44 | int lastSubtileIndex = -1; 45 | int lastFrameEntryIndex = 0; 46 | }; 47 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Qt 19 | uses: jurplel/install-qt-action@v3 20 | with: 21 | version: '6.2.4' 22 | cache: 'true' 23 | 24 | - name: Configure 25 | run: cmake -S. -Bbuild 26 | 27 | - name: Make 28 | run: cmake --build build --config Release -j $(nproc) 29 | 30 | - name: Package 31 | run: | 32 | mkdir dist 33 | copy build\Release\D1GraphicsTool.exe dist 34 | cd dist 35 | windeployqt D1GraphicsTool.exe --no-compiler-runtime --no-opengl-sw --no-system-d3d-compiler --no-virtualkeyboard --no-translations --no-quick-import 36 | shell: cmd 37 | 38 | - name: Upload 39 | if: ${{ !env.ACT }} 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: D1GraphicsTool-Windows-x64 43 | path: dist 44 | -------------------------------------------------------------------------------- /.github/workflows/windows-32.yml: -------------------------------------------------------------------------------- 1 | name: Windows-x86 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Install Qt 19 | uses: jurplel/install-qt-action@v3 20 | with: 21 | version: '5.15.2' 22 | arch: 'win32_msvc2019' 23 | 24 | - name: Configure 25 | run: cmake -S. -Bbuild -A Win32 26 | 27 | - name: Make 28 | run: cmake --build build --config Release -j $(nproc) 29 | 30 | - name: Package 31 | run: | 32 | mkdir dist 33 | copy build\Release\D1GraphicsTool.exe dist 34 | cd dist 35 | windeployqt D1GraphicsTool.exe --no-compiler-runtime --no-opengl-sw --no-system-d3d-compiler --no-virtualkeyboard --no-translations --no-quick-import 36 | shell: cmd 37 | 38 | - name: Upload 39 | if: ${{ !env.ACT }} 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: D1GraphicsTool-Windows-x86 43 | path: dist 44 | -------------------------------------------------------------------------------- /source/undostack/undostack.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.h" 4 | #include "undomacro.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | enum OperationType { 13 | Undo, 14 | Redo 15 | }; 16 | 17 | class UndoStack : public QObject { 18 | Q_OBJECT 19 | 20 | public: 21 | UndoStack() = default; 22 | ~UndoStack() = default; 23 | 24 | void push(std::unique_ptr cmd); 25 | 26 | void undo(); 27 | void redo(); 28 | [[nodiscard]] bool canUndo() const; 29 | [[nodiscard]] bool canRedo() const; 30 | 31 | void clear(); 32 | void addMacro(UndoMacroFactory ¯oFactory); 33 | 34 | signals: 35 | void updateWidget(bool &userCancelled); 36 | void initializeWidget(std::unique_ptr &userData, enum OperationType opType); 37 | 38 | private: 39 | bool m_canUndo = false; 40 | bool m_canRedo = false; 41 | int8_t m_undoPos { -1 }; 42 | std::vector> m_cmds; // holds all the commands on the stack 43 | std::vector m_macros; 44 | 45 | void eraseRedundantCmds(); 46 | }; 47 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] 12 | # Visual C++ Code Style settings 13 | cpp_generate_documentation_comments = doxygen_slash_star 14 | 15 | [*.ui] 16 | indent_style = space 17 | indent_size = 1 18 | end_of_line = lf 19 | 20 | [*.{qrc,rc,ps1}] 21 | indent_style = space 22 | indent_size = 4 23 | end_of_line = lf 24 | 25 | [*.{yml,qss}] 26 | indent_style = space 27 | indent_size = 2 28 | end_of_line = lf 29 | 30 | [*.sh] 31 | end_of_line = lf 32 | 33 | [.clang-format] 34 | end_of_line = lf 35 | 36 | [.gitignore] 37 | end_of_line = lf 38 | 39 | [*.cmake] 40 | indent_style = space 41 | indent_size = 2 42 | 43 | [*.md] 44 | indent_style = space 45 | indent_size = 2 46 | end_of_line = crlf 47 | 48 | [*.txt] 49 | end_of_line = crlf 50 | 51 | [AppRun] 52 | end_of_line = lf 53 | 54 | [{CMakeLists.txt,CMakeSettings.json}] 55 | indent_style = space 56 | indent_size = 2 57 | end_of_line = crlf 58 | 59 | [control] 60 | end_of_line = lf 61 | -------------------------------------------------------------------------------- /source/d1formats/d1trn.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "palette/d1pal.h" 6 | 7 | #define D1TRN_TRANSLATIONS 256 8 | 9 | class D1Trn final : public D1Pal { 10 | Q_OBJECT 11 | 12 | public: 13 | static constexpr const char *IDENTITY_PATH = ":/null.trn"; 14 | static constexpr const char *IDENTITY_NAME = "_null.trn"; 15 | 16 | D1Trn() = default; 17 | D1Trn(D1Pal *pal); 18 | ~D1Trn() = default; 19 | 20 | bool load(QString filepath) override; 21 | bool save(QString filepath) override; 22 | 23 | [[nodiscard]] bool isModified() const override; 24 | 25 | void refreshResultingPalette(); 26 | QColor getResultingColor(quint8); 27 | 28 | QString getFilePath() override; 29 | 30 | [[nodiscard]] QString getDefaultPath() const override; 31 | [[nodiscard]] QString getDefaultName() const override; 32 | 33 | quint8 getTranslation(quint8); 34 | void setTranslation(quint8, quint8); 35 | void setPalette(D1Pal *pal); 36 | D1Pal *getResultingPalette(); 37 | 38 | private: 39 | QString trnFilePath; 40 | bool modified; 41 | quint8 translations[D1TRN_TRANSLATIONS]; 42 | D1Pal *palette = nullptr; 43 | D1Pal resultingPalette; 44 | }; 45 | -------------------------------------------------------------------------------- /source/widgets/leveltabtilewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Ui { 6 | class LevelTabTileWidget; 7 | } // namespace Ui 8 | 9 | class LevelCelView; 10 | class D1Til; 11 | class D1Min; 12 | class D1Amp; 13 | 14 | class LevelTabTileWidget : public QWidget { 15 | Q_OBJECT 16 | 17 | public: 18 | explicit LevelTabTileWidget(); 19 | ~LevelTabTileWidget(); 20 | 21 | void initialize(LevelCelView *v, D1Til *t, D1Min *m, D1Amp *a); 22 | void update(); 23 | 24 | private slots: 25 | void on_ampTypeComboBox_activated(int index); 26 | 27 | void on_amp0_clicked(); 28 | void on_amp1_clicked(); 29 | void on_amp2_clicked(); 30 | void on_amp3_clicked(); 31 | void on_amp4_clicked(); 32 | void on_amp5_clicked(); 33 | void on_amp6_clicked(); 34 | void on_amp7_clicked(); 35 | 36 | private: 37 | void updateAmpType(); 38 | void updateAmpProperty(); 39 | quint8 readAmpType(); 40 | quint8 readAmpProperty(); 41 | 42 | Ui::LevelTabTileWidget *ui; 43 | LevelCelView *levelCelView; 44 | D1Til *til; 45 | D1Min *min; 46 | D1Amp *amp; 47 | 48 | bool onUpdate = false; 49 | int lastTileIndex = -1; 50 | int lastSubTileEntryIndex = 0; 51 | }; 52 | -------------------------------------------------------------------------------- /source/palette/d1palhits.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "d1formats/d1gfx.h" 6 | #include "d1formats/d1min.h" 7 | #include "d1formats/d1til.h" 8 | 9 | enum class D1PALHITS_MODE { 10 | ALL_COLORS, 11 | ALL_FRAMES, 12 | CURRENT_TILE, 13 | CURRENT_SUBTILE, 14 | CURRENT_FRAME 15 | }; 16 | 17 | class D1PalHits : public QObject { 18 | Q_OBJECT 19 | 20 | public: 21 | D1PalHits(D1Gfx *g, D1Min *m = nullptr, D1Til *t = nullptr); 22 | 23 | void update(); 24 | 25 | D1PALHITS_MODE getMode() const; 26 | void setMode(D1PALHITS_MODE m); 27 | 28 | // Returns the number of hits for a specific index 29 | int getIndexHits(quint8 colorIndex, int itemIndex) const; 30 | 31 | private: 32 | void buildPalHits(); 33 | void buildSubtilePalHits(); 34 | void buildTilePalHits(); 35 | 36 | private: 37 | D1PALHITS_MODE mode = D1PALHITS_MODE::ALL_COLORS; 38 | 39 | D1Gfx *gfx; 40 | D1Min *min; 41 | D1Til *til; 42 | 43 | // Palette hits are stored with a palette index key and a hit count value 44 | QMap allFramesPalHits; 45 | QMap> framePalHits; 46 | QMap> tilePalHits; 47 | QMap> subtilePalHits; 48 | }; 49 | -------------------------------------------------------------------------------- /source/d1formats/d1min.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "d1celtilesetframe.h" 11 | #include "d1gfx.h" 12 | #include "d1sol.h" 13 | 14 | class D1Min : public QObject { 15 | Q_OBJECT 16 | 17 | public: 18 | D1Min() = default; 19 | ~D1Min() = default; 20 | 21 | bool load(QString minFilePath, D1Gfx *gfx, D1Sol *sol, std::map &celFrameTypes, const OpenAsParam ¶ms); 22 | bool save(const QString &gfxPath); 23 | 24 | QImage getSubtileImage(int subtileIndex); 25 | 26 | void insertSubtile(int subtileIndex, const QList &frameIndicesList); 27 | void createSubtile(); 28 | void removeSubtile(int subtileIndex); 29 | void remapSubtiles(const QMap &remap); 30 | 31 | bool isModified() const; 32 | QString getFilePath(); 33 | int getSubtileCount(); 34 | quint16 getSubtileWidth(); 35 | quint16 getSubtileHeight(); 36 | void setSubtileHeight(int height); 37 | QList &getCelFrameIndices(int subtileIndex); 38 | 39 | private: 40 | bool modified; 41 | QString minFilePath; 42 | D1Gfx *gfx = nullptr; 43 | quint8 subtileWidth; 44 | quint8 subtileHeight; 45 | QList> celFrameIndices; 46 | }; 47 | -------------------------------------------------------------------------------- /source/palette/d1pal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define D1PAL_COLORS 256 9 | #define D1PAL_COLOR_BITS 8 10 | #define D1PAL_SIZE_BYTES 768 11 | 12 | enum class D1PAL_TYPE { 13 | REGULAR, 14 | JASC 15 | }; 16 | 17 | enum class D1PAL_CYCLE_TYPE { 18 | CAVES, 19 | HELL, 20 | CRYPT, 21 | NEST, 22 | }; 23 | 24 | class D1Pal : public QObject { 25 | Q_OBJECT 26 | 27 | public: 28 | static constexpr const char *DEFAULT_PATH = ":/default.pal"; 29 | static constexpr const char *DEFAULT_NAME = "_default.pal"; 30 | 31 | D1Pal() = default; 32 | ~D1Pal() override = default; 33 | 34 | virtual bool load(QString); 35 | virtual bool save(QString); 36 | 37 | [[nodiscard]] virtual bool isModified() const; 38 | 39 | virtual QString getFilePath(); 40 | 41 | [[nodiscard]] virtual QString getDefaultPath() const; 42 | [[nodiscard]] virtual QString getDefaultName() const; 43 | 44 | QColor getColor(quint8); 45 | void setColor(quint8, QColor); 46 | 47 | void resetColors(); 48 | void cycleColors(D1PAL_CYCLE_TYPE type); 49 | 50 | private: 51 | void loadRegularPalette(QFile &file); 52 | bool loadJascPalette(QFile &file); 53 | 54 | private: 55 | QString palFilePath; 56 | bool modified; 57 | QColor colors[D1PAL_COLORS]; 58 | quint8 currentCycleCounter = 3; 59 | // buffer to store the original colors in case of color cycling 60 | QColor origCyclePalette[32]; 61 | }; 62 | -------------------------------------------------------------------------------- /source/undostack/framecmds.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "framecmds.h" 6 | #include "mainwindow.h" 7 | 8 | RemoveFrameCommand::RemoveFrameCommand(int currentFrameIndex, const QImage img) 9 | : frameIndexToRevert(currentFrameIndex) 10 | , imgToRevert(img) 11 | { 12 | } 13 | 14 | void RemoveFrameCommand::undo() 15 | { 16 | emit this->inserted(frameIndexToRevert, imgToRevert); 17 | } 18 | 19 | void RemoveFrameCommand::redo() 20 | { 21 | // emit this signal which will call LevelCelView/CelView::removeCurrentFrame 22 | emit this->removed(frameIndexToRevert); 23 | } 24 | 25 | ReplaceFrameCommand::ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore) 26 | : frameIndexToReplace(currentFrameIndex) 27 | , imgToReplace(imgToReplace) 28 | , imgToRestore(imgToRestore) 29 | { 30 | } 31 | 32 | void ReplaceFrameCommand::undo() 33 | { 34 | emit this->undoReplaced(frameIndexToReplace, imgToRestore); 35 | } 36 | 37 | void ReplaceFrameCommand::redo() 38 | { 39 | // emit this signal which will call LevelCelView/CelView::replaceCurrentFrame 40 | emit this->replaced(frameIndexToReplace, imgToReplace); 41 | } 42 | 43 | AddFrameCommand::AddFrameCommand(int index, QImage &img) 44 | : m_index(index) 45 | , m_image(std::move(img)) 46 | { 47 | } 48 | 49 | void AddFrameCommand::undo() 50 | { 51 | emit this->undoAdded(m_index); 52 | } 53 | 54 | void AddFrameCommand::redo() 55 | { 56 | emit this->added(m_index, m_image); 57 | } 58 | -------------------------------------------------------------------------------- /source/dialogs/openasdialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | enum class OPEN_CLIPPED_TYPE { 6 | Auto, 7 | Yes, 8 | No, 9 | }; 10 | 11 | enum class OPEN_TILESET_TYPE { 12 | Auto, 13 | Yes, 14 | No, 15 | }; 16 | 17 | class OpenAsParam { 18 | public: 19 | QString celFilePath; 20 | OPEN_TILESET_TYPE isTileset = OPEN_TILESET_TYPE::Auto; 21 | 22 | quint16 celWidth = 0; 23 | OPEN_CLIPPED_TYPE clipped = OPEN_CLIPPED_TYPE::Auto; 24 | 25 | QString tilFilePath; 26 | QString minFilePath; 27 | QString solFilePath; 28 | QString ampFilePath; 29 | quint16 minWidth = 0; 30 | quint16 minHeight = 0; 31 | }; 32 | 33 | namespace Ui { 34 | class OpenAsDialog; 35 | } 36 | 37 | class OpenAsDialog : public QDialog { 38 | Q_OBJECT 39 | 40 | public: 41 | explicit OpenAsDialog(QWidget *parent = nullptr); 42 | ~OpenAsDialog(); 43 | 44 | void initialize(); 45 | 46 | private: 47 | void update(); 48 | 49 | private slots: 50 | void on_inputFileBrowseButton_clicked(); 51 | void on_isTilesetYesRadioButton_toggled(bool checked); 52 | void on_isTilesetNoRadioButton_toggled(bool checked); 53 | void on_isTilesetAutoRadioButton_toggled(bool checked); 54 | void on_tilFileBrowseButton_clicked(); 55 | void on_minFileBrowseButton_clicked(); 56 | void on_solFileBrowseButton_clicked(); 57 | void on_ampFileBrowseButton_clicked(); 58 | void on_openButton_clicked(); 59 | void on_openCancelButton_clicked(); 60 | 61 | private: 62 | Ui::OpenAsDialog *ui; 63 | }; 64 | -------------------------------------------------------------------------------- /source/dialogs/exportdialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "d1formats/d1amp.h" 7 | #include "d1formats/d1gfx.h" 8 | #include "d1formats/d1min.h" 9 | #include "d1formats/d1sol.h" 10 | #include "d1formats/d1til.h" 11 | 12 | namespace Ui { 13 | class ExportDialog; 14 | } 15 | 16 | // subtiles per line if the output is groupped, an odd number to ensure it is not recognized as a flat tile 17 | #define EXPORT_SUBTILES_PER_LINE 15 18 | 19 | // frames per line if the output of a tileset-frames is groupped, an odd number to ensure it is not recognized as a flat tile or as subtiles 20 | #define EXPORT_LVLFRAMES_PER_LINE 31 21 | 22 | class ExportDialog : public QDialog { 23 | Q_OBJECT 24 | 25 | public: 26 | explicit ExportDialog(QWidget *parent = nullptr); 27 | ~ExportDialog(); 28 | 29 | void initialize(D1Gfx *gfx, D1Min *min, D1Til *til, D1Sol *sol, D1Amp *amp); 30 | 31 | private slots: 32 | void on_outputFolderBrowseButton_clicked(); 33 | void on_exportButton_clicked(); 34 | void on_exportCancelButton_clicked(); 35 | 36 | private: 37 | QString getFileFormatExtension(); 38 | 39 | bool exportLevelTiles25D(QProgressDialog &progress); 40 | bool exportLevelTiles(QProgressDialog &progress); 41 | bool exportLevelSubtiles(QProgressDialog &progress); 42 | bool exportFrames(QProgressDialog &progress); 43 | 44 | Ui::ExportDialog *ui; 45 | 46 | D1Gfx *gfx = nullptr; 47 | D1Min *min = nullptr; 48 | D1Til *til = nullptr; 49 | D1Sol *sol = nullptr; 50 | D1Amp *amp = nullptr; 51 | 52 | QString outputFolder; 53 | }; 54 | -------------------------------------------------------------------------------- /source/undostack/framecmds.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.h" 4 | #include "views/celview.h" 5 | 6 | #include 7 | 8 | class RemoveFrameCommand : public QObject, public Command { 9 | Q_OBJECT 10 | 11 | public: 12 | explicit RemoveFrameCommand(int currentFrameIndex, const QImage img); 13 | ~RemoveFrameCommand() = default; 14 | 15 | void undo() override; 16 | void redo() override; 17 | 18 | signals: 19 | void removed(int idxToRemove); 20 | void inserted(int idxToRestore, const QImage imgToRestore); 21 | 22 | private: 23 | QImage imgToRevert; 24 | int frameIndexToRevert = 0; 25 | }; 26 | 27 | class ReplaceFrameCommand : public QObject, public Command { 28 | Q_OBJECT 29 | 30 | public: 31 | explicit ReplaceFrameCommand(int currentFrameIndex, const QImage imgToReplace, const QImage imgToRestore); 32 | ~ReplaceFrameCommand() = default; 33 | 34 | void undo() override; 35 | void redo() override; 36 | 37 | signals: 38 | void undoReplaced(int idxToRemove, const QImage imgToRestore); 39 | void replaced(int idxToReplace, const QImage imgToReplace); 40 | 41 | private: 42 | QImage imgToReplace; 43 | QImage imgToRestore; 44 | int frameIndexToReplace = 0; 45 | }; 46 | 47 | class AddFrameCommand : public QObject, public Command { 48 | Q_OBJECT 49 | 50 | public: 51 | explicit AddFrameCommand(int index, QImage &image); 52 | ~AddFrameCommand() = default; 53 | 54 | void undo() override; 55 | void redo() override; 56 | 57 | signals: 58 | void undoAdded(int index); 59 | void added(int index, const QImage &image); 60 | 61 | private: 62 | QImage m_image; 63 | int m_index = 0; 64 | }; 65 | -------------------------------------------------------------------------------- /source/d1formats/d1image.cpp: -------------------------------------------------------------------------------- 1 | #include "d1image.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | static quint8 getPalColor(D1Pal *pal, QColor color) 10 | { 11 | int res = 0; 12 | int best = INT_MAX; 13 | 14 | for (int i = 0; i < D1PAL_COLORS; i++) { 15 | if (i == 1 && pal->getFilePath() == D1Pal::DEFAULT_PATH) { 16 | i = 128; // skip indices between 1 and 127 from the default palette 17 | } 18 | QColor palColor = pal->getColor(i); 19 | int currR = color.red() - palColor.red(); 20 | int currG = color.green() - palColor.green(); 21 | int currB = color.blue() - palColor.blue(); 22 | int curr = currR * currR + currG * currG + currB * currB; 23 | if (curr < best) { 24 | best = curr; 25 | res = i; 26 | } 27 | } 28 | 29 | return res; 30 | } 31 | 32 | bool D1ImageFrame::load(D1GfxFrame &frame, const QImage &image, D1Pal *pal) 33 | { 34 | frame.width = image.width(); 35 | frame.height = image.height(); 36 | 37 | frame.pixels.clear(); 38 | 39 | for (int y = 0; y < frame.height; y++) { 40 | QList pixelLine; 41 | for (int x = 0; x < frame.width; x++) { 42 | QColor color = image.pixelColor(x, y); 43 | // if (color == QColor(Qt::transparent)) { 44 | if (color.alpha() < COLOR_ALPHA_LIMIT) { 45 | pixelLine.append(D1GfxPixel::transparentPixel()); 46 | } else { 47 | pixelLine.append(D1GfxPixel::colorPixel(getPalColor(pal, color))); 48 | } 49 | } 50 | frame.pixels.append(pixelLine); 51 | } 52 | 53 | return true; 54 | } 55 | -------------------------------------------------------------------------------- /source/views/view.cpp: -------------------------------------------------------------------------------- 1 | #include "view.h" 2 | #include "mainwindow.h" 3 | 4 | View::View(QWidget *parent) 5 | : QGraphicsView(parent) 6 | { 7 | this->setMouseTracking(true); 8 | } 9 | 10 | void View::mousePressEvent(QMouseEvent *event) 11 | { 12 | switch (event->button()) { 13 | case Qt::LeftButton: { 14 | // left out for left mouse button events 15 | QGraphicsView::mousePressEvent(event); 16 | break; 17 | } 18 | case Qt::MiddleButton: { 19 | this->setDragMode(QGraphicsView::ScrollHandDrag); 20 | 21 | // after middle button has been pressed - send the mouse press event to base 22 | // class that holds this scene, since it will toggle on dragging on ScrollHandDrag 23 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 24 | QMouseEvent *pressEvent = new QMouseEvent(QEvent::MouseButtonPress, 25 | event->pos(), event->globalPosition().toPoint(), Qt::MouseButton::LeftButton, 26 | Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier); 27 | #else 28 | QMouseEvent *pressEvent = new QMouseEvent(QEvent::MouseButtonPress, 29 | event->pos(), event->globalPos(), Qt::MouseButton::LeftButton, 30 | Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier); 31 | #endif 32 | 33 | QGraphicsView::mousePressEvent(pressEvent); 34 | break; 35 | } 36 | default: { 37 | QGraphicsView::mousePressEvent(event); 38 | break; 39 | } 40 | } 41 | } 42 | 43 | void View::mouseReleaseEvent(QMouseEvent *event) 44 | { 45 | switch (event->button()) { 46 | case Qt::LeftButton: { 47 | break; 48 | } 49 | case Qt::MiddleButton: { 50 | this->setDragMode(QGraphicsView::NoDrag); 51 | break; 52 | } 53 | } 54 | } 55 | 56 | void View::leaveEvent(QEvent *event) 57 | { 58 | dynamic_cast(this->window())->updateStatusBar("", "color: rgb(0, 0, 0);"); 59 | } 60 | -------------------------------------------------------------------------------- /source/undostack/undomacro.cpp: -------------------------------------------------------------------------------- 1 | #include "undomacro.h" 2 | 3 | #include 4 | 5 | UserData::UserData(QString labelText, QString cancelButtonText, std::pair &&minMax) 6 | : m_labelText(std::move(labelText)) 7 | , m_cancelButtonText(std::move(cancelButtonText)) 8 | , m_minMax(minMax) 9 | { 10 | } 11 | 12 | UserData::UserData(QString labelText, QString cancelButtonText) 13 | : m_labelText(std::move(labelText)) 14 | , m_cancelButtonText(std::move(cancelButtonText)) 15 | , m_minMax({ 0, 0 }) 16 | { 17 | } 18 | 19 | UndoMacroFactory::UndoMacroFactory(UserData &&userData) 20 | : m_userData(std::make_unique(userData.labelText(), userData.cancelButtonText(), std::make_pair(userData.min(), userData.max()))) 21 | { 22 | } 23 | 24 | void UndoMacroFactory::setUserData(const UserData &&userData) 25 | { 26 | m_userData = std::make_unique(userData.labelText(), userData.cancelButtonText(), std::make_pair(userData.min(), userData.max())); 27 | } 28 | 29 | void UndoMacroFactory::add(std::unique_ptr cmd) 30 | { 31 | m_commands.push_back(std::move(cmd)); 32 | } 33 | 34 | UndoMacro::UndoMacro(std::unique_ptr userData, std::pair rangeIdxs) 35 | : m_userData(std::move(userData)) 36 | , m_rangeIdxs(rangeIdxs) 37 | { 38 | } 39 | 40 | UndoMacro::UndoMacro(UndoMacro &&undoMacro) noexcept 41 | : m_userData(std::move(undoMacro.m_userData)) 42 | , m_rangeIdxs(undoMacro.m_rangeIdxs) 43 | { 44 | } 45 | 46 | UndoMacro &UndoMacro::operator=(UndoMacro &&undoMacro) noexcept 47 | { 48 | m_userData = std::move(undoMacro.m_userData); 49 | m_rangeIdxs = undoMacro.m_rangeIdxs; 50 | return *this; 51 | } 52 | 53 | void UndoMacro::setLastIndex(int index) 54 | { 55 | m_rangeIdxs.second = index; 56 | /* TODO: later on, if/when we will implement more types of macros, we could use some update method of UserData 57 | * instead of using setters directly 58 | */ 59 | m_userData->setMax((m_rangeIdxs.second - m_rangeIdxs.first) + 1); 60 | } 61 | -------------------------------------------------------------------------------- /source/d1formats/d1celtilesetframe.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "dialogs/openasdialog.h" 6 | 7 | #define MICRO_WIDTH 32 8 | #define MICRO_HEIGHT 32 9 | 10 | class D1GfxFrame; 11 | 12 | enum class D1CEL_FRAME_TYPE { 13 | Square, // opaque square (bitmap) 14 | TransparentSquare, // bitmap with transparent pixels 15 | LeftTriangle, // opaque triangle on its left edge 16 | RightTriangle, // opaque triangle on its right edge 17 | LeftTrapezoid, // bottom half is a left triangle, upper half is a square 18 | RightTrapezoid, // bottom half is a right triangle, upper half is a square 19 | Empty = -1, // transparent frame (only for efficiency tests) 20 | }; 21 | 22 | class D1CelTilesetFrame { 23 | public: 24 | static bool load(D1GfxFrame &frame, D1CEL_FRAME_TYPE frameType, QByteArray rawData, const OpenAsParam ¶ms); 25 | 26 | static quint8 *writeFrameData(D1GfxFrame &frame, quint8 *pBuf); 27 | 28 | private: 29 | static void LoadSquare(D1GfxFrame &frame, QByteArray &rawData); 30 | static void LoadTransparentSquare(D1GfxFrame &frame, QByteArray &rawData); 31 | static void LoadBottomLeftTriangle(D1GfxFrame &frame, QByteArray &rawData); 32 | static void LoadBottomRightTriangle(D1GfxFrame &frame, QByteArray &rawData); 33 | static void LoadLeftTriangle(D1GfxFrame &frame, QByteArray &rawData); 34 | static void LoadRightTriangle(D1GfxFrame &frame, QByteArray &rawData); 35 | static void LoadTopHalfSquare(D1GfxFrame &frame, QByteArray &rawData); 36 | static void LoadLeftTrapezoid(D1GfxFrame &frame, QByteArray &rawData); 37 | static void LoadRightTrapezoid(D1GfxFrame &frame, QByteArray &rawData); 38 | 39 | static quint8 *WriteSquare(D1GfxFrame &frame, quint8 *pBuf); 40 | static quint8 *WriteTransparentSquare(D1GfxFrame &frame, quint8 *pBuf); 41 | static quint8 *WriteLeftTriangle(D1GfxFrame &frame, quint8 *pBuf); 42 | static quint8 *WriteRightTriangle(D1GfxFrame &frame, quint8 *pBuf); 43 | static quint8 *WriteLeftTrapezoid(D1GfxFrame &frame, quint8 *pBuf); 44 | static quint8 *WriteRightTrapezoid(D1GfxFrame &frame, quint8 *pBuf); 45 | }; 46 | -------------------------------------------------------------------------------- /source/dialogs/settingsdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "settingsdialog.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "config/config.h" 7 | #include "ui_settingsdialog.h" 8 | 9 | SettingsDialog::SettingsDialog(QWidget *parent) 10 | : QDialog(parent) 11 | , ui(new Ui::SettingsDialog) 12 | { 13 | ui->setupUi(this); 14 | } 15 | 16 | SettingsDialog::~SettingsDialog() 17 | { 18 | delete ui; 19 | } 20 | 21 | void SettingsDialog::initialize() 22 | { 23 | QColor palDefaultColor = QColor(Config::value("PaletteDefaultColor").toString()); 24 | this->ui->defaultPaletteColorLineEdit->setText(palDefaultColor.name()); 25 | 26 | QColor palSelectionBorderColor = QColor(Config::value("PaletteSelectionBorderColor").toString()); 27 | this->ui->paletteSelectionBorderColorLineEdit->setText(palSelectionBorderColor.name()); 28 | } 29 | 30 | void SettingsDialog::on_defaultPaletteColorPushButton_clicked() 31 | { 32 | QColor color = QColor(ui->defaultPaletteColorLineEdit->text()); 33 | color = QColorDialog::getColor(color); 34 | 35 | if (color.isValid()) 36 | this->ui->defaultPaletteColorLineEdit->setText(color.name()); 37 | } 38 | 39 | void SettingsDialog::on_paletteSelectionBorderColorPushButton_clicked() 40 | { 41 | QColor color = QColor(ui->paletteSelectionBorderColorLineEdit->text()); 42 | color = QColorDialog::getColor(color); 43 | 44 | if (color.isValid()) 45 | this->ui->paletteSelectionBorderColorLineEdit->setText(color.name()); 46 | } 47 | 48 | void SettingsDialog::on_settingsOkButton_clicked() 49 | { 50 | // PaletteDefaultColor 51 | QColor palDefaultColor = QColor(ui->defaultPaletteColorLineEdit->text()); 52 | Config::insert("PaletteDefaultColor", palDefaultColor.name()); 53 | 54 | // PaletteSelectionBorderColor 55 | QColor palSelectionBorderColor = QColor(ui->paletteSelectionBorderColorLineEdit->text()); 56 | Config::insert("PaletteSelectionBorderColor", palSelectionBorderColor.name()); 57 | 58 | Config::storeConfiguration(); 59 | 60 | emit this->configurationSaved(); 61 | 62 | this->close(); 63 | } 64 | 65 | void SettingsDialog::on_settingsCancelButton_clicked() 66 | { 67 | this->close(); 68 | } 69 | -------------------------------------------------------------------------------- /source/undostack/undomacro.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "command.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class UserData { 10 | private: 11 | QString m_labelText; 12 | QString m_cancelButtonText; 13 | std::pair m_minMax; 14 | 15 | public: 16 | UserData(QString labelText, QString cancelButtonText, std::pair &&minMax); 17 | UserData(QString labelText, QString cancelButtonText); 18 | ~UserData() = default; 19 | 20 | [[nodiscard]] int min() const 21 | { 22 | return m_minMax.first; 23 | } 24 | [[nodiscard]] int max() const 25 | { 26 | return m_minMax.second; 27 | } 28 | [[nodiscard]] const QString &labelText() const 29 | { 30 | return m_labelText; 31 | } 32 | [[nodiscard]] const QString &cancelButtonText() const 33 | { 34 | return m_cancelButtonText; 35 | } 36 | void setMin(int min) 37 | { 38 | m_minMax.first = min; 39 | } 40 | void setMax(int max) 41 | { 42 | m_minMax.second = max; 43 | } 44 | }; 45 | 46 | class UndoMacroFactory { 47 | private: 48 | std::unique_ptr m_userData; 49 | std::vector> m_commands; 50 | 51 | public: 52 | UndoMacroFactory(UserData &&userData); 53 | UndoMacroFactory() = default; 54 | ~UndoMacroFactory() = default; 55 | 56 | void setUserData(const UserData &&userData); 57 | 58 | void add(std::unique_ptr cmd); 59 | [[nodiscard]] std::vector> &cmds() 60 | { 61 | return m_commands; 62 | }; 63 | [[nodiscard]] std::unique_ptr &userdata() 64 | { 65 | return m_userData; 66 | }; 67 | }; 68 | 69 | class UndoMacro { 70 | private: 71 | std::unique_ptr m_userData; 72 | std::pair m_rangeIdxs; 73 | 74 | public: 75 | UndoMacro(std::unique_ptr userData, std::pair rangeIdxs); 76 | UndoMacro(UndoMacro &&undoMacro) noexcept; 77 | UndoMacro &operator=(UndoMacro &&undoMacro) noexcept; 78 | ~UndoMacro() = default; 79 | 80 | [[nodiscard]] std::unique_ptr &userdata() 81 | { 82 | return m_userData; 83 | }; 84 | [[nodiscard]] int beginIndex() const 85 | { 86 | return m_rangeIdxs.first; 87 | } 88 | [[nodiscard]] int lastIndex() const 89 | { 90 | return m_rangeIdxs.second; 91 | } 92 | void setLastIndex(int index); 93 | }; 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diablo 1 Graphics Tool 2 | [![Discord](https://img.shields.io/discord/830522505605283862.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48)](https://discord.gg/devilutionx) 3 | [![Downloads](https://img.shields.io/github/downloads/diasurgical/d1-graphics-tool/total.svg)](https://github.com/diasurgical/d1-graphics-tool/releases) 4 | [![Downloads](https://img.shields.io/github/v/release/diasurgical/d1-graphics-tool)](https://github.com/diasurgical/d1-graphics-tool/releases) 5 | [![Downloads](https://img.shields.io/github/downloads-pre/diasurgical/d1-graphics-tool/latest/total)](https://github.com/diasurgical/d1-graphics-tool/releases) 6 | [![Windows-x64](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/windows.yml/badge.svg)](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/windows.yml) 7 | [![Windows-x86](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/windows-32.yml/badge.svg)](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/windows-32.yml) 8 | [![Linux](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/linux.yml/badge.svg)](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/linux.yml) 9 | [![Linux](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/linux-qt5.yml/badge.svg)](https://github.com/diasurgical/d1-graphics-tool/actions/workflows/linux-qt5.yml) 10 | [![CodeFactor](https://www.codefactor.io/repository/github/diasurgical/d1-graphics-tool/badge)](https://www.codefactor.io/repository/github/diasurgical/d1-graphics-tool) 11 | 12 | Diablo 1 Graphics Tool can display and edit Diablo 1 assets (CEL/CL2 graphics files and PAL/TRN/TIL/MIN/SOL/AMP metadata). 13 | 14 | ![Screenshot 1](/resources/demo001.gif) 15 | 16 | This tool is cross-platform, it can be compiled on Windows, Linux, or macOS (using the Qt5/6 Framework). 17 | Go to the [releases page](https://github.com/diasurgical/d1-graphics-tool/releases) to download the latest build. 18 | 19 | ## Features 20 | - Regular, level and compiled CEL support with animation. 21 | - Mono and multi-group CL2 support with animation. 22 | - PAL/TRN support. 23 | - Palette hits and color translation hits display. 24 | - Exporting to (multiple) image files (PNG, BMP, JPG, etc...). 25 | 26 | For a full list of features and changes see our [changelog](CHANGELOG.md). 27 | 28 | ## Acknowledgements 29 | I would like to thank anyone who contributed to the development, bug reporting or testing of this tool. 30 | 31 | # Legal 32 | 33 | Diablo 1 Graphics Tool is made publicly available and released under the Sustainable Use License (see [LICENSE](LICENSE.md)) 34 | -------------------------------------------------------------------------------- /source/d1formats/d1trn.cpp: -------------------------------------------------------------------------------- 1 | #include "d1trn.h" 2 | 3 | D1Trn::D1Trn(D1Pal *pal) 4 | : palette(pal) 5 | { 6 | } 7 | 8 | bool D1Trn::load(QString filePath) 9 | { 10 | if (this->palette == nullptr) 11 | return false; 12 | 13 | QFile file = QFile(filePath); 14 | 15 | if (!file.open(QIODevice::ReadOnly)) 16 | return false; 17 | 18 | if (file.size() != D1TRN_TRANSLATIONS) 19 | return false; 20 | 21 | int readBytes = file.read((char *)this->translations, D1TRN_TRANSLATIONS); 22 | if (readBytes != D1TRN_TRANSLATIONS) 23 | return false; 24 | 25 | this->refreshResultingPalette(); 26 | 27 | this->trnFilePath = filePath; 28 | this->modified = false; 29 | return true; 30 | } 31 | 32 | bool D1Trn::save(QString filePath) 33 | { 34 | QFile file = QFile(filePath); 35 | 36 | if (!file.open(QIODevice::WriteOnly)) 37 | return false; 38 | 39 | int outBytes = file.write((char *)this->translations, D1TRN_TRANSLATIONS); 40 | if (outBytes != D1TRN_TRANSLATIONS) 41 | return false; 42 | 43 | if (this->trnFilePath == filePath) { 44 | this->modified = false; 45 | } else { 46 | // -- do not update, the user is creating a new one and the original needs to be preserved 47 | // this->modified = false; 48 | // this->trnFilePath = filePath; 49 | } 50 | return true; 51 | } 52 | 53 | bool D1Trn::isModified() const 54 | { 55 | return this->modified; 56 | } 57 | 58 | void D1Trn::refreshResultingPalette() 59 | { 60 | for (int i = 0; i < D1TRN_TRANSLATIONS; i++) { 61 | this->resultingPalette.setColor( 62 | i, this->palette->getColor(this->translations[i])); 63 | } 64 | } 65 | 66 | QColor D1Trn::getResultingColor(quint8 index) 67 | { 68 | return this->resultingPalette.getColor(index); 69 | } 70 | 71 | QString D1Trn::getFilePath() 72 | { 73 | return this->trnFilePath; 74 | } 75 | 76 | QString D1Trn::getDefaultPath() const 77 | { 78 | return IDENTITY_PATH; 79 | } 80 | 81 | QString D1Trn::getDefaultName() const 82 | { 83 | return IDENTITY_NAME; 84 | } 85 | 86 | quint8 D1Trn::getTranslation(quint8 index) 87 | { 88 | return this->translations[index]; 89 | } 90 | 91 | void D1Trn::setTranslation(quint8 index, quint8 translation) 92 | { 93 | this->translations[index] = translation; 94 | 95 | this->modified = true; 96 | } 97 | 98 | void D1Trn::setPalette(D1Pal *pal) 99 | { 100 | this->palette = pal; 101 | } 102 | 103 | D1Pal *D1Trn::getResultingPalette() 104 | { 105 | return &this->resultingPalette; 106 | } 107 | -------------------------------------------------------------------------------- /source/d1formats/d1gfx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "d1celtilesetframe.h" 10 | #include "palette/d1pal.h" 11 | 12 | // TODO: move these to some persistency class? 13 | #define SUB_HEADER_SIZE 0x0A 14 | #define CEL_BLOCK_HEIGHT 32 15 | 16 | #define SwapLE16(X) qToLittleEndian((quint16)(X)) 17 | #define SwapLE32(X) qToLittleEndian((quint32)(X)) 18 | 19 | class D1GfxPixel { 20 | public: 21 | static D1GfxPixel transparentPixel(); 22 | static D1GfxPixel colorPixel(quint8 color); 23 | 24 | ~D1GfxPixel() = default; 25 | 26 | bool isTransparent() const; 27 | quint8 getPaletteIndex() const; 28 | 29 | friend bool operator==(const D1GfxPixel &lhs, const D1GfxPixel &rhs); 30 | 31 | private: 32 | D1GfxPixel() = default; 33 | 34 | bool transparent = false; 35 | quint8 paletteIndex = 0; 36 | }; 37 | 38 | class D1GfxFrame { 39 | friend class D1Cel; 40 | friend class D1CelFrame; 41 | friend class D1Cl2; 42 | friend class D1Cl2Frame; 43 | friend class D1CelTileset; 44 | friend class D1CelTilesetFrame; 45 | friend class D1ImageFrame; 46 | 47 | public: 48 | D1GfxFrame() = default; 49 | ~D1GfxFrame() = default; 50 | 51 | int getWidth() const; 52 | int getHeight() const; 53 | D1GfxPixel getPixel(int x, int y) const; 54 | D1CEL_FRAME_TYPE getFrameType() const; 55 | void setFrameType(D1CEL_FRAME_TYPE type); 56 | 57 | protected: 58 | int width = 0; 59 | int height = 0; 60 | QList> pixels; 61 | // fields of tileset-frames 62 | D1CEL_FRAME_TYPE frameType = D1CEL_FRAME_TYPE::TransparentSquare; 63 | }; 64 | 65 | class D1Gfx : public QObject { 66 | Q_OBJECT 67 | 68 | friend class D1Cel; 69 | friend class D1Cl2; 70 | friend class D1CelTileset; 71 | 72 | public: 73 | D1Gfx() = default; 74 | ~D1Gfx() = default; 75 | 76 | QImage getFrameImage(quint16 frameIndex); 77 | D1GfxFrame *insertFrame(int frameIdx, const QImage &image); 78 | void insertFrameInGroup(int frameIdx, int groupIdx, const QImage &image); 79 | D1GfxFrame *replaceFrame(int frameIndex, const QImage &image); 80 | std::optional removeFrame(quint16 frameIndex); 81 | void regroupFrames(int count); 82 | void remapFrames(const QMap &remap); 83 | 84 | bool isModified() const; 85 | void setModified(bool isModified); 86 | bool isTileset() const; 87 | bool hasHeader() const; 88 | void setHasHeader(bool hasHeader); 89 | QString getFilePath(); 90 | D1Pal *getPalette(); 91 | void setPalette(D1Pal *pal); 92 | void insertGroup(int groupIdx, int frameIdx, const QImage &image); 93 | int getGroupCount(); 94 | QPair getGroupFrameIndices(int groupIndex); 95 | int getFrameCount(); 96 | D1GfxFrame *getFrame(int frameIndex); 97 | int getFrameWidth(int frameIndex); 98 | int getFrameHeight(int frameIndex); 99 | 100 | protected: 101 | bool modified = false; 102 | bool isTileset_ = false; 103 | bool hasHeader_ = true; 104 | QString gfxFilePath; 105 | D1Pal *palette = nullptr; 106 | QList> groupFrameIndices; 107 | QList frames; 108 | }; 109 | -------------------------------------------------------------------------------- /source/d1formats/d1sol.cpp: -------------------------------------------------------------------------------- 1 | #include "d1sol.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | bool D1Sol::load(QString filePath) 10 | { 11 | // prepare file data source 12 | QFile file; 13 | // done by the caller 14 | // if (!params.solFilePath.isEmpty()) { 15 | // filePath = params.solFilePath; 16 | // } 17 | if (!filePath.isEmpty()) { 18 | file.setFileName(filePath); 19 | if (!file.open(QIODevice::ReadOnly)) { 20 | return false; 21 | } 22 | } 23 | 24 | QByteArray fileData = file.readAll(); 25 | QBuffer fileBuffer(&fileData); 26 | 27 | if (!fileBuffer.open(QIODevice::ReadOnly)) { 28 | return false; 29 | } 30 | 31 | int subTileCount = file.size(); 32 | 33 | this->subProperties.clear(); 34 | 35 | // Read SOL binary data 36 | QDataStream in(&fileBuffer); 37 | in.setByteOrder(QDataStream::LittleEndian); 38 | 39 | for (int i = 0; i < subTileCount; i++) { 40 | quint8 readBytr; 41 | in >> readBytr; 42 | this->subProperties.append(readBytr); 43 | } 44 | 45 | this->solFilePath = filePath; 46 | this->modified = false; 47 | 48 | return true; 49 | } 50 | 51 | bool D1Sol::save(const QString &gfxPath) 52 | { 53 | QString filePath = gfxPath; 54 | filePath.chop(3); 55 | filePath += "sol"; 56 | 57 | QFile outFile = QFile(filePath); 58 | if (!outFile.open(QIODevice::WriteOnly | QFile::Truncate)) { 59 | QMessageBox::critical(nullptr, "Error", "Failed open file: " + filePath); 60 | return false; 61 | } 62 | 63 | // write to file 64 | QDataStream out(&outFile); 65 | for (int i = 0; i < this->subProperties.size(); i++) { 66 | out << this->subProperties[i]; 67 | } 68 | 69 | this->solFilePath = filePath; 70 | this->modified = false; 71 | 72 | return true; 73 | } 74 | 75 | bool D1Sol::isModified() const 76 | { 77 | return this->modified; 78 | } 79 | 80 | QString D1Sol::getFilePath() 81 | { 82 | return this->solFilePath; 83 | } 84 | 85 | quint16 D1Sol::getSubtileCount() 86 | { 87 | return this->subProperties.count(); 88 | } 89 | 90 | quint8 D1Sol::getSubtileProperties(int subtileIndex) 91 | { 92 | if (subtileIndex >= this->subProperties.count()) 93 | return 0; 94 | 95 | return this->subProperties.at(subtileIndex); 96 | } 97 | 98 | void D1Sol::insertSubtile(int subtileIndex, quint8 value) 99 | { 100 | this->subProperties.insert(subtileIndex, value); 101 | this->modified = true; 102 | } 103 | 104 | void D1Sol::setSubtileProperties(int subtileIndex, quint8 value) 105 | { 106 | this->subProperties[subtileIndex] = value; 107 | this->modified = true; 108 | } 109 | 110 | void D1Sol::createSubtile() 111 | { 112 | this->subProperties.append(0); 113 | this->modified = true; 114 | } 115 | 116 | void D1Sol::removeSubtile(int subtileIndex) 117 | { 118 | this->subProperties.removeAt(subtileIndex); 119 | this->modified = true; 120 | } 121 | 122 | void D1Sol::remapSubtiles(const QMap &remap) 123 | { 124 | QList newSubProperties; 125 | 126 | for (auto iter = remap.cbegin(); iter != remap.cend(); ++iter) { 127 | newSubProperties.append(this->subProperties.at(iter.value())); 128 | } 129 | this->subProperties.swap(newSubProperties); 130 | this->modified = true; 131 | } 132 | -------------------------------------------------------------------------------- /source/dialogs/importdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "importdialog.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "mainwindow.h" 8 | #include "ui_importdialog.h" 9 | 10 | ImportDialog::ImportDialog(QWidget *parent) 11 | : QDialog(parent) 12 | , ui(new Ui::ImportDialog()) 13 | { 14 | ui->setupUi(this); 15 | } 16 | 17 | ImportDialog::~ImportDialog() 18 | { 19 | delete ui; 20 | } 21 | 22 | void ImportDialog::initialize() 23 | { 24 | // - remember the last selected type 25 | QComboBox *typeBox = this->ui->typeComboBox; 26 | QString lastFmt = typeBox->currentText(); 27 | if (lastFmt.isEmpty()) { 28 | lastFmt = "Font"; 29 | } 30 | typeBox->clear(); 31 | typeBox->addItem("Font"); 32 | typeBox->setCurrentIndex(typeBox->findText(lastFmt)); 33 | 34 | setRenderColor(this->renderColor); 35 | } 36 | 37 | void ImportDialog::setRenderColor(QColor color) 38 | { 39 | QString red = QString::number(color.red()); 40 | QString green = QString::number(color.green()); 41 | QString blue = QString::number(color.blue()); 42 | QString styleSheet = QString("background: rgb(") + red + QString(",") + green + QString(",") + blue + QString(")"); 43 | ui->fontColorButton->setStyleSheet(styleSheet); 44 | this->renderColor = color; 45 | } 46 | 47 | QString ImportDialog::getFileFormatExtension() 48 | { 49 | return "." + this->ui->typeComboBox->currentText().toLower(); 50 | } 51 | 52 | void ImportDialog::on_inputFileBrowseButton_clicked() 53 | { 54 | QString selectedDirectory = QFileDialog::getOpenFileName( 55 | this, "Select Font File", QString(), "Fonts (*.ttf *.otf)"); 56 | 57 | if (selectedDirectory.isEmpty()) 58 | return; 59 | 60 | ui->inputFileEdit->setText(selectedDirectory); 61 | } 62 | 63 | void ImportDialog::on_fontSymbolsEdit_textChanged(const QString &text) 64 | { 65 | bool ok = false; 66 | uint test = text.toUInt(&ok, 16); 67 | if (!ok) { 68 | ui->fontSymbolsRangeLabel->setText("Error"); 69 | return; 70 | } 71 | 72 | QString pad = text.toLower(); 73 | while (pad.size() < 2) 74 | pad = "0" + pad; 75 | 76 | QString start = "U+" + pad + "00"; 77 | QString end = "U+" + pad + "ff"; 78 | ui->fontSymbolsRangeLabel->setText(start + " - " + end); 79 | } 80 | 81 | void ImportDialog::on_fontColorButton_clicked() 82 | { 83 | QColor color = QColorDialog::getColor(this->renderColor); 84 | 85 | if (color.isValid()) 86 | setRenderColor(color); 87 | } 88 | 89 | void ImportDialog::on_importButton_clicked() 90 | { 91 | if (ui->inputFileEdit->text() == "") { 92 | QMessageBox::warning(this, "Warning", "Input file is missing, please choose an input folder."); 93 | return; 94 | } 95 | 96 | try { 97 | MainWindow *mainWindow = dynamic_cast(this->parent()); 98 | if (mainWindow == nullptr) { 99 | QMessageBox::critical(this, "Error", "Window not found."); 100 | return; 101 | } 102 | 103 | QString filePath = ui->inputFileEdit->text(); 104 | int pointSize = ui->fontSizeEdit->text().toInt(); 105 | uint symbolPrefix = ui->fontSymbolsEdit->text().toUInt() << 8; 106 | mainWindow->openFontFile(filePath, this->renderColor, pointSize, symbolPrefix); 107 | } catch (...) { 108 | QMessageBox::critical(this, "Error", "Import Failed."); 109 | return; 110 | } 111 | 112 | this->close(); 113 | } 114 | 115 | void ImportDialog::on_importCancelButton_clicked() 116 | { 117 | this->close(); 118 | } 119 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Sustainable Use License 2 | 3 | Version 1.0 4 | 5 | ## Acceptance 6 | 7 | By using the software, you agree to all of the terms and conditions below. 8 | 9 | ## Copyright License 10 | 11 | The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below. 12 | 13 | ## Limitations 14 | 15 | You may use or modify the software only for your own internal business purposes or for non-commercial or personal use. 16 | You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes. 17 | You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. 18 | 19 | ## Patents 20 | 21 | The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. 22 | 23 | ## Notices 24 | 25 | You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. 26 | If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software. 27 | 28 | ## No Other Rights 29 | 30 | These terms do not imply any licenses other than those expressly granted in these terms. 31 | 32 | ## Termination 33 | 34 | If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently. 35 | 36 | ## No Liability 37 | 38 | As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. 39 | 40 | ## Definitions 41 | 42 | The “licensor” is the entity offering these terms. 43 | 44 | The “software” is the software the licensor makes available under these terms, including any portion of it. 45 | 46 | “You” refers to the individual or entity agreeing to these terms. 47 | 48 | “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. 49 | 50 | “Your license” is the license granted to you for the software under these terms. 51 | 52 | “Use” means anything you do with the software requiring your license. 53 | 54 | “Trademark” means trademarks, service marks, and similar rights. 55 | -------------------------------------------------------------------------------- /source/widgets/leveltabframewidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LevelTabFrameWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 480 10 | 160 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 2 28 | 29 | 30 | 2 31 | 32 | 33 | 2 34 | 35 | 36 | 2 37 | 38 | 39 | 40 | 41 | 4 42 | 43 | 44 | 45 | 46 | 4 47 | 48 | 49 | 50 | 51 | 52 | 100 53 | 0 54 | 55 | 56 | 57 | 58 | 100 59 | 16777215 60 | 61 | 62 | 63 | Encoding: 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 160 72 | 0 73 | 74 | 75 | 76 | 77 | 160 78 | 16777215 79 | 80 | 81 | 82 | 83 | Square 84 | 85 | 86 | 87 | 88 | Transparent square 89 | 90 | 91 | 92 | 93 | Left Triangle 94 | 95 | 96 | 97 | 98 | Right Triangle 99 | 100 | 101 | 102 | 103 | Left Trapezoid 104 | 105 | 106 | 107 | 108 | Right Trapezoid 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /source/widgets/leveltabsubtilewidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LevelTabSubTileWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 480 10 | 90 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 2 28 | 29 | 30 | 2 31 | 32 | 33 | 2 34 | 35 | 36 | 2 37 | 38 | 39 | 40 | 41 | 4 42 | 43 | 44 | 45 | 46 | 47 | 0 48 | 0 49 | 50 | 51 | 52 | 53 | 16777215 54 | 80 55 | 56 | 57 | 58 | Flags 59 | 60 | 61 | 62 | 63 | 64 | Solid 65 | 66 | 67 | 68 | 69 | 70 | 71 | Block light 72 | 73 | 74 | 75 | 76 | 77 | 78 | Block missile 79 | 80 | 81 | 82 | 83 | 84 | 85 | Allow trap 86 | 87 | 88 | 89 | 90 | 91 | 92 | Transparent 93 | 94 | 95 | 96 | 97 | 98 | 99 | Left masked 100 | 101 | 102 | 103 | 104 | 105 | 106 | Right masked 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /source/widgets/leveltabsubtilewidget.cpp: -------------------------------------------------------------------------------- 1 | #include "leveltabsubtilewidget.h" 2 | 3 | #include "ui_leveltabsubtilewidget.h" 4 | #include "views/levelcelview.h" 5 | 6 | LevelTabSubTileWidget::LevelTabSubTileWidget() 7 | : QWidget(nullptr) 8 | , ui(new Ui::LevelTabSubTileWidget) 9 | { 10 | ui->setupUi(this); 11 | } 12 | 13 | LevelTabSubTileWidget::~LevelTabSubTileWidget() 14 | { 15 | delete ui; 16 | } 17 | 18 | void LevelTabSubTileWidget::initialize(LevelCelView *v, D1Gfx *g, D1Min *m, D1Sol *s) 19 | { 20 | this->levelCelView = v; 21 | this->gfx = g; 22 | this->min = m; 23 | this->sol = s; 24 | } 25 | 26 | void LevelTabSubTileWidget::update() 27 | { 28 | this->onUpdate = true; 29 | 30 | bool hasSubtile = this->min->getSubtileCount() != 0; 31 | 32 | this->ui->sol0->setEnabled(hasSubtile); 33 | this->ui->sol1->setEnabled(hasSubtile); 34 | this->ui->sol2->setEnabled(hasSubtile); 35 | this->ui->sol3->setEnabled(hasSubtile); 36 | this->ui->sol4->setEnabled(hasSubtile); 37 | this->ui->sol5->setEnabled(hasSubtile); 38 | this->ui->sol7->setEnabled(hasSubtile); 39 | 40 | if (!hasSubtile) { 41 | this->ui->sol0->setChecked(false); 42 | this->ui->sol1->setChecked(false); 43 | this->ui->sol2->setChecked(false); 44 | this->ui->sol3->setChecked(false); 45 | this->ui->sol4->setChecked(false); 46 | this->ui->sol5->setChecked(false); 47 | this->ui->sol7->setChecked(false); 48 | 49 | this->onUpdate = false; 50 | return; 51 | } 52 | 53 | int subtileIdx = this->levelCelView->getCurrentSubtileIndex(); 54 | quint8 sol = this->sol->getSubtileProperties(subtileIdx); 55 | 56 | this->ui->sol0->setChecked((sol & 1 << 0) != 0); 57 | this->ui->sol1->setChecked((sol & 1 << 1) != 0); 58 | this->ui->sol2->setChecked((sol & 1 << 2) != 0); 59 | this->ui->sol3->setChecked((sol & 1 << 3) != 0); 60 | this->ui->sol4->setChecked((sol & 1 << 4) != 0); 61 | this->ui->sol5->setChecked((sol & 1 << 5) != 0); 62 | this->ui->sol7->setChecked((sol & 1 << 7) != 0); 63 | 64 | this->onUpdate = false; 65 | } 66 | 67 | void LevelTabSubTileWidget::updateSolProperty() 68 | { 69 | int subTileIdx = this->levelCelView->getCurrentSubtileIndex(); 70 | quint8 flags = this->readSol(); 71 | 72 | this->sol->setSubtileProperties(subTileIdx, flags); 73 | } 74 | 75 | quint8 LevelTabSubTileWidget::readSol() 76 | { 77 | quint8 flags = 0; 78 | if (this->ui->sol0->checkState()) 79 | flags |= 1 << 0; 80 | if (this->ui->sol1->checkState()) 81 | flags |= 1 << 1; 82 | if (this->ui->sol2->checkState()) 83 | flags |= 1 << 2; 84 | if (this->ui->sol3->checkState()) 85 | flags |= 1 << 3; 86 | if (this->ui->sol4->checkState()) 87 | flags |= 1 << 4; 88 | if (this->ui->sol5->checkState()) 89 | flags |= 1 << 5; 90 | if (this->ui->sol7->checkState()) 91 | flags |= 1 << 7; 92 | return flags; 93 | } 94 | 95 | void LevelTabSubTileWidget::on_sol0_clicked() 96 | { 97 | this->updateSolProperty(); 98 | } 99 | 100 | void LevelTabSubTileWidget::on_sol1_clicked() 101 | { 102 | this->updateSolProperty(); 103 | } 104 | 105 | void LevelTabSubTileWidget::on_sol2_clicked() 106 | { 107 | this->updateSolProperty(); 108 | } 109 | 110 | void LevelTabSubTileWidget::on_sol3_clicked() 111 | { 112 | this->updateSolProperty(); 113 | } 114 | 115 | void LevelTabSubTileWidget::on_sol4_clicked() 116 | { 117 | this->updateSolProperty(); 118 | } 119 | 120 | void LevelTabSubTileWidget::on_sol5_clicked() 121 | { 122 | this->updateSolProperty(); 123 | } 124 | 125 | void LevelTabSubTileWidget::on_sol7_clicked() 126 | { 127 | this->updateSolProperty(); 128 | } 129 | -------------------------------------------------------------------------------- /source/.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | # clang-tidy configuration 3 | # 4 | # clang-tidy can be run manually like this: 5 | # 6 | # run-clang-tidy -p build 'source.*' 7 | # 8 | # To apply fixes suggested by clang-tidy, run: 9 | # 10 | # run-clang-tidy -p build -fix -format 'source.*' 11 | # 12 | # To limit the run to certain checks: 13 | # 14 | # run-clang-tidy -checks='-*,modernize-use-nullptr' -p build 'Source.*' 15 | # 16 | # clang-tidy also has several IDE integrations listed here: 17 | # https://clang.llvm.org/extra/clang-tidy/Integrations.html 18 | 19 | # Enable most checks. 20 | # 21 | # Built-in checks: 22 | # https://clang.llvm.org/extra/clang-tidy/checks/list.html 23 | # 24 | # Exclusions: 25 | # 26 | # -modernize-avoid-c-arrays 27 | # We use C-arrays throughout, e.g. for `constexpr char []`. 28 | # `std::array` is not a replacement because its length is 29 | # not deduced. 30 | # 31 | # -modernize-use-trailing-return-type 32 | # A purely stylistic change that we do not want. 33 | # 34 | # -modernize-concat-nested-namespaces 35 | # -modernize-avoid-bind 36 | # Compatibility with older compilers. 37 | Checks: > 38 | -*, 39 | bugprone-*, 40 | cppcoreguidelines-pro-type-cstyle-cast, 41 | google-runtime-int, 42 | llvm-include-order, 43 | llvm-namespace-comment, 44 | misc-*, 45 | modernize-*, 46 | performance-*, 47 | portability-*, 48 | readability-*, 49 | -readability-magic-numbers, 50 | -misc-non-private-member-variables-in-classes, 51 | -modernize-avoid-c-arrays, 52 | -modernize-use-trailing-return-type, 53 | -modernize-concat-nested-namespaces, 54 | -modernize-avoid-bind, 55 | -bugprone-easily-swappable-parameters, 56 | -readability-function-cognitive-complexity, 57 | -readability-identifier-length 58 | 59 | HeaderFilterRegex: 'source/.*\.(h|hpp)$' 60 | 61 | CheckOptions: 62 | - { key: readability-identifier-naming.NamespaceCase, value: lower_case } 63 | - { key: readability-identifier-naming.ClassCase, value: CamelCase } 64 | - { key: readability-identifier-naming.StructCase, value: CamelCase } 65 | - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } 66 | - { key: readability-identifier-naming.MethodCase, value: camelBack } 67 | - { key: readability-identifier-naming.FunctionCase, value: CamelCase } 68 | - { key: readability-identifier-naming.ParameterCase, value: camelBack } 69 | - { key: readability-identifier-naming.MemberCase, value: camelBack } 70 | - { key: readability-identifier-naming.VariableCase, value: camelBack } 71 | - { key: readability-identifier-naming.ClassMemberCase, value: lower_case } 72 | - { key: readability-identifier-naming.GlobalVariableCase, value: aNy_CasE } 73 | - { key: readability-identifier-naming.GlobalFunctionCase, value: aNy_CasE } 74 | - { key: readability-identifier-naming.ClassMemberSuffix, value: _ } 75 | - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } 76 | - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } 77 | - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } 78 | - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } 79 | - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } 80 | - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } 81 | - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } 82 | 83 | # Allow short if-statements without braces 84 | - { key: readability-braces-around-statements.ShortStatementLines, value: 3 } 85 | 86 | # Use fixed-width integer types instead of short, long and long long 87 | - { key: google-runtime-int.UnsignedTypePrefix, value: "std::uint" } 88 | - { key: google-runtime-int.SignedTypePrefix, value: "std::int" } 89 | - { key: google-runtime-int.TypeSuffix, value: "_t" } 90 | 91 | # `int8_t` aren't used as chars, disable misleading warning. 92 | - { key: bugprone-signed-char-misuse.CharTypdefsToIgnore, value: "std::int8_t" } 93 | -------------------------------------------------------------------------------- /source/views/celview.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | 17 | #include "d1formats/d1gfx.h" 18 | #include "undostack/undostack.h" 19 | 20 | #define CEL_SCENE_SPACING 8 21 | 22 | namespace Ui { 23 | class CelView; 24 | } // namespace Ui 25 | 26 | enum class IMAGE_FILE_MODE; 27 | 28 | class CelScene : public QGraphicsScene { 29 | Q_OBJECT 30 | 31 | public: 32 | CelScene(QWidget *view); 33 | 34 | private slots: 35 | void mousePressEvent(QGraphicsSceneMouseEvent *event); 36 | void mouseMoveEvent(QGraphicsSceneMouseEvent *event); 37 | void dragEnterEvent(QGraphicsSceneDragDropEvent *event); 38 | void dragMoveEvent(QGraphicsSceneDragDropEvent *event); 39 | void dropEvent(QGraphicsSceneDragDropEvent *event); 40 | void contextMenuEvent(QContextMenuEvent *event); 41 | 42 | signals: 43 | void framePixelClicked(unsigned x, unsigned y); 44 | void showContextMenu(const QPoint &pos); 45 | 46 | private: 47 | QWidget *view; 48 | }; 49 | 50 | class CelView : public QWidget { 51 | Q_OBJECT 52 | 53 | public: 54 | explicit CelView(std::shared_ptr us, QWidget *parent = nullptr); 55 | ~CelView(); 56 | 57 | void initialize(D1Gfx *gfx); 58 | void sendRemoveFrameCmd(); 59 | void sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); 60 | int getCurrentFrameIndex(); 61 | void framePixelClicked(unsigned x, unsigned y); 62 | void insertImageFiles(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); 63 | void sendReplaceCurrentFrameCmd(const QString &imagefilePath); 64 | void replaceCurrentFrame(int frameIdx, const QImage &image); 65 | void removeCurrentFrame(int frameIdx); 66 | void regroupFrames(int numGroups); 67 | void updateGroupIndex(); 68 | 69 | void displayFrame(); 70 | [[nodiscard]] bool isInImage(unsigned int x, unsigned int y) const; 71 | 72 | signals: 73 | void frameRefreshed(); 74 | void colorIndexClicked(quint8); 75 | 76 | private: 77 | void update(); 78 | void removeFrames(int index); 79 | void insertFrames(int index, const QImage &image); 80 | void setGroupIndex(); 81 | 82 | private slots: 83 | void on_firstFrameButton_clicked(); 84 | void on_previousFrameButton_clicked(); 85 | void on_nextFrameButton_clicked(); 86 | void on_lastFrameButton_clicked(); 87 | void on_frameIndexEdit_returnPressed(); 88 | 89 | void on_firstGroupButton_clicked(); 90 | void on_previousGroupButton_clicked(); 91 | void on_groupIndexEdit_returnPressed(); 92 | void on_nextGroupButton_clicked(); 93 | void on_lastGroupButton_clicked(); 94 | 95 | void on_zoomOutButton_clicked(); 96 | void on_zoomInButton_clicked(); 97 | void on_zoomEdit_returnPressed(); 98 | 99 | void on_playDelayEdit_textChanged(const QString &text); 100 | void on_playButton_clicked(); 101 | void on_stopButton_clicked(); 102 | void playGroup(); 103 | 104 | void dragEnterEvent(QDragEnterEvent *event); 105 | void dragMoveEvent(QDragMoveEvent *event); 106 | void dropEvent(QDropEvent *event); 107 | 108 | void ShowContextMenu(const QPoint &pos); 109 | void insertImageFile(int frameIdx, const QImage img); 110 | 111 | private: 112 | std::stack removedGroupIdxs; // holds indexes of groups that have been removed, used for undo ops 113 | std::stack removedFrameGroupIdxs; // holds group indexes of frames that got removed, used for undo ops 114 | 115 | std::shared_ptr undoStack; 116 | Ui::CelView *ui; 117 | CelScene *celScene; 118 | 119 | D1Gfx *gfx; 120 | int currentGroupIndex = 0; 121 | int currentFrameIndex = 0; 122 | quint8 currentZoomFactor = 1; 123 | quint16 currentPlayDelay = 50; 124 | 125 | QTimer playTimer; 126 | }; 127 | -------------------------------------------------------------------------------- /source/d1formats/d1amp.cpp: -------------------------------------------------------------------------------- 1 | #include "d1amp.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | bool D1Amp::load(QString filePath, int tileCount, const OpenAsParam ¶ms) 11 | { 12 | // prepare file data source 13 | QFile file; 14 | // done by the caller 15 | // if (!params.ampFilePath.isEmpty()) { 16 | // filePath = params.ampFilePath; 17 | // } 18 | if (!filePath.isEmpty()) { 19 | file.setFileName(filePath); 20 | if (!file.open(QIODevice::ReadOnly) && !params.ampFilePath.isEmpty()) { 21 | return false; // report read-error only if the file was explicitly requested 22 | } 23 | } 24 | 25 | QByteArray fileData = file.readAll(); 26 | QBuffer fileBuffer(&fileData); 27 | 28 | if (!fileBuffer.open(QIODevice::ReadOnly)) { 29 | return false; 30 | } 31 | 32 | // File size check 33 | auto fileSize = file.size(); 34 | if (fileSize % 2 != 0) { 35 | qDebug() << "Invalid amp-file."; 36 | return false; 37 | } 38 | 39 | int ampTileCount = fileSize / 2; 40 | if (ampTileCount != tileCount) { 41 | if (ampTileCount != 0) { 42 | qDebug() << "The size of amp-file does not align with til-file"; 43 | } 44 | if (ampTileCount > tileCount) { 45 | ampTileCount = tileCount; // skip unusable data 46 | } 47 | } 48 | 49 | // prepare empty lists with zeros 50 | this->properties.clear(); 51 | this->types.clear(); 52 | for (int i = 0; i < tileCount; i++) { 53 | this->types.append(0); 54 | this->properties.append(0); 55 | } 56 | 57 | // Read AMP binary data 58 | QDataStream in(&fileBuffer); 59 | in.setByteOrder(QDataStream::LittleEndian); 60 | 61 | for (int i = 0; i < ampTileCount; i++) { 62 | quint8 readBytr; 63 | in >> readBytr; 64 | this->types[i] = readBytr; 65 | in >> readBytr; 66 | this->properties[i] = readBytr; 67 | } 68 | 69 | this->ampFilePath = filePath; 70 | this->modified = false; 71 | return true; 72 | } 73 | 74 | bool D1Amp::save(const QString &gfxPath) 75 | { 76 | QString filePath = gfxPath; 77 | filePath.chop(3); 78 | filePath += "amp"; 79 | 80 | QFile outFile = QFile(filePath); 81 | if (!outFile.open(QIODevice::WriteOnly | QFile::Truncate)) { 82 | QMessageBox::critical(nullptr, "Error", "Failed open file: " + filePath); 83 | return false; 84 | } 85 | 86 | // write to file 87 | QDataStream out(&outFile); 88 | for (int i = 0; i < this->types.size(); i++) { 89 | out << this->types[i]; 90 | out << this->properties[i]; 91 | } 92 | 93 | this->ampFilePath = filePath; 94 | this->modified = false; 95 | 96 | return true; 97 | } 98 | 99 | bool D1Amp::isModified() const 100 | { 101 | return this->modified; 102 | } 103 | 104 | QString D1Amp::getFilePath() 105 | { 106 | return this->ampFilePath; 107 | } 108 | 109 | quint8 D1Amp::getTileType(quint16 tileIndex) 110 | { 111 | if (tileIndex >= this->types.count()) 112 | return 0; 113 | 114 | return this->types.at(tileIndex); 115 | } 116 | 117 | quint8 D1Amp::getTileProperties(quint16 tileIndex) 118 | { 119 | if (tileIndex >= this->properties.count()) 120 | return 0; 121 | 122 | return this->properties.at(tileIndex); 123 | } 124 | 125 | void D1Amp::setTileType(quint16 tileIndex, quint8 value) 126 | { 127 | this->types[tileIndex] = value; 128 | this->modified = true; 129 | } 130 | 131 | void D1Amp::setTileProperties(quint16 tileIndex, quint8 value) 132 | { 133 | this->properties[tileIndex] = value; 134 | this->modified = true; 135 | } 136 | 137 | void D1Amp::createTile() 138 | { 139 | this->types.append(0); 140 | this->properties.append(0); 141 | this->modified = true; 142 | } 143 | 144 | void D1Amp::removeTile(int tileIndex) 145 | { 146 | this->types.removeAt(tileIndex); 147 | this->properties.removeAt(tileIndex); 148 | this->modified = true; 149 | } 150 | -------------------------------------------------------------------------------- /source/config/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | static QJsonObject theConfig; 12 | QString Config::dirPath; 13 | 14 | /** 15 | * @brief Loads current configuration from the config .json file 16 | * 17 | * This function loads current configuration from the config .json file. 18 | * It inserts specific values mapping them to keys, and creates path for the 19 | * config if user has deleted it, or runs app for the first time. 20 | */ 21 | void Config::loadConfiguration() 22 | { 23 | // create directories on the path if they do not exist 24 | if (!Config::createDirectoriesOnPath()) { 25 | qDebug() << "Couldn't resolve path for the config file. Configuration file won't be loaded."; 26 | return; 27 | } 28 | 29 | // add filename to the absolute path 30 | QString jsonFilePath = dirPath + "/D1GraphicsTool.config.json"; 31 | 32 | bool configurationModified = false; 33 | 34 | // If configuration file exists load it otherwise create it 35 | if (QFile::exists(jsonFilePath)) { 36 | QFile loadJson(jsonFilePath); 37 | loadJson.open(QIODevice::ReadOnly); 38 | QJsonDocument loadJsonDoc = QJsonDocument::fromJson(loadJson.readAll()); 39 | theConfig = loadJsonDoc.object(); 40 | loadJson.close(); 41 | } 42 | 43 | if (!theConfig.contains("LastFilePath")) { 44 | theConfig.insert("LastFilePath", QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/"); 45 | configurationModified = true; 46 | } 47 | if (!theConfig.contains("PaletteDefaultColor")) { 48 | theConfig.insert("PaletteDefaultColor", "#FF00FF"); 49 | configurationModified = true; 50 | } 51 | if (!theConfig.contains("PaletteSelectionBorderColor")) { 52 | theConfig.insert("PaletteSelectionBorderColor", "#FF0000"); 53 | configurationModified = true; 54 | } 55 | 56 | if (configurationModified) { 57 | Config::storeConfiguration(); 58 | } 59 | } 60 | 61 | /** 62 | * @brief Stores current configuration in the config .json file 63 | * 64 | * This function stores current configuration in the config .json 65 | * file, and also - if user has deleted his app's config path durrent 66 | * runtime, it will try to create it once again 67 | */ 68 | void Config::storeConfiguration() 69 | { 70 | // create directories on the path upon closing program 71 | if (!Config::createDirectoriesOnPath()) { 72 | qDebug() << "Couldn't resolve path for the config file. Configuration file won't be saved."; 73 | return; 74 | } 75 | 76 | QFile saveJson(dirPath + "/D1GraphicsTool.config.json"); 77 | saveJson.open(QIODevice::WriteOnly); 78 | QJsonDocument saveDoc(theConfig); 79 | saveJson.write(saveDoc.toJson()); 80 | saveJson.close(); 81 | } 82 | 83 | /** 84 | * @brief Creates directories on certain path 85 | * 86 | * This function creates directories on certain path. Path location depends on 87 | * if user is working on Windows or Mac/Linux. 88 | * 89 | * @return Returns true if path has been created or already existed - false otherwise 90 | */ 91 | bool Config::createDirectoriesOnPath() 92 | { 93 | dirPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); 94 | 95 | return QDir().mkpath(dirPath); 96 | } 97 | 98 | /** 99 | * @brief Retrieves value from .json config file 100 | * 101 | * This function retrieves value from .json config file by the value 102 | * specified by name parameter. 103 | * 104 | * @return Returns QJsonValue containing value of the parameter specified 105 | * by "name" key, otherwise if not found - returns QJsonValue::Undefined 106 | */ 107 | QJsonValue Config::value(const QString &name) 108 | { 109 | return theConfig.value(name); 110 | } 111 | 112 | /** 113 | * @brief Inserts value in .json config file 114 | * 115 | * This function inserts value into .json config file, mapping it with 116 | * the key specified in parameters. 117 | */ 118 | void Config::insert(const QString &key, const QJsonValue &value) 119 | { 120 | theConfig.insert(key, value); 121 | } 122 | -------------------------------------------------------------------------------- /source/palette/d1palhits.cpp: -------------------------------------------------------------------------------- 1 | #include "d1palhits.h" 2 | 3 | D1PalHits::D1PalHits(D1Gfx *g, D1Min *m, D1Til *t) 4 | : gfx(g) 5 | , min(m) 6 | , til(t) 7 | { 8 | this->update(); 9 | } 10 | 11 | void D1PalHits::update() 12 | { 13 | this->buildPalHits(); 14 | this->buildSubtilePalHits(); 15 | this->buildTilePalHits(); 16 | } 17 | 18 | D1PALHITS_MODE D1PalHits::getMode() const 19 | { 20 | return this->mode; 21 | } 22 | 23 | void D1PalHits::setMode(D1PALHITS_MODE m) 24 | { 25 | this->mode = m; 26 | } 27 | 28 | void D1PalHits::buildPalHits() 29 | { 30 | // Go through all frames 31 | this->framePalHits.clear(); 32 | this->allFramesPalHits.clear(); 33 | for (int i = 0; i < this->gfx->getFrameCount(); i++) { 34 | QMap frameHits; 35 | 36 | // Get frame pointer 37 | D1GfxFrame *frame = this->gfx->getFrame(i); 38 | 39 | // Go through every pixels of the frame 40 | for (int x = 0; x < frame->getWidth(); x++) { 41 | for (int y = 0; y < frame->getHeight(); y++) { 42 | // Retrieve the color of the pixel 43 | D1GfxPixel pixel = frame->getPixel(x, y); 44 | if (pixel.isTransparent()) 45 | continue; 46 | quint8 paletteIndex = pixel.getPaletteIndex(); 47 | 48 | // Add one hit to the frameHits and allFramesPalHits maps 49 | frameHits.insert(paletteIndex, frameHits.value(paletteIndex) + 1); 50 | 51 | this->allFramesPalHits.insert(paletteIndex, frameHits.value(paletteIndex) + 1); 52 | } 53 | } 54 | 55 | this->framePalHits[i] = frameHits; 56 | } 57 | } 58 | 59 | void D1PalHits::buildSubtilePalHits() 60 | { 61 | this->subtilePalHits.clear(); 62 | 63 | if (this->min == nullptr) { 64 | return; 65 | } 66 | // Go through all sub-tiles 67 | for (int i = 0; i < this->min->getSubtileCount(); i++) { 68 | QMap subtileHits; 69 | 70 | // Retrieve the CEL frame indices of the current sub-tile 71 | QList &celFrameIndices = this->min->getCelFrameIndices(i); 72 | 73 | // Go through the CEL frames 74 | for (quint16 frameIndex : celFrameIndices) { 75 | frameIndex--; 76 | 77 | // Go through the hits of the CEL frame and add them to the subtile hits 78 | QMapIterator it2(this->framePalHits.value(frameIndex)); 79 | while (it2.hasNext()) { 80 | it2.next(); 81 | subtileHits.insert(it2.key(), it2.value()); 82 | } 83 | } 84 | 85 | this->subtilePalHits[i] = subtileHits; 86 | } 87 | } 88 | 89 | void D1PalHits::buildTilePalHits() 90 | { 91 | this->tilePalHits.clear(); 92 | if (this->til == nullptr) { 93 | return; 94 | } 95 | // Go through all tiles 96 | for (int i = 0; i < this->til->getTileCount(); i++) { 97 | QMap tileHits; 98 | 99 | // Retrieve the sub-tile indices of the current tile 100 | QList &subtileIndices = this->til->getSubtileIndices(i); 101 | 102 | // Go through the sub-tiles 103 | for (quint16 subtileIndex : subtileIndices) { 104 | // Go through the hits of the sub-tile and add them to the tile hits 105 | QMapIterator it2(this->subtilePalHits.value(subtileIndex)); 106 | while (it2.hasNext()) { 107 | it2.next(); 108 | tileHits.insert(it2.key(), it2.value()); 109 | } 110 | } 111 | 112 | this->tilePalHits[i] = tileHits; 113 | } 114 | } 115 | 116 | int D1PalHits::getIndexHits(quint8 colorIndex, int itemIndex) const 117 | { 118 | switch (this->mode) { 119 | case D1PALHITS_MODE::ALL_COLORS: 120 | return 1; 121 | case D1PALHITS_MODE::ALL_FRAMES: 122 | if (this->allFramesPalHits.contains(colorIndex)) 123 | return this->allFramesPalHits[colorIndex]; 124 | break; 125 | case D1PALHITS_MODE::CURRENT_TILE: 126 | if (this->tilePalHits.contains(itemIndex) && this->tilePalHits[itemIndex].contains(colorIndex)) 127 | return this->tilePalHits[itemIndex][colorIndex]; 128 | break; 129 | case D1PALHITS_MODE::CURRENT_SUBTILE: 130 | if (this->subtilePalHits.contains(itemIndex) && this->subtilePalHits[itemIndex].contains(colorIndex)) 131 | return this->subtilePalHits[itemIndex][colorIndex]; 132 | break; 133 | case D1PALHITS_MODE::CURRENT_FRAME: 134 | if (this->framePalHits.contains(itemIndex) && this->framePalHits[itemIndex].contains(colorIndex)) 135 | return this->framePalHits[itemIndex][colorIndex]; 136 | break; 137 | } 138 | 139 | return 0; 140 | } 141 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(D1GraphicsTool VERSION 1.1.0 LANGUAGES CXX) 4 | 5 | set(CMAKE_AUTOUIC ON) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 20) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) 13 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) 14 | 15 | include_directories(source/) 16 | 17 | set(PROJECT_SOURCES 18 | source/views/celview.cpp 19 | source/config/config.cpp 20 | source/d1formats/d1amp.cpp 21 | source/d1formats/d1cel.cpp 22 | source/d1formats/d1celframe.cpp 23 | source/d1formats/d1celtileset.cpp 24 | source/d1formats/d1celtilesetframe.cpp 25 | source/d1formats/d1cl2.cpp 26 | source/d1formats/d1gfx.cpp 27 | source/d1formats/d1image.cpp 28 | source/d1formats/d1min.cpp 29 | source/palette/d1pal.cpp 30 | source/palette/d1palhits.cpp 31 | source/d1formats/d1sol.cpp 32 | source/d1formats/d1til.cpp 33 | source/d1formats/d1trn.cpp 34 | source/dialogs/exportdialog.cpp 35 | source/dialogs/importdialog.cpp 36 | source/views/view.cpp 37 | source/views/levelcelview.cpp 38 | source/widgets/leveltabframewidget.cpp 39 | source/widgets/leveltabsubtilewidget.cpp 40 | source/widgets/leveltabtilewidget.cpp 41 | source/main.cpp 42 | source/mainwindow.cpp 43 | source/dialogs/openasdialog.cpp 44 | source/widgets/palettewidget.cpp 45 | source/dialogs/settingsdialog.cpp 46 | source/undostack/framecmds.cpp 47 | source/undostack/framecmds.h 48 | source/undostack/undostack.cpp 49 | source/undostack/undostack.h 50 | source/undostack/command.cpp 51 | source/undostack/command.h 52 | source/undostack/undomacro.cpp 53 | source/undostack/undomacro.h 54 | ) 55 | 56 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 57 | qt_add_executable(D1GraphicsTool 58 | resources/D1GraphicsTool.rc 59 | resources/d1files.qrc 60 | MANUAL_FINALIZATION 61 | ${PROJECT_SOURCES} 62 | ) 63 | else() 64 | add_executable(D1GraphicsTool 65 | resources/D1GraphicsTool.rc 66 | resources/d1files.qrc 67 | ${PROJECT_SOURCES} 68 | ) 69 | endif() 70 | 71 | target_link_libraries(D1GraphicsTool PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) 72 | 73 | set_target_properties(D1GraphicsTool PROPERTIES 74 | MACOSX_BUNDLE_GUI_IDENTIFIER d1-graphics-tool.savagesteel.net 75 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 76 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 77 | MACOSX_BUNDLE TRUE 78 | WIN32_EXECUTABLE TRUE 79 | ) 80 | 81 | install(TARGETS D1GraphicsTool 82 | BUNDLE DESTINATION . 83 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) 84 | 85 | if(QT_VERSION_MAJOR EQUAL 6) 86 | qt_finalize_executable(D1GraphicsTool) 87 | endif() 88 | 89 | if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 90 | string(TOLOWER ${PROJECT_NAME} project_name) 91 | set(CPACK_PACKAGE_NAME ${project_name}) 92 | 93 | # Common *nix files 94 | set(CPACK_STRIP_FILES TRUE) 95 | install(TARGETS ${BIN_TARGET} DESTINATION bin) 96 | 97 | set(desktop_file "${CMAKE_BINARY_DIR}/${PROJECT_NAME}") 98 | 99 | install(FILES "${desktop_file}" 100 | DESTINATION "/usr/bin/" 101 | PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE 102 | ) 103 | 104 | install(FILES "${PROJECT_SOURCE_DIR}/debian/usr/share/applications/${PROJECT_NAME}.desktop" 105 | DESTINATION "/usr/share/applications" 106 | ) 107 | 108 | install(FILES "${PROJECT_SOURCE_DIR}/resources/icon.svg" 109 | DESTINATION "/opt/d1-graphics-tool/" 110 | RENAME "icon.svg" 111 | ) 112 | 113 | # -G DEB 114 | set(CPACK_PACKAGE_CONTACT "Anders Jenbo ") 115 | set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/diasurgical/d1-graphics-tool") 116 | set(CPACK_PACKAGE_DESCRIPTION "Diablo 1 Graphics Tool can open CEL/CL2 graphics files and display them with chosen color palette (PAL) and color translation (TRN) files.") 117 | set(CPACK_DEBIAN_PACKAGE_SECTION "graphics") 118 | 119 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 120 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6widgets6 (>= 6.2.4), qt6-qpa-plugins (>= 6.2.4)") 121 | else() 122 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5widgets5 (>= 5.15.0)") 123 | endif() 124 | set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) 125 | 126 | find_program(DPKG dpkg) 127 | if(DPKG) 128 | list(APPEND CPACK_GENERATOR "DEB") 129 | endif() 130 | 131 | set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) 132 | set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) 133 | set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) 134 | include(CPack) 135 | 136 | endif() 137 | -------------------------------------------------------------------------------- /source/widgets/leveltabtilewidget.cpp: -------------------------------------------------------------------------------- 1 | #include "leveltabtilewidget.h" 2 | 3 | #include "d1formats/d1amp.h" 4 | #include "d1formats/d1min.h" 5 | #include "d1formats/d1til.h" 6 | #include "ui_leveltabtilewidget.h" 7 | #include "views/levelcelview.h" 8 | 9 | LevelTabTileWidget::LevelTabTileWidget() 10 | : QWidget(nullptr) 11 | , ui(new Ui::LevelTabTileWidget) 12 | { 13 | ui->setupUi(this); 14 | } 15 | 16 | LevelTabTileWidget::~LevelTabTileWidget() 17 | { 18 | delete ui; 19 | } 20 | 21 | void LevelTabTileWidget::initialize(LevelCelView *v, D1Til *t, D1Min *m, D1Amp *a) 22 | { 23 | this->levelCelView = v; 24 | this->til = t; 25 | this->min = m; 26 | this->amp = a; 27 | } 28 | 29 | void LevelTabTileWidget::update() 30 | { 31 | this->onUpdate = true; 32 | 33 | bool hasTile = this->til->getTileCount() != 0; 34 | 35 | this->ui->ampTypeComboBox->setEnabled(hasTile); 36 | 37 | this->ui->amp0->setEnabled(hasTile); 38 | this->ui->amp1->setEnabled(hasTile); 39 | this->ui->amp2->setEnabled(hasTile); 40 | this->ui->amp3->setEnabled(hasTile); 41 | this->ui->amp4->setEnabled(hasTile); 42 | this->ui->amp5->setEnabled(hasTile); 43 | this->ui->amp6->setEnabled(hasTile); 44 | this->ui->amp7->setEnabled(hasTile); 45 | 46 | if (!hasTile) { 47 | this->ui->ampTypeComboBox->setCurrentIndex(-1); 48 | 49 | this->ui->amp0->setChecked(false); 50 | this->ui->amp1->setChecked(false); 51 | this->ui->amp2->setChecked(false); 52 | this->ui->amp3->setChecked(false); 53 | this->ui->amp4->setChecked(false); 54 | this->ui->amp5->setChecked(false); 55 | this->ui->amp6->setChecked(false); 56 | this->ui->amp7->setChecked(false); 57 | 58 | this->onUpdate = false; 59 | return; 60 | } 61 | 62 | int tileIdx = this->levelCelView->getCurrentTileIndex(); 63 | quint8 ampType = this->amp->getTileType(tileIdx); 64 | quint8 ampProperty = this->amp->getTileProperties(tileIdx); 65 | 66 | // update the combo box of the amp-type 67 | this->ui->ampTypeComboBox->setCurrentIndex(ampType); 68 | // update the checkboxes 69 | this->ui->amp0->setChecked((ampProperty & 1 << 0) != 0); 70 | this->ui->amp1->setChecked((ampProperty & 1 << 1) != 0); 71 | this->ui->amp2->setChecked((ampProperty & 1 << 2) != 0); 72 | this->ui->amp3->setChecked((ampProperty & 1 << 3) != 0); 73 | this->ui->amp4->setChecked((ampProperty & 1 << 4) != 0); 74 | this->ui->amp5->setChecked((ampProperty & 1 << 5) != 0); 75 | this->ui->amp6->setChecked((ampProperty & 1 << 6) != 0); 76 | this->ui->amp7->setChecked((ampProperty & 1 << 7) != 0); 77 | 78 | this->onUpdate = false; 79 | } 80 | 81 | void LevelTabTileWidget::updateAmpType() 82 | { 83 | int tileIdx = this->levelCelView->getCurrentTileIndex(); 84 | quint8 index = this->readAmpType(); 85 | 86 | this->amp->setTileType(tileIdx, index); 87 | } 88 | 89 | void LevelTabTileWidget::updateAmpProperty() 90 | { 91 | int tileIdx = this->levelCelView->getCurrentTileIndex(); 92 | quint8 flags = this->readAmpProperty(); 93 | 94 | this->amp->setTileProperties(tileIdx, flags); 95 | } 96 | 97 | quint8 LevelTabTileWidget::readAmpProperty() 98 | { 99 | quint8 flags = 0; 100 | if (this->ui->amp0->checkState()) 101 | flags |= 1 << 0; 102 | if (this->ui->amp1->checkState()) 103 | flags |= 1 << 1; 104 | if (this->ui->amp2->checkState()) 105 | flags |= 1 << 2; 106 | if (this->ui->amp3->checkState()) 107 | flags |= 1 << 3; 108 | if (this->ui->amp4->checkState()) 109 | flags |= 1 << 4; 110 | if (this->ui->amp5->checkState()) 111 | flags |= 1 << 5; 112 | if (this->ui->amp6->checkState()) 113 | flags |= 1 << 6; 114 | if (this->ui->amp7->checkState()) 115 | flags |= 1 << 7; 116 | return flags; 117 | } 118 | 119 | quint8 LevelTabTileWidget::readAmpType() 120 | { 121 | return this->ui->ampTypeComboBox->currentIndex(); 122 | } 123 | 124 | void LevelTabTileWidget::on_ampTypeComboBox_activated(int index) 125 | { 126 | if (!this->onUpdate) { 127 | this->updateAmpType(); 128 | } 129 | } 130 | 131 | void LevelTabTileWidget::on_amp0_clicked() 132 | { 133 | this->updateAmpProperty(); 134 | } 135 | 136 | void LevelTabTileWidget::on_amp1_clicked() 137 | { 138 | this->updateAmpProperty(); 139 | } 140 | 141 | void LevelTabTileWidget::on_amp2_clicked() 142 | { 143 | this->updateAmpProperty(); 144 | } 145 | 146 | void LevelTabTileWidget::on_amp3_clicked() 147 | { 148 | this->updateAmpProperty(); 149 | } 150 | 151 | void LevelTabTileWidget::on_amp4_clicked() 152 | { 153 | this->updateAmpProperty(); 154 | } 155 | 156 | void LevelTabTileWidget::on_amp5_clicked() 157 | { 158 | this->updateAmpProperty(); 159 | } 160 | 161 | void LevelTabTileWidget::on_amp6_clicked() 162 | { 163 | this->updateAmpProperty(); 164 | } 165 | 166 | void LevelTabTileWidget::on_amp7_clicked() 167 | { 168 | this->updateAmpProperty(); 169 | } 170 | -------------------------------------------------------------------------------- /source/dialogs/settingsdialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 200 10 | 120 11 | 12 | 13 | 14 | Settings 15 | 16 | 17 | 18 | 19 | 20 | Palettes and translations 21 | 22 | 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | 30 | 31 | 32 | 33 | Default palette color: 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 100 42 | 0 43 | 44 | 45 | 46 | 47 | 100 48 | 16777215 49 | 50 | 51 | 52 | 7 53 | 54 | 55 | Qt::AlignCenter 56 | 57 | 58 | 59 | 60 | 61 | 62 | Pick 63 | 64 | 65 | 66 | 67 | 68 | 69 | Qt::Horizontal 70 | 71 | 72 | 73 | 74 | 75 | 76 | Selection border color: 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 100 85 | 0 86 | 87 | 88 | 89 | 90 | 100 91 | 16777215 92 | 93 | 94 | 95 | 7 96 | 97 | 98 | Qt::AlignCenter 99 | 100 | 101 | 102 | 103 | 104 | 105 | Pick 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 4 117 | 118 | 119 | 4 120 | 121 | 122 | 4 123 | 124 | 125 | 4 126 | 127 | 128 | 4 129 | 130 | 131 | 132 | 133 | Qt::Horizontal 134 | 135 | 136 | 137 | 138 | 139 | 140 | Ok 141 | 142 | 143 | 144 | 145 | 146 | 147 | Cancel 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /source/palette/d1pal.cpp: -------------------------------------------------------------------------------- 1 | #include "d1pal.h" 2 | 3 | #include 4 | #include 5 | 6 | bool D1Pal::load(QString filePath) 7 | { 8 | QFile file = QFile(filePath); 9 | 10 | if (!file.open(QIODevice::ReadOnly)) 11 | return false; 12 | 13 | if (file.size() == D1PAL_SIZE_BYTES) { 14 | this->loadRegularPalette(file); 15 | } else if (!this->loadJascPalette(file)) { 16 | return false; 17 | } 18 | 19 | for (int i = 0; i < 32; i++) 20 | this->origCyclePalette[i] = this->colors[i]; 21 | 22 | this->palFilePath = filePath; 23 | this->modified = false; 24 | return true; 25 | } 26 | 27 | void D1Pal::loadRegularPalette(QFile &file) 28 | { 29 | QDataStream in(&file); 30 | 31 | for (int i = 0; i < D1PAL_COLORS; i++) { 32 | quint8 red; 33 | in >> red; 34 | 35 | quint8 green; 36 | in >> green; 37 | 38 | quint8 blue; 39 | in >> blue; 40 | 41 | this->colors[i] = QColor(red, green, blue); 42 | } 43 | } 44 | 45 | bool D1Pal::loadJascPalette(QFile &file) 46 | { 47 | QTextStream txt(&file); 48 | QString line; 49 | QStringList lineParts; 50 | quint16 lineNumber = 0; 51 | 52 | while (!txt.atEnd()) { 53 | line = txt.readLine(); 54 | lineNumber++; 55 | 56 | if (lineNumber == 1 && line != "JASC-PAL") 57 | return false; 58 | if (lineNumber == 3 && line != "256") 59 | return false; 60 | 61 | if (lineNumber <= 3) 62 | continue; 63 | if (lineNumber > 256 + 3) 64 | continue; 65 | 66 | lineParts = line.split(" "); 67 | if (lineParts.size() != 3) { 68 | return false; 69 | } 70 | 71 | quint8 red = lineParts[0].toInt(); 72 | quint8 green = lineParts[1].toInt(); 73 | quint8 blue = lineParts[2].toInt(); 74 | // assert(D1PAL_COLORS == 256); 75 | this->colors[lineNumber - 4] = QColor(red, green, blue); 76 | } 77 | 78 | return lineNumber >= D1PAL_COLORS + 3; 79 | } 80 | 81 | bool D1Pal::save(QString filePath) 82 | { 83 | QFile file = QFile(filePath); 84 | 85 | if (!file.open(QIODevice::WriteOnly)) 86 | return false; 87 | 88 | QDataStream out(&file); 89 | for (int i = 0; i < D1PAL_COLORS; i++) { 90 | QColor color = this->colors[i]; 91 | quint8 byteToWrite; 92 | 93 | byteToWrite = color.red(); 94 | out << byteToWrite; 95 | 96 | byteToWrite = color.green(); 97 | out << byteToWrite; 98 | 99 | byteToWrite = color.blue(); 100 | out << byteToWrite; 101 | } 102 | 103 | if (this->palFilePath == filePath) { 104 | this->modified = false; 105 | } else { 106 | // -- do not update, the user is creating a new one and the original needs to be preserved 107 | // this->modified = false; 108 | // this->palFilePath = filePath; 109 | } 110 | return true; 111 | } 112 | 113 | bool D1Pal::isModified() const 114 | { 115 | return this->modified; 116 | } 117 | 118 | QString D1Pal::getFilePath() 119 | { 120 | return this->palFilePath; 121 | } 122 | 123 | QString D1Pal::getDefaultPath() const 124 | { 125 | return DEFAULT_PATH; 126 | } 127 | 128 | QString D1Pal::getDefaultName() const 129 | { 130 | return DEFAULT_NAME; 131 | } 132 | 133 | QColor D1Pal::getColor(quint8 index) 134 | { 135 | return this->colors[index]; 136 | } 137 | 138 | void D1Pal::setColor(quint8 index, QColor color) 139 | { 140 | this->colors[index] = color; 141 | if (index < 32) 142 | this->origCyclePalette[index] = color; 143 | this->modified = true; 144 | } 145 | 146 | void D1Pal::resetColors() 147 | { 148 | for (int i = 0; i < 32; i++) 149 | this->colors[i] = this->origCyclePalette[i]; 150 | } 151 | 152 | void D1Pal::cycleColors(D1PAL_CYCLE_TYPE type) 153 | { 154 | QColor celColor; 155 | int i; 156 | 157 | switch (type) { 158 | case D1PAL_CYCLE_TYPE::CAVES: 159 | case D1PAL_CYCLE_TYPE::HELL: 160 | celColor = this->colors[1]; 161 | for (i = 1; i < 31; i++) { 162 | this->colors[i] = this->colors[i + 1]; 163 | } 164 | this->colors[i] = celColor; 165 | break; 166 | case D1PAL_CYCLE_TYPE::NEST: 167 | if (--this->currentCycleCounter != 0) 168 | break; 169 | this->currentCycleCounter = 3; 170 | celColor = this->colors[8]; 171 | for (i = 8; i > 1; i--) { 172 | this->colors[i] = this->colors[i - 1]; 173 | } 174 | this->colors[i] = celColor; 175 | 176 | celColor = this->colors[15]; 177 | for (i = 15; i > 9; i--) { 178 | this->colors[i] = this->colors[i - 1]; 179 | } 180 | this->colors[i] = celColor; 181 | break; 182 | case D1PAL_CYCLE_TYPE::CRYPT: 183 | if (--this->currentCycleCounter == 0) { 184 | this->currentCycleCounter = 3; 185 | 186 | celColor = this->colors[15]; 187 | for (i = 15; i > 1; i--) { 188 | this->colors[i] = this->colors[i - 1]; 189 | } 190 | this->colors[i] = celColor; 191 | } 192 | 193 | celColor = this->colors[31]; 194 | for (i = 31; i > 16; i--) { 195 | this->colors[i] = this->colors[i - 1]; 196 | } 197 | this->colors[i] = celColor; 198 | break; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contact@diasurgical.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /source/d1formats/d1til.cpp: -------------------------------------------------------------------------------- 1 | #include "d1til.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define TILE_SIZE (TILE_WIDTH * TILE_HEIGHT) 11 | 12 | bool D1Til::load(QString filePath, D1Min *m) 13 | { 14 | // prepare file data source 15 | QFile file; 16 | // done by the caller 17 | // if (!params.tilFilePath.isEmpty()) { 18 | // filePath = params.tilFilePath; 19 | // } 20 | if (!filePath.isEmpty()) { 21 | file.setFileName(filePath); 22 | if (!file.open(QIODevice::ReadOnly)) { 23 | return false; 24 | } 25 | } 26 | 27 | QByteArray fileData = file.readAll(); 28 | QBuffer fileBuffer(&fileData); 29 | 30 | if (!fileBuffer.open(QIODevice::ReadOnly)) { 31 | return false; 32 | } 33 | 34 | // File size check 35 | auto fileSize = file.size(); 36 | if (fileSize % (2 * TILE_SIZE) != 0) { 37 | qDebug() << "Invalid til-file."; 38 | return false; 39 | } 40 | 41 | this->min = m; 42 | 43 | int tileCount = fileSize / (2 * TILE_SIZE); 44 | 45 | // Read TIL binary data 46 | QDataStream in(&fileBuffer); 47 | in.setByteOrder(QDataStream::LittleEndian); 48 | 49 | this->subtileIndices.clear(); 50 | for (int i = 0; i < tileCount; i++) { 51 | QList subtileIndicesList; 52 | for (int j = 0; j < TILE_SIZE; j++) { 53 | quint16 readWord; 54 | in >> readWord; 55 | subtileIndicesList.append(readWord); 56 | } 57 | this->subtileIndices.append(subtileIndicesList); 58 | } 59 | 60 | this->tilFilePath = filePath; 61 | this->modified = false; 62 | 63 | return true; 64 | } 65 | 66 | bool D1Til::save(const QString &gfxPath) 67 | { 68 | QString filePath = gfxPath; 69 | filePath.chop(3); 70 | filePath += "til"; 71 | 72 | QFile outFile = QFile(filePath); 73 | if (!outFile.open(QIODevice::WriteOnly | QFile::Truncate)) { 74 | QMessageBox::critical(nullptr, "Error", "Failed open file: " + filePath); 75 | return false; 76 | } 77 | 78 | // write to file 79 | QDataStream out(&outFile); 80 | out.setByteOrder(QDataStream::LittleEndian); 81 | for (int i = 0; i < this->subtileIndices.count(); i++) { 82 | QList &subtileIndicesList = this->subtileIndices[i]; 83 | for (int j = 0; j < TILE_SIZE; j++) { 84 | quint16 writeWord = subtileIndicesList[j]; 85 | out << writeWord; 86 | } 87 | } 88 | 89 | this->tilFilePath = filePath; 90 | this->modified = false; 91 | 92 | return true; 93 | } 94 | 95 | QImage D1Til::getTileImage(int tileIndex) 96 | { 97 | if (tileIndex < 0 || tileIndex >= this->subtileIndices.size()) 98 | return QImage(); 99 | 100 | unsigned subtileWidth = this->min->getSubtileWidth() * MICRO_WIDTH; 101 | unsigned subtileHeight = this->min->getSubtileHeight() * MICRO_HEIGHT; 102 | // assert(TILE_WIDTH == 2 && TILE_HEIGHT == 2); 103 | unsigned subtileShiftY = subtileWidth / 4; 104 | QImage tile = QImage(subtileWidth * 2, 105 | subtileHeight + 2 * subtileShiftY, QImage::Format_ARGB32); 106 | tile.fill(Qt::transparent); 107 | QPainter tilePainter(&tile); 108 | // 0 109 | // 2 1 110 | // 3 111 | tilePainter.drawImage(subtileWidth / 2, 0, 112 | this->min->getSubtileImage( 113 | this->subtileIndices.at(tileIndex).at(0))); 114 | 115 | tilePainter.drawImage(subtileWidth, subtileShiftY, 116 | this->min->getSubtileImage( 117 | this->subtileIndices.at(tileIndex).at(1))); 118 | 119 | tilePainter.drawImage(0, subtileShiftY, 120 | this->min->getSubtileImage( 121 | this->subtileIndices.at(tileIndex).at(2))); 122 | 123 | tilePainter.drawImage(subtileWidth / 2, 2 * subtileShiftY, 124 | this->min->getSubtileImage( 125 | this->subtileIndices.at(tileIndex).at(3))); 126 | 127 | tilePainter.end(); 128 | return tile; 129 | } 130 | 131 | QImage D1Til::getFlatTileImage(int tileIndex) 132 | { 133 | if (tileIndex < 0 || tileIndex >= this->subtileIndices.size()) 134 | return QImage(); 135 | 136 | unsigned subtileWidth = this->min->getSubtileWidth() * MICRO_WIDTH; 137 | unsigned subtileHeight = this->min->getSubtileHeight() * MICRO_HEIGHT; 138 | QImage tile = QImage(subtileWidth * TILE_SIZE, subtileHeight, QImage::Format_ARGB32); 139 | tile.fill(Qt::transparent); 140 | QPainter tilePainter(&tile); 141 | 142 | for (int i = 0; i < TILE_SIZE; i++) { 143 | tilePainter.drawImage(subtileWidth * i, 0, 144 | this->min->getSubtileImage( 145 | this->subtileIndices.at(tileIndex).at(i))); 146 | } 147 | 148 | tilePainter.end(); 149 | return tile; 150 | } 151 | 152 | bool D1Til::isModified() const 153 | { 154 | return this->modified; 155 | } 156 | 157 | QString D1Til::getFilePath() 158 | { 159 | return this->tilFilePath; 160 | } 161 | 162 | int D1Til::getTileCount() 163 | { 164 | return this->subtileIndices.count(); 165 | } 166 | 167 | QList &D1Til::getSubtileIndices(int tileIndex) 168 | { 169 | return const_cast &>(this->subtileIndices.at(tileIndex)); 170 | } 171 | 172 | void D1Til::insertTile(int tileIndex, const QList &subtileIndices) 173 | { 174 | this->subtileIndices.insert(tileIndex, subtileIndices); 175 | this->modified = true; 176 | } 177 | 178 | void D1Til::createTile() 179 | { 180 | QList subtileIndices; 181 | 182 | for (int i = 0; i < TILE_SIZE; i++) { 183 | subtileIndices.append(0); 184 | } 185 | this->subtileIndices.append(subtileIndices); 186 | this->modified = true; 187 | } 188 | 189 | void D1Til::removeTile(int tileIndex) 190 | { 191 | this->subtileIndices.removeAt(tileIndex); 192 | this->modified = true; 193 | } 194 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to D1GT (Diablo 1 Graphics Tool) 2 | 3 | Whenever you want to contribute to the project, whether it's by editing a simple file and fixing a typo, or implementing a huge feature which will add something new to the application - be sure that the change you are making is in a direction that the project is heading. You can make sure by either opening an issue and discussing it here first, or by asking on our discord sever, on an appropriate channel, like `#development`. 4 | 5 | ## Communication 6 | 7 | Join our Discord server: [Diasurgical Discord](https://discord.gg/devilutionx) 8 | 9 | ## Creating an issue - guidelines 10 | 11 | Issues are mostly created during the following occassions: 12 | 13 | * There is a weird/undefined behavior of the app, and you're not sure if it's a bug or something that is expected to happen 14 | * There is an obvious bug that for example causes the app to crash 15 | * You're not sure if the implementation you want to make aligns with the project's direction - here you can create an issue to propose your implementation change and see if community approves it 16 | 17 | For questions regarding your implementation change, you can always ask on our Discord, on `#development` channel. 18 | 19 | Please try to do these when creating an issue: 20 | 21 | * Try to prepend your issue's title with the category/component that the issue is related to, for example, if your change is mostly related to LevelCelView, you'd name your issue as follows: `LevelCelView: Modified <...>`. If you're not sure, you can drop the tag, and if the issue is most probably related to every, or many components of the app, you can use `Everywhere:` tag. 22 | * Create only one issue per bug/implementation request 23 | * Thoroughly describe the bug that you witnessed, if possible also add screenshots - there is no general pattern or template on how the issue should look like, so try to give the next person who will take care of it as many hints as possible 24 | * Include your platform name that you've witnessed the bug on, and libraries versions that you're using on your system 25 | * Please refrain from creating issues related to build problems or other support requests. If CI builds the application without any issues during a pull request, the problem is likely on your side. Instead, seek assistance on the #request-support channel on our Discord or initiate a proper Q&A discussion on the [Discussions](https://github.com/diasurgical/d1-graphics-tool/discussions) tab on GitHub. 26 | 27 | ## Creating a PR - guidelines 28 | 29 | Those guidelines won't be necessarily as strictly executed as they should be, but it should be your responsibility to mark all the checks. If you're a future maintainer, or a future frequent contributor, you'd not want to debug a change that's not as descriptive or readable as it should be, at the same time not knowing if it introduced something important or not. So it's better for everyone to stick to the guidelines and always check them before opening a new Pull Request. 30 | 31 | **Please stick to the following rules:** 32 | * When opening a pull request, prepend the title with the category/component that the PR is related to, for example if your PR is mostly changing LevelCelView component, do as follows: `LevelCelView: Modified <...>`. If you're not sure, you can drop the tag, and if the PR is most probably related to every, or many components of the app, you can use `Everywhere:` tag. 33 | * Split your changes to separate, small commits. If one commit changes resolution of the window, there shouldn't be an implementation of a button that adds rectangles to a view in the same commit. 34 | * Write PR title and all commit messages in imperative mood, for example, don't do this: ("LevelCelView: Changed size of the window"), rather do this: ("LevelCelView: Change size of the window). 35 | * Write everything in proper English, with proper punctuation 36 | * Check spelling of your code and commit messages, and PR title 37 | * Do not use slang inside the code or commit messages/PR title 38 | * If you're adding a method to a class, it's encouraged to add documentation. By default we use Doxygen to generate documentation for our code. For examples how to describe your newly added methods you can check `config.cpp` file. Documentation should also be included in separate commits. 39 | * If a change happened to a line that has been introduced in commit X, do not create a new commit Y, amend your changes to commit X and then push with force. 40 | * Always make sure you're rebased on master to avoid conflicts with other contributors 41 | * Try to wrap your commit messages on 72 character in a single line 42 | * Right now we have a basic formatting check in CI, so to make sure you're in line with it, you can run `clang-format` on your files with `.clang-format` file in `source/` directory of the project. 43 | * Do not resolve comments by yourself, if your change satisfies reviewer's request - they will resolve their comments, just let them know in an answer that the request has been satisfied. 44 | 45 | ## Behavior etiquette 46 | 47 | We're all working on this project in our free time, many of us are frustrated after work or after a day of problems. We try to keep it purely technical, since everyone is from a different country, culture and everyone has a set of different beliefs. But to maintain things in a clear, transparent way we hold ourselves and respect a set of rules, so it would be good that everyone who joins the project, or is a frequent contributor would stick to those: 48 | 49 | * Do not advertise your personal politics or religious beliefs anywhere in the code, or on our github repository. Do not add changes related to those topics. 50 | * Do not curse extensively in pull requests/issues 51 | * Do not bully someone for their github profile, bad code, profile picture, beliefs discovered on some personal page or anything that they done in the past 52 | * If you have questions regarding code during a review, it's good to assume someone had a reason to do something, and it's better to ask "Why is it here, shouldn't we do XYZ?" rather than "What the f... is that garbage?" 53 | * If your contribution is not getting attention, you can always ask people to take a look on the `#development` channel on our Discord, maybe someone will have some time and will give you a review. Just don't expect someone to do that immediately and don't spam the link, that may annoy people. 54 | * Try to treat everyone with respect, this way we can develop things with professionalism, and in a faster, straight to the point way 55 | 56 | If you see someone breaking the rules, contact one of the following maintainers that take a part in D1GT: 57 | - [@AJenbo](https://github.com/AJenbo) 58 | - [@StephenCWills](https://github.com/StephenCWills) 59 | -------------------------------------------------------------------------------- /source/dialogs/openasdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "openasdialog.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "ui_openasdialog.h" 7 | 8 | #include "mainwindow.h" 9 | 10 | OpenAsDialog::OpenAsDialog(QWidget *parent) 11 | : QDialog(parent) 12 | , ui(new Ui::OpenAsDialog) 13 | { 14 | ui->setupUi(this); 15 | } 16 | 17 | OpenAsDialog::~OpenAsDialog() 18 | { 19 | delete ui; 20 | } 21 | 22 | void OpenAsDialog::initialize() 23 | { 24 | // clear the input fields 25 | ui->inputFileEdit->setText(""); 26 | ui->isTilesetAutoRadioButton->setChecked(true); 27 | // - celSettingsGroupBox 28 | ui->celWidthEdit->setText("0"); 29 | ui->celClippedAutoRadioButton->setChecked(true); 30 | // - tilSettingsGroupBox 31 | ui->tilFileEdit->setText(""); 32 | ui->minFileEdit->setText(""); 33 | ui->solFileEdit->setText(""); 34 | ui->ampFileEdit->setText(""); 35 | ui->minWidthEdit->setText("0"); 36 | ui->minHeightEdit->setText("0"); 37 | 38 | this->update(); 39 | } 40 | 41 | void OpenAsDialog::update() 42 | { 43 | bool hasInputFile = !ui->inputFileEdit->text().isEmpty(); 44 | bool isTileset = ui->isTilesetYesRadioButton->isChecked() || (ui->isTilesetAutoRadioButton->isChecked() && !ui->tilFileEdit->text().isEmpty()); 45 | 46 | ui->celSettingsGroupBox->setEnabled(hasInputFile && !isTileset); 47 | ui->tilSettingsGroupBox->setEnabled(hasInputFile && isTileset); 48 | } 49 | 50 | void OpenAsDialog::on_inputFileBrowseButton_clicked() 51 | { 52 | MainWindow *qw = (MainWindow *)this->parentWidget(); 53 | QString openFilePath = qw->fileDialog(FILE_DIALOG_MODE::OPEN, "Open Graphics", "CEL/CL2/CLX Files (*.cel *.CEL *.cl2 *.CL2 *.clx *.CLX)"); 54 | 55 | if (openFilePath.isEmpty()) 56 | return; 57 | 58 | ui->inputFileEdit->setText(openFilePath); 59 | // activate optional fields based on the extension 60 | if (ui->isTilesetAutoRadioButton->isChecked()) { 61 | bool isTileset = false; 62 | QString basePath; 63 | QString tilFilePath; 64 | QString minFilePath; 65 | QString solFilePath; 66 | if (openFilePath.toLower().endsWith(".cel")) { 67 | // If a SOL, MIN and TIL files exists then preset them 68 | basePath = openFilePath; 69 | basePath.chop(3); 70 | tilFilePath = basePath + "til"; 71 | minFilePath = basePath + "min"; 72 | solFilePath = basePath + "sol"; 73 | isTileset = QFileInfo::exists(tilFilePath) && QFileInfo::exists(minFilePath) && QFileInfo::exists(solFilePath); 74 | } 75 | if (isTileset) { 76 | ui->tilFileEdit->setText(tilFilePath); 77 | ui->minFileEdit->setText(minFilePath); 78 | ui->solFileEdit->setText(solFilePath); 79 | QString ampFilePath = basePath + "amp"; 80 | if (QFileInfo::exists(ampFilePath)) { 81 | ui->ampFileEdit->setText(ampFilePath); 82 | } else { 83 | ui->ampFileEdit->setText(""); 84 | } 85 | } else { 86 | ui->tilFileEdit->setText(""); 87 | ui->minFileEdit->setText(""); 88 | ui->solFileEdit->setText(""); 89 | ui->ampFileEdit->setText(""); 90 | } 91 | } 92 | 93 | this->update(); 94 | } 95 | 96 | void OpenAsDialog::on_isTilesetYesRadioButton_toggled(bool checked) 97 | { 98 | this->update(); 99 | } 100 | 101 | void OpenAsDialog::on_isTilesetNoRadioButton_toggled(bool checked) 102 | { 103 | this->update(); 104 | } 105 | 106 | void OpenAsDialog::on_isTilesetAutoRadioButton_toggled(bool checked) 107 | { 108 | this->update(); 109 | } 110 | 111 | void OpenAsDialog::on_tilFileBrowseButton_clicked() 112 | { 113 | MainWindow *qw = (MainWindow *)this->parentWidget(); 114 | QString openFilePath = qw->fileDialog(FILE_DIALOG_MODE::OPEN, "Open TIL file", "TIL Files (*.til *.TIL)"); 115 | 116 | if (openFilePath.isEmpty()) 117 | return; 118 | 119 | ui->tilFileEdit->setText(openFilePath); 120 | } 121 | 122 | void OpenAsDialog::on_minFileBrowseButton_clicked() 123 | { 124 | MainWindow *qw = (MainWindow *)this->parentWidget(); 125 | QString openFilePath = qw->fileDialog(FILE_DIALOG_MODE::OPEN, "Open MIN file", "MIN Files (*.min *.MIN)"); 126 | 127 | if (openFilePath.isEmpty()) 128 | return; 129 | 130 | ui->minFileEdit->setText(openFilePath); 131 | } 132 | 133 | void OpenAsDialog::on_solFileBrowseButton_clicked() 134 | { 135 | MainWindow *qw = (MainWindow *)this->parentWidget(); 136 | QString openFilePath = qw->fileDialog(FILE_DIALOG_MODE::OPEN, "Open SOL file", "SOL Files (*.sol *.SOL)"); 137 | 138 | if (openFilePath.isEmpty()) 139 | return; 140 | 141 | ui->solFileEdit->setText(openFilePath); 142 | } 143 | 144 | void OpenAsDialog::on_ampFileBrowseButton_clicked() 145 | { 146 | MainWindow *qw = (MainWindow *)this->parentWidget(); 147 | QString openFilePath = qw->fileDialog(FILE_DIALOG_MODE::OPEN, "Open AMP file", "AMP Files (*.amp *.AMP)"); 148 | 149 | if (openFilePath.isEmpty()) 150 | return; 151 | 152 | ui->ampFileEdit->setText(openFilePath); 153 | } 154 | 155 | void OpenAsDialog::on_openButton_clicked() 156 | { 157 | OpenAsParam params; 158 | params.celFilePath = ui->inputFileEdit->text(); 159 | if (params.celFilePath.isEmpty()) { 160 | QMessageBox::warning(this, "Warning", "Input file is missing, please choose an input file."); 161 | return; 162 | } 163 | if (ui->isTilesetYesRadioButton->isChecked()) { 164 | params.isTileset = OPEN_TILESET_TYPE::Yes; 165 | } else if (ui->isTilesetNoRadioButton->isChecked()) { 166 | params.isTileset = OPEN_TILESET_TYPE::No; 167 | } else { 168 | params.isTileset = OPEN_TILESET_TYPE::Auto; 169 | } 170 | // cel/cl2: clipped, width 171 | params.celWidth = this->ui->celWidthEdit->text().toUShort(); 172 | if (ui->celClippedYesRadioButton->isChecked()) { 173 | params.clipped = OPEN_CLIPPED_TYPE::Yes; 174 | } else if (ui->celClippedNoRadioButton->isChecked()) { 175 | params.clipped = OPEN_CLIPPED_TYPE::No; 176 | } else { 177 | params.clipped = OPEN_CLIPPED_TYPE::Auto; 178 | } 179 | params.tilFilePath = ui->tilFileEdit->text(); 180 | params.minFilePath = ui->minFileEdit->text(); 181 | params.solFilePath = ui->solFileEdit->text(); 182 | params.ampFilePath = ui->ampFileEdit->text(); 183 | params.minWidth = ui->minWidthEdit->text().toUShort(); 184 | params.minHeight = ui->minHeightEdit->text().toUShort(); 185 | 186 | MainWindow *qw = (MainWindow *)this->parentWidget(); 187 | this->close(); 188 | 189 | qw->openFile(params); 190 | } 191 | 192 | void OpenAsDialog::on_openCancelButton_clicked() 193 | { 194 | this->close(); 195 | } 196 | -------------------------------------------------------------------------------- /source/mainwindow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "d1formats/d1gfx.h" 12 | #include "d1formats/d1min.h" 13 | #include "d1formats/d1sol.h" 14 | #include "d1formats/d1til.h" 15 | #include "d1formats/d1trn.h" 16 | #include "dialogs/exportdialog.h" 17 | #include "dialogs/importdialog.h" 18 | #include "dialogs/openasdialog.h" 19 | #include "dialogs/settingsdialog.h" 20 | #include "palette/d1pal.h" 21 | #include "palette/d1palhits.h" 22 | #include "undostack/undostack.h" 23 | #include "views/celview.h" 24 | #include "views/levelcelview.h" 25 | #include "widgets/palettewidget.h" 26 | 27 | #define D1_GRAPHICS_TOOL_TITLE "Diablo 1 Graphics Tool" 28 | #define D1_GRAPHICS_TOOL_VERSION "1.1.0" 29 | 30 | enum class FILE_DIALOG_MODE { 31 | OPEN, // open existing 32 | SAVE_CONF, // save with confirm 33 | SAVE_NO_CONF, // save without confirm 34 | }; 35 | 36 | enum class IMAGE_FILE_MODE { 37 | FRAME, // open as frames 38 | SUBTILE, // open as subtiles 39 | TILE, // open as tiles 40 | AUTO, // auto-detect 41 | }; 42 | 43 | namespace Ui { 44 | class MainWindow; 45 | } 46 | 47 | namespace mw { 48 | bool QuestionDiscardChanges(bool isModified, QString filePath); 49 | } // namespace mw 50 | 51 | class MainWindow : public QMainWindow { 52 | Q_OBJECT 53 | 54 | public: 55 | explicit MainWindow(); 56 | ~MainWindow(); 57 | 58 | void setPal(const QString &); 59 | void setTrn(const QString &); 60 | void setTrnUnique(const QString &); 61 | 62 | void openFile(const OpenAsParam ¶ms); 63 | void openImageFiles(IMAGE_FILE_MODE mode, QStringList filePaths, bool append); 64 | void openPalFiles(const QStringList &filePaths, PaletteWidget *widget) const; 65 | void openFontFile(QString filePath, QColor renderColor, int pointSize, uint symbolPrefix); 66 | void saveFile(const QString &gfxPath); 67 | 68 | void paletteWidget_callback(PaletteWidget *widget, PWIDGET_CALLBACK_TYPE type); 69 | 70 | void nextPaletteCycle(D1PAL_CYCLE_TYPE type); 71 | void resetPaletteCycle(); 72 | void updateStatusBar(const QString &status, const QString &styleSheet); 73 | 74 | QString getLastFilePath(); 75 | QString fileDialog(FILE_DIALOG_MODE mode, const char *title, const char *filter); 76 | QStringList filesDialog(const char *title, const char *filter); 77 | PaletteWidget *trnWidget() 78 | { 79 | return m_trnWidget; 80 | } 81 | PaletteWidget *uniqTrnWidget() 82 | { 83 | return m_trnUniqueWidget; 84 | } 85 | PaletteWidget *paletteWidget() 86 | { 87 | return m_palWidget; 88 | } 89 | 90 | static bool hasImageUrl(const QMimeData *mimeData); 91 | 92 | protected: 93 | void closeEvent(QCloseEvent *event) override; 94 | 95 | private: 96 | void updateWindow(); 97 | 98 | void addFrames(bool append); 99 | void addSubtiles(bool append); 100 | void addTiles(bool append); 101 | 102 | public slots: 103 | void actionUndo_triggered(); 104 | void actionRedo_triggered(); 105 | 106 | void actionInsertFrame_triggered(); 107 | void actionAddFrame_triggered(); 108 | void actionReplaceFrame_triggered(); 109 | void actionDelFrame_triggered(); 110 | 111 | void actionCreateSubtile_triggered(); 112 | void actionInsertSubtile_triggered(); 113 | void actionAddSubtile_triggered(); 114 | void actionReplaceSubtile_triggered(); 115 | void actionCloneSubtile_triggered(); 116 | void actionDelSubtile_triggered(); 117 | 118 | void actionCreateTile_triggered(); 119 | void actionCloneTile_triggered(); 120 | void actionInsertTile_triggered(); 121 | void actionAddTile_triggered(); 122 | void actionReplaceTile_triggered(); 123 | void actionDelTile_triggered(); 124 | 125 | // slots used for UndoMacro signals 126 | void setupUndoMacroWidget(std::unique_ptr &userData, enum OperationType opType); 127 | void updateUndoMacroWidget(bool &result); 128 | 129 | void buildRecentFilesMenu(); 130 | void addRecentFile(QString filePath); 131 | void on_actionClear_History_triggered(); 132 | 133 | private slots: 134 | void actionNewSprite_triggered(); 135 | void actionNewTileset_triggered(); 136 | 137 | void on_actionOpen_triggered(); 138 | void on_actionOpenAs_triggered(); 139 | void on_actionImport_triggered(); 140 | void on_actionSave_triggered(); 141 | void on_actionSaveAs_triggered(); 142 | bool isOkToQuit(); 143 | void on_actionClose_triggered(); 144 | void closeAllElements(); 145 | void on_actionExport_triggered(); 146 | void on_actionSettings_triggered(); 147 | void on_actionQuit_triggered(); 148 | 149 | void on_actionRegroupFrames_triggered(); 150 | 151 | void on_actionReportUse_Tileset_triggered(); 152 | void on_actionResetFrameTypes_Tileset_triggered(); 153 | void on_actionCleanupFrames_Tileset_triggered(); 154 | void on_actionCleanupSubtiles_Tileset_triggered(); 155 | void on_actionCompressTileset_Tileset_triggered(); 156 | void on_actionSortFrames_Tileset_triggered(); 157 | void on_actionSortSubtiles_Tileset_triggered(); 158 | 159 | void on_actionAbout_triggered(); 160 | void on_actionAbout_Qt_triggered(); 161 | 162 | void dragEnterEvent(QDragEnterEvent *event); 163 | void dragMoveEvent(QDragMoveEvent *event); 164 | void dropEvent(QDropEvent *event); 165 | 166 | private: 167 | Ui::MainWindow *ui; 168 | QString lastFilePath; 169 | 170 | QMenu newMenu = QMenu("New"); 171 | QMenu frameMenu = QMenu("Frame"); 172 | QMenu subtileMenu = QMenu("Tile"); 173 | QMenu tileMenu = QMenu("MegaTile"); 174 | 175 | std::shared_ptr undoStack; 176 | QAction *undoAction; 177 | QAction *redoAction; 178 | 179 | QPointer celView; 180 | QPointer levelCelView; 181 | 182 | PaletteWidget *m_palWidget = nullptr; 183 | PaletteWidget *m_trnWidget = nullptr; 184 | PaletteWidget *m_trnUniqueWidget = nullptr; 185 | 186 | OpenAsDialog openAsDialog = OpenAsDialog(this); 187 | SettingsDialog settingsDialog = SettingsDialog(this); 188 | ImportDialog importDialog = ImportDialog(this); 189 | ExportDialog exportDialog = ExportDialog(this); 190 | 191 | QPointer gfx; 192 | QPointer min; 193 | QPointer til; 194 | QPointer sol; 195 | QPointer amp; 196 | 197 | std::unique_ptr m_progressDialog; 198 | 199 | // Palette hits are instantiated in main window to make them available to the three PaletteWidgets 200 | QPointer palHits; 201 | 202 | int m_currProgDialogPos { 0 }; 203 | enum OperationType m_currMacroOpType; 204 | }; 205 | -------------------------------------------------------------------------------- /source/d1formats/d1celtileset.cpp: -------------------------------------------------------------------------------- 1 | #include "d1celtileset.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "d1celtilesetframe.h" 10 | 11 | D1CEL_FRAME_TYPE guessFrameType(QByteArray &rawFrameData) 12 | { 13 | if (rawFrameData.size() == 544 || rawFrameData.size() == 800) { 14 | const int leftZeros[32] = { 15 | 0, 1, 8, 9, 24, 25, 48, 49, 80, 81, 120, 121, 168, 169, 224, 225, 16 | 288, 289, 348, 349, 400, 401, 444, 445, 480, 481, 508, 509, 528, 529, 540, 541 17 | }; 18 | 19 | for (int i = 0; i < 32; i++) { 20 | std::uint8_t byte = rawFrameData[leftZeros[i]]; 21 | if (byte != 0) 22 | break; 23 | if (i == 15 && rawFrameData.size() == 800) 24 | return D1CEL_FRAME_TYPE::LeftTrapezoid; 25 | if (i == 31 && rawFrameData.size() == 544) 26 | return D1CEL_FRAME_TYPE::LeftTriangle; 27 | } 28 | 29 | const int rightZeros[32] = { 30 | 2, 3, 14, 15, 34, 35, 62, 63, 98, 99, 142, 143, 194, 195, 254, 255, 31 | 318, 319, 374, 375, 422, 423, 462, 463, 494, 495, 518, 519, 534, 535, 542, 543 32 | }; 33 | 34 | for (int i = 0; i < 32; i++) { 35 | std::uint8_t byte = rawFrameData[rightZeros[i]]; 36 | if (byte != 0) 37 | break; 38 | if (i == 15 && rawFrameData.size() == 800) 39 | return D1CEL_FRAME_TYPE::RightTrapezoid; 40 | if (i == 31 && rawFrameData.size() == 544) 41 | return D1CEL_FRAME_TYPE::RightTriangle; 42 | } 43 | } 44 | 45 | if (rawFrameData.size() == 1024) { 46 | return D1CEL_FRAME_TYPE::Square; 47 | } 48 | 49 | return D1CEL_FRAME_TYPE::TransparentSquare; 50 | } 51 | 52 | bool D1CelTileset::load(D1Gfx &gfx, std::map &celFrameTypes, QString filePath, const OpenAsParam ¶ms) 53 | { 54 | // prepare file data source 55 | QFile file; 56 | // done by the caller 57 | // if (!params.celFilePath.isEmpty()) { 58 | // filePath = params.celFilePath; 59 | // } 60 | if (!filePath.isEmpty()) { 61 | file.setFileName(filePath); 62 | if (!file.open(QIODevice::ReadOnly)) { 63 | return false; 64 | } 65 | } 66 | 67 | QByteArray fileData = file.readAll(); 68 | QBuffer fileBuffer(&fileData); 69 | 70 | if (!fileBuffer.open(QIODevice::ReadOnly)) { 71 | return false; 72 | } 73 | 74 | // Read CEL binary data 75 | QDataStream in(&fileBuffer); 76 | in.setByteOrder(QDataStream::LittleEndian); 77 | 78 | // File size check 79 | int numFrames = 0; 80 | auto fileSize = fileBuffer.size(); 81 | if (fileSize != 0) { 82 | // CEL HEADER CHECKS 83 | if (fileSize < 4) { 84 | qDebug() << "Level-cel-file is too small."; 85 | return false; 86 | } 87 | 88 | // Read first DWORD 89 | quint32 readDword; 90 | in >> readDword; 91 | 92 | numFrames = readDword; 93 | 94 | // Trying to find file size in CEL header 95 | if (fileSize < (4 + numFrames * 4 + 4)) { 96 | qDebug() << "Header of the level-cel-file is too small."; 97 | return false; 98 | } 99 | 100 | fileBuffer.seek(numFrames * 4 + 4); 101 | quint32 fileSizeDword; 102 | in >> fileSizeDword; 103 | 104 | if (fileSize != fileSizeDword) { 105 | qDebug() << "Invalid level-cel-file header."; 106 | return false; 107 | } 108 | } 109 | 110 | gfx.groupFrameIndices.clear(); 111 | gfx.groupFrameIndices.append(qMakePair(0, numFrames - 1)); 112 | gfx.isTileset_ = true; 113 | 114 | // CEL FRAMES OFFSETS CALCULATION 115 | QList> frameOffsets; 116 | for (int i = 1; i <= numFrames; i++) { 117 | fileBuffer.seek(i * 4); 118 | quint32 celFrameStartOffset; 119 | in >> celFrameStartOffset; 120 | quint32 celFrameEndOffset; 121 | in >> celFrameEndOffset; 122 | 123 | frameOffsets.append(qMakePair(celFrameStartOffset, celFrameEndOffset)); 124 | } 125 | 126 | // BUILDING {CEL FRAMES} 127 | gfx.frames.clear(); 128 | for (int i = 0; i < frameOffsets.count(); i++) { 129 | const auto &offset = frameOffsets[i]; 130 | fileBuffer.seek(offset.first); 131 | QByteArray celFrameRawData = fileBuffer.read(offset.second - offset.first); 132 | D1CEL_FRAME_TYPE frameType; 133 | auto iter = celFrameTypes.find(i + 1); 134 | if (iter != celFrameTypes.end()) { 135 | frameType = iter->second; 136 | } else { 137 | qDebug() << "Unknown frame type for frame " << i + 1; 138 | frameType = guessFrameType(celFrameRawData); 139 | } 140 | D1GfxFrame frame; 141 | if (!D1CelTilesetFrame::load(frame, frameType, celFrameRawData, params)) { 142 | // TODO: log + add placeholder? 143 | continue; 144 | } 145 | gfx.frames.append(frame); 146 | } 147 | gfx.gfxFilePath = filePath; 148 | return true; 149 | } 150 | 151 | bool D1CelTileset::writeFileData(D1Gfx &gfx, QFile &outFile) 152 | { 153 | const int numFrames = gfx.getFrameCount(); 154 | 155 | // calculate header size 156 | int headerSize = 4 + numFrames * 4 + 4; 157 | 158 | // estimate data size 159 | int maxSize = headerSize; 160 | maxSize += MICRO_WIDTH * MICRO_HEIGHT * numFrames; 161 | 162 | QByteArray fileData; 163 | fileData.append(maxSize, 0); 164 | 165 | quint8 *buf = (quint8 *)fileData.data(); 166 | *(quint32 *)&buf[0] = SwapLE32(numFrames); 167 | 168 | quint8 *pBuf = &buf[headerSize]; 169 | for (int ii = 0; ii < numFrames; ii++) { 170 | *(quint32 *)&buf[(ii + 1) * sizeof(quint32)] = SwapLE32(pBuf - buf); 171 | 172 | pBuf = D1CelTilesetFrame::writeFrameData(*gfx.getFrame(ii), pBuf); 173 | } 174 | 175 | *(quint32 *)&buf[(numFrames + 1) * sizeof(quint32)] = SwapLE32(pBuf - buf); 176 | 177 | // write to file 178 | QDataStream out(&outFile); 179 | out.writeRawData((char *)buf, pBuf - buf); 180 | 181 | return true; 182 | } 183 | 184 | bool D1CelTileset::save(D1Gfx &gfx, const QString &gfxPath) 185 | { 186 | QFile outFile = QFile(gfxPath); 187 | if (!outFile.open(QIODevice::WriteOnly | QFile::Truncate)) { 188 | QMessageBox::critical(nullptr, "Error", "Failed open file: " + gfxPath); 189 | return false; 190 | } 191 | 192 | bool success = D1CelTileset::writeFileData(gfx, outFile); 193 | 194 | if (success) { 195 | gfx.modified = false; 196 | gfx.gfxFilePath = gfxPath; 197 | } 198 | return success; 199 | } 200 | -------------------------------------------------------------------------------- /source/views/levelcelview.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "celview.h" 20 | #include "d1formats/d1amp.h" 21 | #include "d1formats/d1gfx.h" 22 | #include "d1formats/d1min.h" 23 | #include "d1formats/d1sol.h" 24 | #include "d1formats/d1til.h" 25 | #include "undostack/undostack.h" 26 | #include "widgets/leveltabframewidget.h" 27 | #include "widgets/leveltabsubtilewidget.h" 28 | #include "widgets/leveltabtilewidget.h" 29 | 30 | namespace Ui { 31 | class LevelCelView; 32 | } // namespace Ui 33 | 34 | enum class IMAGE_FILE_MODE; 35 | 36 | enum class TILESET_MODE { 37 | FREE, // Free navigation 38 | SUBTILE, // Subtile editing 39 | TILE, // Edit tile 40 | }; 41 | 42 | enum class IMAGE_TYPE { 43 | NONE, 44 | FRAME, 45 | SUBTILE, 46 | TILE 47 | }; 48 | 49 | class LevelCelView : public QWidget { 50 | Q_OBJECT 51 | 52 | public: 53 | explicit LevelCelView(std::shared_ptr us, QWidget *parent = nullptr); 54 | ~LevelCelView(); 55 | 56 | void initialize(D1Gfx *gfx, D1Min *min, D1Til *til, D1Sol *sol, D1Amp *amp); 57 | 58 | [[nodiscard]] int getCurrentFrameIndex() const 59 | { 60 | return this->currentFrameIndex; 61 | } 62 | [[nodiscard]] int getCurrentSubtileIndex() const 63 | { 64 | return this->currentSubtileIndex; 65 | } 66 | [[nodiscard]] int getCurrentTileIndex() const 67 | { 68 | return this->currentTileIndex; 69 | } 70 | 71 | void framePixelClicked(unsigned x, unsigned y); 72 | 73 | void insertImageFiles(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); 74 | 75 | void sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); 76 | 77 | void sendReplaceCurrentFrameCmd(const QString &imagefilePath); 78 | void replaceCurrentFrame(int frameIdx, const QImage &image); 79 | 80 | void sendRemoveFrameCmd(); 81 | void removeCurrentFrame(int index); 82 | 83 | void createSubtile(); 84 | void cloneSubtile(); 85 | void replaceCurrentSubtile(const QString &imagefilePath); 86 | void removeCurrentSubtile(); 87 | 88 | void createTile(); 89 | void cloneTile(); 90 | void replaceCurrentTile(const QString &imagefilePath); 91 | void removeCurrentTile(); 92 | 93 | void reportUsage(); 94 | void resetFrameTypes(); 95 | void cleanupFrames(); 96 | void cleanupSubtiles(); 97 | void cleanupTileset(); 98 | void compressTileset(); 99 | void sortFrames(); 100 | void sortSubtiles(); 101 | 102 | void displayFrame(); 103 | 104 | IMAGE_TYPE checkImageType(unsigned int x, unsigned int y); 105 | 106 | private: 107 | void update(); 108 | void collectFrameUsers(int frameIndex, QList &users) const; 109 | void collectSubtileUsers(int subtileIndex, QList &users) const; 110 | void insertFrames(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); 111 | void insertFrames(int index, const QImage &image); 112 | void insertSubtile(int subtileIndex, const QImage &image); 113 | void insertSubtiles(IMAGE_FILE_MODE mode, int index, const QImage &image); 114 | void insertSubtiles(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); 115 | void insertSubtiles(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); 116 | void insertTile(int tileIndex, const QImage &image); 117 | void insertTiles(IMAGE_FILE_MODE mode, int index, const QImage &image); 118 | void insertTiles(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); 119 | void insertTiles(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); 120 | void assignSubtiles(const QImage &image, int tileIndex, int subtileIndex); 121 | void removeFrame(int frameIndex); 122 | void removeSubtile(int subtileIndex); 123 | void removeUnusedFrames(QString &report); 124 | void removeUnusedSubtiles(QString &report); 125 | void reuseFrames(QString &report); 126 | void reuseSubtiles(QString &report); 127 | bool sortFrames_impl(); 128 | bool sortSubtiles_impl(); 129 | 130 | signals: 131 | void frameRefreshed(); 132 | void colorIndexClicked(quint8); 133 | 134 | private slots: 135 | void on_firstFrameButton_clicked(); 136 | void on_previousFrameButton_clicked(); 137 | void on_nextFrameButton_clicked(); 138 | void on_lastFrameButton_clicked(); 139 | void on_frameIndexEdit_returnPressed(); 140 | 141 | void on_firstSubtileButton_clicked(); 142 | void on_previousSubtileButton_clicked(); 143 | void on_nextSubtileButton_clicked(); 144 | void on_lastSubtileButton_clicked(); 145 | void on_subtileIndexEdit_returnPressed(); 146 | 147 | void on_firstTileButton_clicked(); 148 | void on_previousTileButton_clicked(); 149 | void on_nextTileButton_clicked(); 150 | void on_lastTileButton_clicked(); 151 | void on_tileIndexEdit_returnPressed(); 152 | 153 | void on_minFrameHeightEdit_returnPressed(); 154 | 155 | void on_zoomOutButton_clicked(); 156 | void on_zoomInButton_clicked(); 157 | void on_zoomEdit_returnPressed(); 158 | 159 | void on_playDelayEdit_textChanged(const QString &text); 160 | void on_playButton_clicked(); 161 | void on_stopButton_clicked(); 162 | 163 | void on_addTileButton_clicked(); 164 | void on_cloneTileButton_clicked(); 165 | void on_addSubTileButton_clicked(); 166 | void on_cloneSubTileButton_clicked(); 167 | 168 | void playGroup(); 169 | 170 | void dragEnterEvent(QDragEnterEvent *event); 171 | void dragMoveEvent(QDragMoveEvent *event); 172 | void dropEvent(QDropEvent *event); 173 | 174 | void ShowContextMenu(const QPoint &pos); 175 | void insertFrame(int index, QImage image); 176 | 177 | private: 178 | std::shared_ptr undoStack; 179 | Ui::LevelCelView *ui; 180 | CelScene *celScene; 181 | LevelTabTileWidget *tabTileWidget = new LevelTabTileWidget(); 182 | LevelTabSubTileWidget *tabSubTileWidget = new LevelTabSubTileWidget(); 183 | LevelTabFrameWidget *tabFrameWidget = new LevelTabFrameWidget(); 184 | 185 | TILESET_MODE mode = TILESET_MODE::FREE; 186 | int editIndex = 0; 187 | 188 | std::stack>> tilesAndFramesIdxStack; // stores tile index + frame indices list index 189 | 190 | D1Gfx *gfx; 191 | D1Min *min; 192 | D1Til *til; 193 | D1Sol *sol; 194 | D1Amp *amp; 195 | int currentFrameIndex = 0; 196 | int currentSubtileIndex = 0; 197 | int currentTileIndex = 0; 198 | quint8 currentZoomFactor = 1; 199 | quint16 currentPlayDelay = 50; 200 | 201 | QTimer playTimer; 202 | }; 203 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## 1.1.0 - 2024-12-14 10 | ### Fixed 11 | - Incorrect CLX header 12 | - Crash when trying to fetch invalid offset 13 | 14 | ### Changed 15 | - Rename "Add " to "Append " 16 | - Improve color selector 17 | 18 | ### Added 19 | - Support for empty sprite frames 20 | - Support for importing symbols from fonts as a spritesheet 21 | - Display mouse cordinates 22 | - More actions can now be undon/redon 23 | 24 | ## 1.0.1 - 2023-11-09 25 | ### Fixed 26 | - Windows: No longer requires MSVC installation to run. 27 | - Linux: Now depends on the correct version of Qt. 28 | - Linux: Settings are now saved correctly, and recent paths are remembered. 29 | - Frame counter no longer resets when only one frame exists. 30 | - Corrected the issue of the wrong frame being displayed in tile mode. 31 | 32 | ### Changed 33 | - Aligned the tileset naming convention with other projects. 34 | 35 | ### Added 36 | - Introduced an alert for users when an image doesn't fit in the tileset. 37 | - Users can now drag the view using the middle mouse button. 38 | 39 | ## 1.0.0 - 2023-04-12 40 | ### Added 41 | - Create new sprites or tilesets. 42 | - Ability to save graphics in Diablo 1 formats. 43 | - Ability to add, insert, delete and replace frames. 44 | - Ability to modify the tiles and subtiles. 45 | - Ability to create, add, insert, delete or replace tiles and subtiles. 46 | - Ability to optimize tilesets. 47 | - View and edit tilset properties 48 | - Subtile height is now editable. 49 | - Export to any image format supported by Qt (JPEG, WEBP, etc.). 50 | - Option to limit the range of exported items. 51 | - Context menu to undo/redo modifications of the palette/translation. 52 | - Drag and drop support. 53 | - Recent files list. 54 | - Icon buttons to create, load, and save palette/translation in place. 55 | - Open As menu option to open bugged files. 56 | (use width 96 to open wlbat.cl2, whbat.cl2 and wmbat.cl2 graphics of the warrior) 57 | - File dialogs start from the last used folder/file (even after restart). 58 | - Configurable playback speed. 59 | - Palette cycling animation of Diablo 1 and Hellfire. 60 | - Button to apply trn-adjustments of the game (done to normal monster-trns). 61 | 62 | ### Fixed 63 | - Memory leaks. 64 | - A bunch of bug fixes. 65 | 66 | ## 0.5.0 - 2021-08-12 67 | ### Added 68 | - Color palette (PAL) write support. 69 | - Color translation (TRN) write support. 70 | - Multi-selection support in the palette widgets. 71 | - Color editing in the palette widget. 72 | - Translation editing in the palette widgets. 73 | - "Show translated colors" display filter for color translations palette widgets. 74 | - CEL level tiles can now be clicked to select the corresponding sub-tile. 75 | - CEL level sub-tiles can now be clicked to select the corresponding frame. 76 | - CEL/CL2 frames can now be clicked to select the corresponding color in the palette widgets. 77 | - Cycling through tiles, sub-tiles, frame groups and frames is now allowed when clicking previous/next on first/last item. 78 | - New setting for palette default color. 79 | - New setting for selection border color. 80 | - Tooltip to display full path of PAL/TRN files when hovering the path dropdown list. 81 | - Application icon. 82 | 83 | ### Changed 84 | - Qt Framework updated to 6.1.2. 85 | - Palette view is replaced by three palette widgets (one for the palette and two for translations). 86 | - Palette hits are now displayed in the same graphic view as colors through a display mask mechanism. 87 | - Translation 1 and 2 have been swapped and renamed "Translation" and "Unique translation"; unique translation applies first. 88 | 89 | ### Removed 90 | - town.pal (_town.pal) from the application resource file. 91 | 92 | ### Fixed 93 | - CEL/CL2 group and frame button alignments. 94 | - Level CEL tile, sub-tile and frame button alignments. 95 | 96 | ## 0.4.1 - 2021-03-11 97 | ### Changed 98 | - Qt Framework updated to 5.15.2 LTS. 99 | 100 | ### Fixed 101 | - CL2 loading issue, the top pixel line of CL2 frames was not loaded nor rendered. 102 | 103 | 104 | ## 0.4.0 - 2020-01-08 105 | ### Added 106 | - Palette hits view for all frames and current frame. 107 | - Palette translation hits view for all frames and current frame. 108 | - Palette hits view for current tile and current sub-tile when displaying a level CEL. 109 | - JSON configuration file and corresponding settings dialog. 110 | - Working folder setting. 111 | - Status bar message when opening file. 112 | 113 | ### Changed 114 | - Default palette from town.pal to builtin _default.pal. 115 | - Default palette translation to _null.trn. 116 | 117 | ### Fixed 118 | - Export dialog button height. 119 | 120 | ## 0.3.2 - 2020-01-08 121 | ### Changed 122 | - Qt Framework updated to 5.12.6 LTS. 123 | - Rewrite changelog. 124 | 125 | ### Fixed 126 | - Fix palette display bug (unexpected crop). 127 | 128 | ## 0.3.1 - 2018-03-09 129 | ### Changed 130 | - Qt Framework updated to 5.9 LTS. 131 | - Code cleaning. 132 | 133 | ## 0.3.0 134 | ### Added 135 | - Automatic TRN listing. 136 | - BMP and PNG export support (multi-file or sprites). 137 | 138 | ### Changed 139 | - Cleaned CelFrameBase constructor. 140 | - Optimized TIL QImage rendering by adding and using tile width and pixel width/height. 141 | 142 | ### Fixed 143 | - Bug fix for Type 2, 3, 4, 5 frames rendering. 144 | - Bug fix for automatic PAL loading. 145 | - Bug fix for mono-group CEL/CL2 files. 146 | 147 | ## 0.2.4 148 | ### Added 149 | - Zoom support. 150 | - CEL/CL2 group based playing support. 151 | - Incomplete export support (only GUI). 152 | 153 | ## 0.2.3 154 | ### Added 155 | - MIN and TIL viewing support for level CEL files. 156 | 157 | ### Fixed 158 | - Bug fix for Type 2 and 3 frames detection. 159 | 160 | ## 0.2.2 161 | ### Added 162 | - Double TRN support. 163 | - Automatic PAL listing/loading. 164 | 165 | ## 0.2.1 166 | ### Added 167 | - Full CL2 viewing support. 168 | 169 | ## 0.2.0 170 | ### Added 171 | - Full CEL viewing support (including level CEL files). 172 | 173 | ### Changed 174 | - Object model modified so D1Cel and D1Cl2 classes both inherit D1CelBase. 175 | - CelView modified to allow CEL compilations and CL2 groups browsing. 176 | 177 | ## 0.1.3 178 | ### Added 179 | - Incomplete CEL viewing support , new algorithm, only level CEL files are not displayed. 180 | 181 | ## 0.1.2 182 | ### Added 183 | - Full TRN viewing support. 184 | 185 | ## 0.1.1 186 | ### Added 187 | - Incomplete CEL viewing support, new algorithm, only level CEL files are not displayed. 188 | 189 | ## 0.1.0 190 | ### Added 191 | - Full PAL viewing support. 192 | - Incomplete CEL viewing support. 193 | -------------------------------------------------------------------------------- /source/widgets/leveltabtilewidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LevelTabTileWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 480 10 | 160 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 2 28 | 29 | 30 | 2 31 | 32 | 33 | 2 34 | 35 | 36 | 2 37 | 38 | 39 | 40 | 41 | 4 42 | 43 | 44 | 45 | 46 | 4 47 | 48 | 49 | 50 | 51 | 52 | 100 53 | 0 54 | 55 | 56 | 57 | 58 | 100 59 | 16777215 60 | 61 | 62 | 63 | Automap type: 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | None 72 | 73 | 74 | 75 | 76 | Pillar 77 | 78 | 79 | 80 | 81 | Vertical wall 82 | 83 | 84 | 85 | 86 | Horizontal wall 87 | 88 | 89 | 90 | 91 | Wall intersection 92 | 93 | 94 | 95 | 96 | Vertical wall ending 97 | 98 | 99 | 100 | 101 | Horizontal wall ending 102 | 103 | 104 | 105 | 106 | Corner wall 107 | 108 | 109 | 110 | 111 | Wall intersection (west) 112 | 113 | 114 | 115 | 116 | Wall intersection (east) 117 | 118 | 119 | 120 | 121 | Horizontal wall (backside) 122 | 123 | 124 | 125 | 126 | Vertical wall (backside) 127 | 128 | 129 | 130 | 131 | Wall intersection (south) 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | 148 | 16777215 149 | 120 150 | 151 | 152 | 153 | Automap flags 154 | 155 | 156 | 157 | 158 | 159 | Vertical: 160 | 161 | 162 | 163 | 164 | 165 | 166 | Door 167 | 168 | 169 | 170 | 171 | 172 | 173 | Arch 174 | 175 | 176 | 177 | 178 | 179 | 180 | Grate 181 | 182 | 183 | 184 | 185 | 186 | 187 | Horizontal: 188 | 189 | 190 | 191 | 192 | 193 | 194 | Door 195 | 196 | 197 | 198 | 199 | 200 | 201 | Arch 202 | 203 | 204 | 205 | 206 | 207 | 208 | Grate 209 | 210 | 211 | 212 | 213 | 214 | 215 | Dirt 216 | 217 | 218 | 219 | 220 | 221 | 222 | Stairs 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /source/undostack/undostack.cpp: -------------------------------------------------------------------------------- 1 | #include "undostack.h" 2 | #include 3 | 4 | /** 5 | * @brief Pushes new commands onto the commands stack (Undostack) 6 | * 7 | * This function pushes new commands onto the undo stack, and redos 8 | * the command that got currently pushed, essentially triggering the command. 9 | * It also erases any commands that had isObsolete flag set. 10 | * Push also removes any commands that were currently undo'd on the stack. 11 | */ 12 | void UndoStack::push(std::unique_ptr cmd) 13 | { 14 | try { 15 | cmd->redo(); 16 | } catch (...) { 17 | /* TODO: here we we will start to implement proper 18 | * exception handling, but first we need to implement stuff 19 | * that will use it 20 | */ 21 | } 22 | 23 | eraseRedundantCmds(); 24 | 25 | m_cmds.push_back(std::move(cmd)); 26 | m_canUndo = true; 27 | m_canRedo = false; 28 | m_undoPos = m_cmds.size() - 1; 29 | } 30 | 31 | /** 32 | * @brief Undos command that is currently the first one on the stack 33 | * 34 | * This function undos the command that is currently on the stack, setting 35 | * redo option to be available at the same time. It also pops any commands that 36 | * have the isObsolete flag set. 37 | */ 38 | void UndoStack::undo() 39 | { 40 | // Erase any command that was previously set as obsolete 41 | std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); 42 | 43 | /* If current processed command has a macroID higher than 0, then it means it's a macro. 44 | * So we need to start going through commands backwards in a loop 45 | */ 46 | unsigned int macroID = m_cmds[m_undoPos]->macroID(); 47 | if (macroID > 0) { 48 | emit initializeWidget(m_macros[macroID - 1].userdata(), OperationType::Undo); 49 | 50 | while (m_cmds[m_undoPos]->macroID() > 0) { 51 | bool result = false; 52 | m_cmds[m_undoPos]->undo(); 53 | m_undoPos--; 54 | emit updateWidget(result); 55 | if (result || m_undoPos < 0 || m_cmds[m_undoPos + 1]->macroID() != m_cmds[m_undoPos]->macroID()) { 56 | break; 57 | } 58 | } 59 | } else { 60 | m_cmds[m_undoPos]->undo(); 61 | m_undoPos--; 62 | } 63 | 64 | if (m_undoPos < 0) 65 | m_canUndo = false; 66 | 67 | m_canRedo = true; 68 | } 69 | 70 | /** 71 | * @brief Redos command that is currently the first one on the stack 72 | * 73 | * This function redos the command that is currently on the stack, setting 74 | * undo option to be available at the same time. It also pops any commands that 75 | * have the isObsolete flag set. 76 | */ 77 | void UndoStack::redo() 78 | { 79 | // erase any command that was previously set as obsolete 80 | std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); 81 | 82 | unsigned int macroID = m_cmds[m_undoPos + 1]->macroID(); 83 | if (macroID > 0) { 84 | emit initializeWidget(m_macros[macroID - 1].userdata(), OperationType::Redo); 85 | 86 | while (m_cmds[m_undoPos + 1]->macroID() > 0) { 87 | bool result = false; 88 | m_cmds[m_undoPos + 1]->redo(); 89 | m_undoPos++; 90 | emit updateWidget(result); 91 | if (result || m_undoPos + 1 >= m_cmds.size() || m_cmds[m_undoPos + 1]->macroID() != m_cmds[m_undoPos]->macroID()) { 92 | break; 93 | } 94 | } 95 | } else { 96 | m_cmds[m_undoPos + 1]->redo(); 97 | m_undoPos++; 98 | } 99 | 100 | m_canUndo = true; 101 | 102 | if (m_undoPos + 1 >= m_cmds.size()) 103 | m_canRedo = false; 104 | } 105 | 106 | /** 107 | * @brief Returns if stack can currently redo 108 | */ 109 | bool UndoStack::canRedo() const 110 | { 111 | return m_canRedo; 112 | } 113 | 114 | /** 115 | * @brief Returns if stack can currently undo 116 | */ 117 | bool UndoStack::canUndo() const 118 | { 119 | return m_canUndo; 120 | } 121 | 122 | /** 123 | * @brief Sets undo stack to a starting position 124 | * 125 | * This function clears undo stack to a starting position. To be 126 | * exact it sets it's position to 0, clears any commands and disables 127 | * flags that inform if stack can currently undo or redo. 128 | */ 129 | void UndoStack::clear() 130 | { 131 | m_undoPos = -1; 132 | m_canUndo = m_canRedo = false; 133 | m_cmds.clear(); 134 | m_macros.clear(); 135 | } 136 | 137 | /** 138 | * @brief Adds a macro to undo stack 139 | * 140 | * This function is being called whenever someone wants to insert 141 | * a macro in the undo stack. It calls redo() on all commands contained 142 | * in the macro, and updates undo stack position accordingly, it also 143 | * sets macroIDs depending on the macros' vector size. 144 | * 145 | */ 146 | void UndoStack::addMacro(UndoMacroFactory ¯oFactory) 147 | { 148 | eraseRedundantCmds(); 149 | 150 | emit initializeWidget(macroFactory.userdata(), OperationType::Redo); 151 | m_macros.emplace_back(std::move(macroFactory.userdata()), std::make_pair(m_cmds.size(), (m_cmds.size() + macroFactory.cmds().size()) - 1)); 152 | 153 | for (auto &cmd : macroFactory.cmds()) { 154 | bool result = false; 155 | cmd->redo(); 156 | m_undoPos++; 157 | 158 | emit updateWidget(result); 159 | if (result) { 160 | m_canRedo = true; 161 | break; 162 | } 163 | } 164 | 165 | // For each command that will be inserted set a macroID so it is located in the same span 166 | std::for_each(macroFactory.cmds().begin(), macroFactory.cmds().end(), [&](const std::unique_ptr &cmd) { 167 | cmd->setMacroID(m_macros.size()); 168 | }); 169 | 170 | m_cmds.insert(m_cmds.end(), std::make_move_iterator(macroFactory.cmds().begin()), std::make_move_iterator(macroFactory.cmds().end())); 171 | m_canUndo = true; 172 | } 173 | 174 | /** 175 | * @brief Erases redundant commands and macros from the undo stack 176 | * 177 | * This function erases redundant commands and macros from both vectors 178 | * in undostack. Redundant in this case means commands which will no longer 179 | * be available, i.e. command being obsolete (having obsolete flag set to true), or 180 | * all commands + macros after currently pushed command - upon insertion undo stack removes all 181 | * commands + macros that are after current undo stack position (were undo'd) and are possible 182 | * to redo. 183 | * 184 | */ 185 | void UndoStack::eraseRedundantCmds() 186 | { 187 | // If user has undo'd all the commands, then erase everything 188 | if (m_undoPos < 0) { 189 | m_cmds.clear(); 190 | m_macros.clear(); 191 | return; 192 | } 193 | 194 | // Erase any command that was set to obsolete 195 | std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); 196 | 197 | if (m_cmds.begin() + (m_undoPos + 1) < m_cmds.end()) { 198 | // Drop any command that's after currently undo'd index 199 | m_cmds.erase(m_cmds.begin() + (m_undoPos + 1), m_cmds.end()); 200 | 201 | // Drop any macro that's after currently undo'd index 202 | std::erase_if(m_macros, [&](const auto ¯o) { return macro.beginIndex() > m_undoPos; }); 203 | 204 | // If undoPos is currently on a macro, then update it's ending index because we could have removed some of 205 | // it's commands 206 | unsigned int macroID = m_cmds[m_undoPos]->macroID(); 207 | if (macroID > 0) 208 | m_macros[macroID - 1].setLastIndex(m_undoPos); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /source/widgets/palettewidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "d1formats/d1trn.h" 12 | #include "palette/d1pal.h" 13 | #include "palette/d1palhits.h" 14 | #include "undostack/undostack.h" 15 | #include "views/celview.h" 16 | #include "views/levelcelview.h" 17 | 18 | #define PALETTE_WIDTH 192 19 | #define PALETTE_COLORS_PER_LINE 16 20 | #define PALETTE_COLOR_SPACING 1 21 | #define PALETTE_SELECTION_WIDTH 2 22 | 23 | enum class PWIDGET_CALLBACK_TYPE { 24 | PWIDGET_CALLBACK_NEW, 25 | PWIDGET_CALLBACK_OPEN, 26 | PWIDGET_CALLBACK_SAVE, 27 | PWIDGET_CALLBACK_SAVEAS, 28 | PWIDGET_CALLBACK_CLOSE, 29 | }; 30 | 31 | namespace Ui { 32 | class PaletteScene; 33 | class PaletteWidget; 34 | class EditColorsCommand; 35 | class EditTranslationsCommand; 36 | } // namespace Ui 37 | 38 | class EditColorsCommand : public QObject, public Command { 39 | Q_OBJECT 40 | 41 | public: 42 | explicit EditColorsCommand(D1Pal *, quint8, quint8, QColor, QColor); 43 | ~EditColorsCommand() = default; 44 | 45 | void undo() override; 46 | void redo() override; 47 | 48 | signals: 49 | void modified(); 50 | 51 | private: 52 | QPointer pal; 53 | quint8 startColorIndex; 54 | quint8 endColorIndex; 55 | QList initialColors; 56 | QColor newColor; 57 | QColor endColor; 58 | }; 59 | 60 | class EditTranslationsCommand : public QObject, public Command { 61 | Q_OBJECT 62 | 63 | public: 64 | explicit EditTranslationsCommand(D1Trn *, quint8, quint8, QList); 65 | ~EditTranslationsCommand() = default; 66 | 67 | void undo() override; 68 | void redo() override; 69 | 70 | signals: 71 | void modified(); 72 | 73 | private: 74 | QPointer trn; 75 | quint8 startColorIndex; 76 | quint8 endColorIndex; 77 | QList initialTranslations; 78 | QList newTranslations; 79 | }; 80 | 81 | class ClearTranslationsCommand : public QObject, public Command { 82 | Q_OBJECT 83 | 84 | public: 85 | explicit ClearTranslationsCommand(D1Trn *, quint8, quint8); 86 | ~ClearTranslationsCommand() = default; 87 | 88 | void undo() override; 89 | void redo() override; 90 | 91 | signals: 92 | void modified(); 93 | 94 | private: 95 | QPointer trn; 96 | quint8 startColorIndex; 97 | quint8 endColorIndex; 98 | QList initialTranslations; 99 | }; 100 | 101 | class PaletteScene : public QGraphicsScene { 102 | Q_OBJECT 103 | 104 | public: 105 | PaletteScene(QWidget *view); 106 | 107 | private slots: 108 | void mousePressEvent(QGraphicsSceneMouseEvent *event); 109 | void mouseMoveEvent(QGraphicsSceneMouseEvent *event); 110 | void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); 111 | void dragEnterEvent(QGraphicsSceneDragDropEvent *event); 112 | void dragMoveEvent(QGraphicsSceneDragDropEvent *event); 113 | void dropEvent(QGraphicsSceneDragDropEvent *event); 114 | void contextMenuEvent(QContextMenuEvent *event); 115 | 116 | signals: 117 | void framePixelClicked(quint16, quint16); 118 | 119 | private: 120 | QWidget *view; 121 | }; 122 | 123 | using PaletteFileInfo = struct PaletteFileInfo { 124 | QString name; // e.g., "palette" or "translation" 125 | QString suffix; // e.g., ".pal" or ".trn" 126 | }; 127 | 128 | enum class PaletteType : std::uint8_t { 129 | Palette = 0, 130 | Translation = 1, 131 | UniqTranslation = 2 132 | }; 133 | 134 | class PaletteWidget : public QWidget { 135 | Q_OBJECT 136 | 137 | public: 138 | explicit PaletteWidget(std::shared_ptr undoStack, QString title); 139 | ~PaletteWidget(); 140 | 141 | void setPal(const QString &path); 142 | void setTrn(const QString &path); 143 | bool isTrnWidget(); 144 | 145 | void initialize(D1Pal *p, CelView *c, D1PalHits *ph, PaletteType palType); 146 | void initialize(D1Pal *p, LevelCelView *lc, D1PalHits *ph, PaletteType palType); 147 | 148 | void initialize(D1Pal *p, D1Trn *t, CelView *c, D1PalHits *ph, PaletteType palType); 149 | void initialize(D1Pal *p, D1Trn *t, LevelCelView *lc, D1PalHits *ph, PaletteType palType); 150 | 151 | void initializeUi(); 152 | void initializePathComboBox(); 153 | void initializeDisplayComboBox(); 154 | 155 | void reloadConfig(); 156 | void selectColor(quint8); 157 | void checkTranslationsSelection(QList); 158 | 159 | void addPath(const QString &, const QString &, D1Pal *pal); 160 | void removePath(const QString &); 161 | void selectPath(const QString &); 162 | 163 | QString getWidgetsDefaultPath() const; 164 | QString getSelectedPath() const; 165 | 166 | // color selection handlers 167 | void startColorSelection(int colorIndex); 168 | void changeColorSelection(int colorIndex); 169 | void finishColorSelection(); 170 | [[nodiscard]] D1Pal *pal() const 171 | { 172 | return m_pal; 173 | }; 174 | [[nodiscard]] D1Trn *trn() const 175 | { 176 | return m_trn; 177 | }; 178 | 179 | void save(); 180 | void newOrSaveAsFile(PWIDGET_CALLBACK_TYPE action); 181 | 182 | bool loadPalette(const QString &filepath); 183 | void openPalette(); 184 | 185 | bool isOkToQuit(); 186 | 187 | void closePalette(); 188 | 189 | void setTrnPalette(D1Pal *pal); 190 | // Display functions 191 | bool displayColor(int index); 192 | void displayColors(); 193 | void displaySelection(); 194 | void temporarilyDisplayAllColors(); 195 | void displayInfo(QString); 196 | void clearInfo(); 197 | void displayBorder(); 198 | void clearBorder(); 199 | 200 | void refreshPathComboBox(); 201 | void refreshColorLineEdit(); 202 | void refreshIndexLineEdit(); 203 | void refreshTranslationIndexLineEdit(); 204 | 205 | void modify(); 206 | void refresh(); 207 | 208 | signals: 209 | void pathSelected(QString); 210 | void colorsSelected(QList); 211 | 212 | void displayAllRootColors(); 213 | void displayRootInformation(QString); 214 | void clearRootInformation(); 215 | void displayRootBorder(); 216 | void clearRootBorder(); 217 | 218 | void modified(); 219 | void refreshed(); 220 | 221 | private: 222 | [[nodiscard]] PaletteFileInfo paletteFileInfo() const; 223 | void performSave(const QString &palFilePath, const PaletteFileInfo &fileInfo); 224 | QPushButton *addButton(QStyle::StandardPixmap type, QString tooltip, void (PaletteWidget::*callback)(void)); 225 | 226 | public slots: 227 | void ShowContextMenu(const QPoint &pos); 228 | 229 | private slots: 230 | // Due to a bug in Qt these functions can not follow the naming conventions 231 | // if they follow, the application is going to vomit warnings in the background (maybe only in debug mode) 232 | void on_newPushButtonClicked(); 233 | void on_openPushButtonClicked(); 234 | void on_savePushButtonClicked(); 235 | void on_saveAsPushButtonClicked(); 236 | void on_closePushButtonClicked(); 237 | 238 | void actionUndo_triggered(); 239 | void actionRedo_triggered(); 240 | 241 | void on_pathComboBox_activated(int index); 242 | void on_displayComboBox_activated(int index); 243 | void on_colorLineEdit_returnPressed(); 244 | void on_colorPickPushButton_clicked(); 245 | void on_colorClearPushButton_clicked(); 246 | void on_translationIndexLineEdit_returnPressed(); 247 | void on_translationPickPushButton_clicked(); 248 | void on_translationClearPushButton_clicked(); 249 | void on_monsterTrnPushButton_clicked(); 250 | 251 | private: 252 | std::shared_ptr undoStack; 253 | Ui::PaletteWidget *ui; 254 | 255 | CelView *celView; 256 | LevelCelView *levelCelView; 257 | 258 | PaletteScene *scene; 259 | 260 | QColor paletteDefaultColor = Qt::magenta; 261 | 262 | QColor selectionBorderColor = Qt::red; 263 | quint8 selectedFirstColorIndex = 0; 264 | quint8 selectedLastColorIndex = 0; 265 | 266 | bool pickingTranslationColor = false; 267 | bool temporarilyDisplayingAllColors = false; 268 | 269 | D1Pal *m_pal; 270 | D1Trn *m_trn; 271 | 272 | PaletteType m_paletteType; 273 | 274 | D1PalHits *palHits; 275 | 276 | std::map> m_palettes_map; 277 | }; 278 | -------------------------------------------------------------------------------- /source/d1formats/d1celframe.cpp: -------------------------------------------------------------------------------- 1 | #include "d1celframe.h" 2 | 3 | D1CelPixelGroup::D1CelPixelGroup(bool t, quint16 c) 4 | : transparent(t) 5 | , pixelCount(c) 6 | { 7 | } 8 | 9 | bool D1CelPixelGroup::isTransparent() const 10 | { 11 | return this->transparent; 12 | } 13 | 14 | quint16 D1CelPixelGroup::getPixelCount() 15 | { 16 | return this->pixelCount; 17 | } 18 | 19 | bool D1CelFrame::load(D1GfxFrame &frame, QByteArray rawData, const OpenAsParam ¶ms) 20 | { 21 | if (rawData.size() == 0) 22 | return false; 23 | 24 | quint32 frameDataStartOffset = 0; 25 | quint16 width = 0; 26 | if (params.clipped != OPEN_CLIPPED_TYPE::No) { 27 | QDataStream in(rawData); 28 | in.setByteOrder(QDataStream::LittleEndian); 29 | quint16 offset; 30 | in >> offset; 31 | if (offset == 0x0A || params.clipped == OPEN_CLIPPED_TYPE::Yes) { 32 | frameDataStartOffset += offset; 33 | // If header is present, try to compute frame width from frame header 34 | width = D1CelFrame::computeWidthFromHeader(rawData); 35 | } 36 | } 37 | frame.width = params.celWidth == 0 ? width : params.celWidth; 38 | 39 | // If width could not be calculated with frame header, 40 | // attempt to calculate it from the frame data (by identifying pixel groups line wraps) 41 | if (frame.width == 0) 42 | frame.width = D1CelFrame::computeWidthFromData(rawData); 43 | 44 | // if CEL width was not found, return false 45 | if (frame.width == 0) 46 | return false; 47 | 48 | // READ {CEL FRAME DATA} 49 | QList pixelLine; 50 | for (int o = frameDataStartOffset; o < rawData.size(); o++) { 51 | quint8 readByte = rawData[o]; 52 | 53 | // Transparent pixels group 54 | if (readByte > 0x7F) { 55 | // A pixel line can't exceed the image width 56 | if ((pixelLine.size() + (256 - readByte)) > frame.width) 57 | return false; 58 | 59 | for (int i = 0; i < (256 - readByte); i++) 60 | pixelLine.append(D1GfxPixel::transparentPixel()); 61 | } else { 62 | // Palette indices group 63 | // A pixel line can't exceed the image width 64 | if ((pixelLine.size() + readByte) > frame.width) 65 | return false; 66 | 67 | for (int i = 0; i < readByte; i++) { 68 | o++; 69 | pixelLine.append(D1GfxPixel::colorPixel(rawData[o])); 70 | } 71 | } 72 | 73 | if (pixelLine.size() == frame.width) { 74 | frame.pixels.insert(0, pixelLine); 75 | pixelLine.clear(); 76 | } 77 | } 78 | 79 | frame.height = frame.pixels.size(); 80 | 81 | return true; 82 | } 83 | 84 | quint16 D1CelFrame::computeWidthFromHeader(QByteArray &rawFrameData) 85 | { 86 | // Reading the frame header 87 | QDataStream in(rawFrameData); 88 | in.setByteOrder(QDataStream::LittleEndian); 89 | 90 | quint16 celFrameHeaderSize; 91 | in >> celFrameHeaderSize; 92 | 93 | if (celFrameHeaderSize & 1) 94 | return 0; // invalid header 95 | 96 | // Decode the 32 pixel-lines blocks to calculate the image width 97 | quint16 celFrameWidth = 0; 98 | quint16 lastFrameOffset = celFrameHeaderSize; 99 | for (int i = 0; i < (celFrameHeaderSize / 2) - 1; i++) { 100 | quint16 nextFrameOffset; 101 | in >> nextFrameOffset; 102 | if (nextFrameOffset == 0) 103 | break; 104 | 105 | quint16 pixelCount = 0; 106 | for (int j = lastFrameOffset; j < nextFrameOffset; j++) { 107 | quint8 readByte = rawFrameData[j]; 108 | 109 | if (readByte > 0x7F) { 110 | pixelCount += (256 - readByte); 111 | } else { 112 | pixelCount += readByte; 113 | j += readByte; 114 | } 115 | } 116 | 117 | quint16 width = pixelCount / CEL_BLOCK_HEIGHT; 118 | // The calculated width has to be the identical for each 32 pixel-line block 119 | // If it's not the case, 0 is returned 120 | if (celFrameWidth != 0 && celFrameWidth != width) 121 | return 0; 122 | 123 | celFrameWidth = width; 124 | lastFrameOffset = nextFrameOffset; 125 | } 126 | 127 | return celFrameWidth; 128 | } 129 | 130 | quint16 D1CelFrame::computeWidthFromData(QByteArray &rawFrameData) 131 | { 132 | quint16 biggestGroupPixelCount = 0; 133 | quint16 pixelCount = 0; 134 | quint16 width = 0; 135 | QList pixelGroups; 136 | 137 | // Checking the presence of the {CEL FRAME HEADER} 138 | quint32 frameDataStartOffset = 0; 139 | if ((quint8)rawFrameData[0] == 0x0A && (quint8)rawFrameData[1] == 0x00) 140 | frameDataStartOffset = 0x0A; 141 | 142 | // Going through the frame data to find pixel groups 143 | quint32 globalPixelCount = 0; 144 | for (int o = frameDataStartOffset; o < rawFrameData.size(); o++) { 145 | quint8 readByte = rawFrameData[o]; 146 | 147 | // Transparent pixels group 148 | if (readByte > 0x80) { 149 | pixelCount += (256 - readByte); 150 | pixelGroups.append(new D1CelPixelGroup(true, pixelCount)); 151 | globalPixelCount += pixelCount; 152 | if (pixelCount > biggestGroupPixelCount) 153 | biggestGroupPixelCount = pixelCount; 154 | pixelCount = 0; 155 | } else if (readByte == 0x80) { 156 | pixelCount += 0x80; 157 | } 158 | // Palette indices pixel group 159 | else if (readByte == 0x7F) { 160 | pixelCount += 0x7F; 161 | o += 0x7F; 162 | } else { 163 | pixelCount += readByte; 164 | pixelGroups.append(new D1CelPixelGroup(false, pixelCount)); 165 | globalPixelCount += pixelCount; 166 | if (pixelCount > biggestGroupPixelCount) 167 | biggestGroupPixelCount = pixelCount; 168 | pixelCount = 0; 169 | o += readByte; 170 | } 171 | } 172 | 173 | // Going through pixel groups to find pixel-lines wraps 174 | pixelCount = 0; 175 | for (int i = 1; i < pixelGroups.size(); i++) { 176 | pixelCount += pixelGroups[i - 1]->getPixelCount(); 177 | 178 | if (pixelGroups[i - 1]->isTransparent() == pixelGroups[i]->isTransparent()) { 179 | // If width == 0 then it's the first pixel-line wrap and width needs to be set 180 | // If pixelCount is less than width then the width has to be set to the new value 181 | if (width == 0 || pixelCount < width) 182 | width = pixelCount; 183 | 184 | // If the pixelCount of the last group is less than the current pixel group 185 | // then width is equal to this last pixel group's pixel count. 186 | // Mostly useful for small frames like the "J" frame in smaltext.cel 187 | if (i == pixelGroups.size() - 1 && pixelGroups[i]->getPixelCount() < pixelCount) 188 | width = pixelGroups[i]->getPixelCount(); 189 | 190 | pixelCount = 0; 191 | } 192 | 193 | // If last pixel group is being processed and width is still unknown 194 | // then set the width to the pixelCount of the last two pixel groups 195 | if (i == pixelGroups.size() - 1 && width == 0) { 196 | width = pixelGroups[i - 1]->getPixelCount() + pixelGroups[i]->getPixelCount(); 197 | } 198 | } 199 | 200 | // If width wasnt found return 0 201 | if (width == 0) { 202 | qDeleteAll(pixelGroups); 203 | return 0; 204 | } 205 | 206 | // If width is consistent 207 | if (globalPixelCount % width == 0) { 208 | qDeleteAll(pixelGroups); 209 | return width; 210 | } 211 | 212 | // Try to find relevant width by adding pixel groups' pixel counts iteratively 213 | pixelCount = 0; 214 | for (int i = 0; i < pixelGroups.size(); i++) { 215 | pixelCount += pixelGroups[i]->getPixelCount(); 216 | if (pixelCount > 1 217 | && globalPixelCount % pixelCount == 0 218 | && pixelCount >= biggestGroupPixelCount) { 219 | qDeleteAll(pixelGroups); 220 | return pixelCount; 221 | } 222 | } 223 | 224 | qDeleteAll(pixelGroups); 225 | // If still no width found return 0 226 | return 0; 227 | } 228 | -------------------------------------------------------------------------------- /source/d1formats/d1min.cpp: -------------------------------------------------------------------------------- 1 | #include "d1min.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "d1image.h" 11 | 12 | bool D1Min::load(QString filePath, D1Gfx *g, D1Sol *sol, std::map &celFrameTypes, const OpenAsParam ¶ms) 13 | { 14 | // prepare file data source 15 | QFile file; 16 | // done by the caller 17 | // if (!params.minFilePath.isEmpty()) { 18 | // filePath = params.minFilePath; 19 | // } 20 | if (!filePath.isEmpty()) { 21 | file.setFileName(filePath); 22 | if (!file.open(QIODevice::ReadOnly)) { 23 | return false; 24 | } 25 | } 26 | 27 | QByteArray fileData = file.readAll(); 28 | QBuffer fileBuffer(&fileData); 29 | 30 | if (!fileBuffer.open(QIODevice::ReadOnly)) { 31 | return false; 32 | } 33 | 34 | // calculate subtileWidth/Height 35 | auto fileSize = file.size(); 36 | int subtileCount = sol->getSubtileCount(); 37 | int width = params.minWidth; 38 | if (width == 0) { 39 | width = 2; 40 | } 41 | int height = params.minHeight; 42 | if (height == 0) { 43 | if (fileSize == 0 || subtileCount == 0) { 44 | height = 5; 45 | } else { 46 | // guess subtileHeight based on the data 47 | height = fileSize / (subtileCount * width * 2); 48 | } 49 | } 50 | 51 | // File size check 52 | int subtileNumberOfCelFrames = width * height; 53 | if ((fileSize % (subtileNumberOfCelFrames * 2)) != 0) { 54 | qDebug() << "Sub-tile width/height does not align with min-file."; 55 | return false; 56 | } 57 | 58 | this->gfx = g; 59 | this->subtileWidth = width; 60 | this->subtileHeight = height; 61 | int minSubtileCount = fileSize / (subtileNumberOfCelFrames * 2); 62 | if (minSubtileCount != subtileCount) { 63 | qDebug() << "The size of sol-file does not align with min-file"; 64 | // add subtiles to sol if necessary 65 | while (minSubtileCount > subtileCount) { 66 | subtileCount++; 67 | sol->createSubtile(); 68 | } 69 | } 70 | 71 | // prepare an empty list with zeros 72 | this->celFrameIndices.clear(); 73 | for (int i = 0; i < subtileCount; i++) { 74 | QList celFrameIndicesList; 75 | for (int j = 0; j < subtileNumberOfCelFrames; j++) { 76 | celFrameIndicesList.append(0); 77 | } 78 | this->celFrameIndices.append(celFrameIndicesList); 79 | } 80 | 81 | // Read MIN binary data 82 | QDataStream in(&fileBuffer); 83 | in.setByteOrder(QDataStream::LittleEndian); 84 | 85 | for (int i = 0; i < minSubtileCount; i++) { 86 | QList &celFrameIndicesList = this->celFrameIndices[i]; 87 | for (int j = 0; j < subtileNumberOfCelFrames; j++) { 88 | quint16 readWord; 89 | in >> readWord; 90 | quint16 id = readWord & 0x0FFF; 91 | celFrameIndicesList[j] = id; 92 | celFrameTypes[id] = static_cast((readWord & 0x7000) >> 12); 93 | } 94 | } 95 | 96 | this->minFilePath = filePath; 97 | this->modified = false; 98 | 99 | return true; 100 | } 101 | 102 | bool D1Min::save(const QString &gfxPath) 103 | { 104 | QString filePath = gfxPath; 105 | filePath.chop(3); 106 | filePath += "min"; 107 | 108 | QFile outFile = QFile(filePath); 109 | if (!outFile.open(QIODevice::WriteOnly | QFile::Truncate)) { 110 | QMessageBox::critical(nullptr, "Error", "Failed open file: " + filePath); 111 | return false; 112 | } 113 | 114 | // write to file 115 | QDataStream out(&outFile); 116 | out.setByteOrder(QDataStream::LittleEndian); 117 | for (int i = 0; i < this->celFrameIndices.size(); i++) { 118 | QList &celFrameIndicesList = this->celFrameIndices[i]; 119 | for (int j = 0; j < celFrameIndicesList.count(); j++) { 120 | quint16 writeWord = celFrameIndicesList[j]; 121 | if (writeWord != 0) { 122 | writeWord |= ((quint16)this->gfx->getFrame(writeWord - 1)->getFrameType()) << 12; 123 | } 124 | out << writeWord; 125 | } 126 | } 127 | 128 | this->minFilePath = filePath; 129 | this->modified = false; 130 | 131 | return true; 132 | } 133 | 134 | QImage D1Min::getSubtileImage(int subtileIndex) 135 | { 136 | if (subtileIndex < 0 || subtileIndex >= this->celFrameIndices.size()) 137 | return QImage(); 138 | 139 | unsigned subtileWidthPx = this->subtileWidth * MICRO_WIDTH; 140 | QImage subtile = QImage(subtileWidthPx, 141 | this->subtileHeight * MICRO_HEIGHT, QImage::Format_ARGB32); 142 | subtile.fill(Qt::transparent); 143 | QPainter subtilePainter(&subtile); 144 | 145 | unsigned dx = 0, dy = 0; 146 | int n = this->subtileWidth * this->subtileHeight; 147 | for (int i = 0; i < n; i++) { 148 | quint16 celFrameIndex = this->celFrameIndices.at(subtileIndex).at(i); 149 | 150 | if (celFrameIndex > 0) 151 | subtilePainter.drawImage(dx, dy, 152 | this->gfx->getFrameImage(celFrameIndex - 1)); 153 | 154 | dx += MICRO_WIDTH; 155 | if (dx == subtileWidthPx) { 156 | dx = 0; 157 | dy += MICRO_HEIGHT; 158 | } 159 | } 160 | 161 | subtilePainter.end(); 162 | return subtile; 163 | } 164 | 165 | bool D1Min::isModified() const 166 | { 167 | return this->modified; 168 | } 169 | 170 | QString D1Min::getFilePath() 171 | { 172 | return this->minFilePath; 173 | } 174 | 175 | int D1Min::getSubtileCount() 176 | { 177 | return this->celFrameIndices.count(); 178 | } 179 | 180 | quint16 D1Min::getSubtileWidth() 181 | { 182 | return this->subtileWidth; 183 | } 184 | 185 | quint16 D1Min::getSubtileHeight() 186 | { 187 | return this->subtileHeight; 188 | } 189 | 190 | void D1Min::setSubtileHeight(int height) 191 | { 192 | if (height == 0) { 193 | return; 194 | } 195 | int width = this->subtileWidth; 196 | int diff = height - this->subtileHeight; 197 | if (diff > 0) { 198 | // extend the subtile-height 199 | int n = diff * width; 200 | for (int i = 0; i < this->celFrameIndices.size(); i++) { 201 | QList &celFrameIndicesList = this->celFrameIndices[i]; 202 | for (int j = 0; j < n; j++) { 203 | celFrameIndicesList.push_front(0); 204 | } 205 | } 206 | } else if (diff < 0) { 207 | diff = -diff; 208 | // check if there is a non-zero frame in the subtiles 209 | bool hasFrame = false; 210 | int n = diff * width; 211 | for (int i = 0; i < this->celFrameIndices.size() && !hasFrame; i++) { 212 | QList &celFrameIndicesList = this->celFrameIndices[i]; 213 | for (int j = 0; j < n; j++) { 214 | if (celFrameIndicesList[j] != 0) { 215 | hasFrame = true; 216 | break; 217 | } 218 | } 219 | } 220 | if (hasFrame) { 221 | QMessageBox::StandardButton reply; 222 | reply = QMessageBox::question(nullptr, "Confirmation", "Non-transparent frames are going to be eliminited. Are you sure you want to proceed?", QMessageBox::Yes | QMessageBox::No); 223 | if (reply != QMessageBox::Yes) { 224 | return; 225 | } 226 | } 227 | // reduce the subtile-height 228 | for (int i = 0; i < this->celFrameIndices.size(); i++) { 229 | QList &celFrameIndicesList = this->celFrameIndices[i]; 230 | celFrameIndicesList.erase(celFrameIndicesList.begin(), celFrameIndicesList.begin() + n); 231 | } 232 | } 233 | this->subtileHeight = height; 234 | this->modified = true; 235 | } 236 | 237 | QList &D1Min::getCelFrameIndices(int subtileIndex) 238 | { 239 | return const_cast &>(this->celFrameIndices.at(subtileIndex)); 240 | } 241 | 242 | void D1Min::insertSubtile(int subtileIndex, const QList &frameIndicesList) 243 | { 244 | this->celFrameIndices.insert(subtileIndex, frameIndicesList); 245 | this->modified = true; 246 | } 247 | 248 | void D1Min::createSubtile() 249 | { 250 | QList celFrameIndicesList; 251 | int n = this->subtileWidth * this->subtileHeight; 252 | 253 | for (int i = 0; i < n; i++) { 254 | celFrameIndicesList.append(0); 255 | } 256 | this->celFrameIndices.append(celFrameIndicesList); 257 | this->modified = true; 258 | } 259 | 260 | void D1Min::removeSubtile(int subtileIndex) 261 | { 262 | this->celFrameIndices.removeAt(subtileIndex); 263 | this->modified = true; 264 | } 265 | 266 | void D1Min::remapSubtiles(const QMap &remap) 267 | { 268 | QList> newCelFrameIndices; 269 | 270 | for (auto iter = remap.cbegin(); iter != remap.cend(); ++iter) { 271 | newCelFrameIndices.append(this->celFrameIndices.at(iter.value())); 272 | } 273 | this->celFrameIndices.swap(newCelFrameIndices); 274 | this->modified = true; 275 | } 276 | -------------------------------------------------------------------------------- /source/d1formats/d1gfx.cpp: -------------------------------------------------------------------------------- 1 | #include "d1gfx.h" 2 | 3 | #include 4 | 5 | #include "d1image.h" 6 | 7 | namespace { 8 | 9 | QImage EmptyFramePlaceholder(QString text) 10 | { 11 | QFont font("", 16); 12 | QFontMetrics metrics = QFontMetrics(font); 13 | QSize size = metrics.size(0, text); 14 | 15 | QImage placeholder = QImage( 16 | size.width(), 17 | size.height(), 18 | QImage::Format_ARGB32); 19 | 20 | placeholder.fill(qRgba(0, 0, 0, 0)); 21 | 22 | QPainter painter = QPainter(&placeholder); 23 | painter.setPen(Qt::gray); 24 | painter.setFont(font); 25 | painter.drawText(0, metrics.ascent(), text); 26 | return placeholder; 27 | } 28 | 29 | } // namespace 30 | 31 | D1GfxPixel D1GfxPixel::transparentPixel() 32 | { 33 | D1GfxPixel pixel; 34 | pixel.transparent = true; 35 | pixel.paletteIndex = 0; 36 | return pixel; 37 | } 38 | 39 | D1GfxPixel D1GfxPixel::colorPixel(quint8 color) 40 | { 41 | D1GfxPixel pixel; 42 | pixel.transparent = false; 43 | pixel.paletteIndex = color; 44 | return pixel; 45 | } 46 | 47 | bool D1GfxPixel::isTransparent() const 48 | { 49 | return this->transparent; 50 | } 51 | 52 | quint8 D1GfxPixel::getPaletteIndex() const 53 | { 54 | return this->paletteIndex; 55 | } 56 | 57 | bool operator==(const D1GfxPixel &lhs, const D1GfxPixel &rhs) 58 | { 59 | return lhs.transparent == rhs.transparent && lhs.paletteIndex == rhs.paletteIndex; 60 | } 61 | 62 | int D1GfxFrame::getWidth() const 63 | { 64 | return this->width; 65 | } 66 | 67 | int D1GfxFrame::getHeight() const 68 | { 69 | return this->height; 70 | } 71 | 72 | D1GfxPixel D1GfxFrame::getPixel(int x, int y) const 73 | { 74 | if (x >= 0 && x < this->width && y >= 0 && y < this->height) 75 | return this->pixels[y][x]; 76 | 77 | return D1GfxPixel::transparentPixel(); 78 | } 79 | 80 | D1CEL_FRAME_TYPE D1GfxFrame::getFrameType() const 81 | { 82 | return this->frameType; 83 | } 84 | 85 | void D1GfxFrame::setFrameType(D1CEL_FRAME_TYPE type) 86 | { 87 | this->frameType = type; 88 | } 89 | 90 | // builds QImage from a D1CelFrame of given index 91 | QImage D1Gfx::getFrameImage(quint16 frameIndex) 92 | { 93 | if (this->palette == nullptr) 94 | return EmptyFramePlaceholder("No palette"); 95 | 96 | if (frameIndex >= this->frames.count()) 97 | return EmptyFramePlaceholder("Out of bounds"); 98 | 99 | D1GfxFrame &frame = this->frames[frameIndex]; 100 | 101 | if (frame.getWidth() == 0 || frame.getHeight() == 0) 102 | return EmptyFramePlaceholder("No frame data"); 103 | 104 | QImage image = QImage( 105 | frame.getWidth(), 106 | frame.getHeight(), 107 | QImage::Format_ARGB32); 108 | 109 | for (int y = 0; y < frame.getHeight(); y++) { 110 | for (int x = 0; x < frame.getWidth(); x++) { 111 | D1GfxPixel d1pix = frame.getPixel(x, y); 112 | 113 | QColor color; 114 | if (d1pix.isTransparent()) 115 | color = QColor(Qt::transparent); 116 | else 117 | color = this->palette->getColor(d1pix.getPaletteIndex()); 118 | 119 | image.setPixel(x, y, color.rgba()); 120 | } 121 | } 122 | 123 | return image; 124 | } 125 | 126 | void D1Gfx::insertGroup(int groupIdx, int frameIdx, const QImage &image) 127 | { 128 | D1GfxFrame frame; 129 | D1ImageFrame::load(frame, image, this->palette); 130 | this->frames.insert(frameIdx, frame); 131 | this->groupFrameIndices.insert(groupIdx, QPair(frameIdx, frameIdx)); 132 | 133 | // We have to increment first frame index in the group that follows the one that we have deleted, 134 | // but only if the current group isn't the last one 135 | for (int i = groupIdx + 1; i < this->groupFrameIndices.count(); i++) { 136 | this->groupFrameIndices[i].first++; 137 | this->groupFrameIndices[i].second++; 138 | } 139 | } 140 | 141 | D1GfxFrame *D1Gfx::insertFrame(int frameIdx, const QImage &image) 142 | { 143 | D1GfxFrame frame; 144 | D1ImageFrame::load(frame, image, this->palette); 145 | this->frames.insert(frameIdx, frame); 146 | 147 | if (this->groupFrameIndices.isEmpty()) { 148 | // create new group if this is the first frame 149 | this->groupFrameIndices.append(qMakePair(0, 0)); 150 | } else if (this->frames.count() == frameIdx + 1) { 151 | // extend the last group if appending a frame 152 | this->groupFrameIndices.last().second = frameIdx; 153 | } else { 154 | // extend the current group and adjust every group after it 155 | for (int i = 0; i < this->groupFrameIndices.count(); i++) { 156 | if (this->groupFrameIndices[i].second < frameIdx) 157 | continue; 158 | if (this->groupFrameIndices[i].first > frameIdx) { 159 | this->groupFrameIndices[i].first++; 160 | } 161 | this->groupFrameIndices[i].second++; 162 | } 163 | } 164 | 165 | this->modified = true; 166 | return &this->frames[frameIdx]; 167 | } 168 | 169 | void D1Gfx::insertFrameInGroup(int frameIdx, int groupIdx, const QImage &image) 170 | { 171 | D1GfxFrame frame; 172 | D1ImageFrame::load(frame, image, this->palette); 173 | this->frames.insert(frameIdx, frame); 174 | 175 | this->groupFrameIndices[groupIdx].second++; 176 | 177 | // shift all indices in groups AFTER group in which frame has been added 178 | for (int i = groupIdx + 1; i < this->groupFrameIndices.count(); i++) { 179 | this->groupFrameIndices[i].first++; 180 | this->groupFrameIndices[i].second++; 181 | } 182 | 183 | this->modified = true; 184 | } 185 | 186 | D1GfxFrame *D1Gfx::replaceFrame(int idx, const QImage &image) 187 | { 188 | D1GfxFrame frame; 189 | D1ImageFrame::load(frame, image, this->palette); 190 | this->frames[idx] = frame; 191 | 192 | this->modified = true; 193 | return &this->frames[idx]; 194 | } 195 | 196 | std::optional D1Gfx::removeFrame(quint16 idx) 197 | { 198 | this->frames.removeAt(idx); 199 | std::optional removedGroupIdx; 200 | 201 | for (int i = 0; i < this->groupFrameIndices.count(); i++) { 202 | if (this->groupFrameIndices[i].second < idx) 203 | continue; 204 | if (this->groupFrameIndices[i].second == idx && this->groupFrameIndices[i].first == idx) { 205 | removedGroupIdx.emplace(i); 206 | this->groupFrameIndices.removeAt(i); 207 | i--; 208 | continue; 209 | } 210 | if (this->groupFrameIndices[i].first > idx) { 211 | this->groupFrameIndices[i].first--; 212 | } 213 | this->groupFrameIndices[i].second--; 214 | } 215 | this->modified = true; 216 | 217 | return removedGroupIdx; 218 | } 219 | 220 | void D1Gfx::regroupFrames(int numGroups) 221 | { 222 | const int numFrames = this->frames.count(); 223 | 224 | // update group indices 225 | this->groupFrameIndices.clear(); 226 | for (int i = 0; i < numGroups; i++) { 227 | int ni = numFrames / numGroups; 228 | this->groupFrameIndices.append(qMakePair(i * ni, i * ni + ni - 1)); 229 | } 230 | this->modified = true; 231 | } 232 | 233 | void D1Gfx::remapFrames(const QMap &remap) 234 | { 235 | QList newFrames; 236 | // assert(this->groupFrameIndices.count() == 1); 237 | for (auto iter = remap.cbegin(); iter != remap.cend(); ++iter) { 238 | newFrames.append(this->frames.at(iter.value() - 1)); 239 | } 240 | this->frames.swap(newFrames); 241 | this->modified = true; 242 | } 243 | 244 | bool D1Gfx::isModified() const 245 | { 246 | return this->modified; 247 | } 248 | 249 | void D1Gfx::setModified(bool isModified) 250 | { 251 | this->modified = isModified; 252 | } 253 | 254 | bool D1Gfx::isTileset() const 255 | { 256 | return this->isTileset_; 257 | } 258 | 259 | bool D1Gfx::hasHeader() const 260 | { 261 | return this->hasHeader_; 262 | } 263 | 264 | void D1Gfx::setHasHeader(bool hasHeader) 265 | { 266 | this->hasHeader_ = hasHeader; 267 | } 268 | 269 | QString D1Gfx::getFilePath() 270 | { 271 | return this->gfxFilePath; 272 | } 273 | 274 | D1Pal *D1Gfx::getPalette() 275 | { 276 | return this->palette; 277 | } 278 | 279 | void D1Gfx::setPalette(D1Pal *pal) 280 | { 281 | this->palette = pal; 282 | } 283 | 284 | int D1Gfx::getGroupCount() 285 | { 286 | return this->groupFrameIndices.count(); 287 | } 288 | 289 | QPair D1Gfx::getGroupFrameIndices(int groupIndex) 290 | { 291 | if (groupIndex < 0 || groupIndex >= this->groupFrameIndices.count()) 292 | return qMakePair(0, 0); 293 | 294 | return this->groupFrameIndices[groupIndex]; 295 | } 296 | 297 | int D1Gfx::getFrameCount() 298 | { 299 | return this->frames.count(); 300 | } 301 | 302 | D1GfxFrame *D1Gfx::getFrame(int frameIndex) 303 | { 304 | if (frameIndex < 0 || frameIndex >= this->frames.count()) 305 | return nullptr; 306 | 307 | return &this->frames[frameIndex]; 308 | } 309 | 310 | int D1Gfx::getFrameWidth(int frameIndex) 311 | { 312 | if (frameIndex < 0 || frameIndex >= this->frames.count()) 313 | return 0; 314 | 315 | return this->frames[frameIndex].getWidth(); 316 | } 317 | 318 | int D1Gfx::getFrameHeight(int frameIndex) 319 | { 320 | if (frameIndex < 0 || frameIndex >= this->frames.count()) 321 | return 0; 322 | 323 | return this->frames[frameIndex].getHeight(); 324 | } 325 | --------------------------------------------------------------------------------