diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a3ea819..0d0cf5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,17 +9,20 @@ on: jobs: main_build: - name: ${{ matrix.package_suffix }} + name: ${{ matrix.package_suffix }} ${{ matrix.interface }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: qt: - - 6.6.2 + - 6.6.3 os: - ubuntu-latest - macos-latest - windows-latest + interface: + - cli + - gui build_type: - Release include: @@ -40,6 +43,12 @@ jobs: qt_arch: win64_msvc2019_64 cmake_extra_args: '-DZLIB_ROOT=C:/zlib' qt_tools: tools_ninja, tools_cmake + - interface: gui + cmake_cli_arg: 'OFF' + cmake_gui_arg: 'ON' + - interface: cli + cmake_cli_arg: 'ON' + cmake_gui_arg: 'OFF' env: qt_installation_path: ${{ github.workspace }} @@ -135,11 +144,11 @@ jobs: - name: Build Deling id: main_build run: | - cmake -B ${{ env.deling_build_path }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -DCMAKE_INSTALL_PREFIX=${{ env.deling_installation_path }} -DPRERELEASE_STRING="$PRERELEASE_STRING" -Dlz4_DIR=${{ env.lz4_installation_path }}/lib/cmake/lz4 ${{ matrix.cmake_extra_args }} + cmake -B ${{ env.deling_build_path }} -DCMAKE_BUILD_TYPE=${{ env.CMAKE_BUILD_TYPE }} -DCMAKE_INSTALL_PREFIX=${{ env.deling_installation_path }} -DCLI:BOOL=${{ matrix.cmake_cli_arg }} -DGUI:BOOL=${{ matrix.cmake_gui_arg }} -DPRERELEASE_STRING="$PRERELEASE_STRING" -Dlz4_DIR=${{ env.lz4_installation_path }}/lib/cmake/lz4 ${{ matrix.cmake_extra_args }} cmake --build ${{ env.deling_build_path }} --target package -j3 - name: Build AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.interface == 'gui' run: | sudo add-apt-repository -y universe sudo apt install -y libfuse2 libxkbcommon-x11-0 libxcb-cursor0 @@ -153,16 +162,16 @@ jobs: -e "${{ env.deling_installation_path }}"/bin/Deling \ -d "${{ env.deling_installation_path }}"/share/applications/io.github.myst6re.deling.desktop \ -i "${{ env.deling_installation_path }}"/share/icons/hicolor/256x256/apps/io.github.myst6re.deling.png - mv *.AppImage deling-continuous-${{ matrix.package_suffix }}.AppImage + mv *.AppImage deling-continuous-${{ matrix.interface }}-${{ matrix.package_suffix }}.AppImage - name: Prepare Upload shell: bash - run: mv ../build-deling/*.${{ matrix.package_extension }} deling-continuous-${{ matrix.package_suffix }}.${{ matrix.package_extension }} + run: mv ../build-deling/*.${{ matrix.package_extension }} deling-continuous-${{ matrix.interface }}-${{ matrix.package_suffix }}.${{ matrix.package_extension }} - name: Upload uses: actions/upload-artifact@v4 with: - name: artifact-${{ matrix.package_suffix }} + name: artifact-${{ matrix.package_suffix }}-${{ matrix.interface }} path: ${{ github.workspace }}/deling-continuous-*.* pre_release_assets: diff --git a/CMakeLists.txt b/CMakeLists.txt index e379592..59c0c09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ cmake_policy(SET CMP0091 NEW) set(RELEASE_NAME "Deling") set(GUI_TARGET "${RELEASE_NAME}") +set(CLI_TARGET "deling-cli") if(NOT PRERELEASE_STRING) set(PRERELEASE_STRING "") endif() @@ -52,6 +53,9 @@ set(CMAKE_AUTOUIC OFF) set(CMAKE_INCLUDE_CURRENT_DIR ON) +option(GUI "Build the gui executable" ON) +option(CLI "Build the cli executable" OFF) + add_compile_definitions( QT_DISABLE_DEPRECATED_BEFORE=0x050F00 #QT_RESTRICTED_CAST_FROM_ASCII @@ -94,8 +98,8 @@ set(PROJECT_SOURCES "src/Config.h" "src/ConfigDialog.cpp" "src/ConfigDialog.h" - "src/CsvFile.cpp" - "src/CsvFile.h" + "src/CsvFile.cpp" + "src/CsvFile.h" "src/Data.cpp" "src/Data.h" "src/EncounterExporter.cpp" @@ -245,10 +249,10 @@ set(PROJECT_SOURCES "src/SpecialCharactersDialog.h" "src/TdwManagerDialog.cpp" "src/TdwManagerDialog.h" - "src/TextExporter.cpp" - "src/TextExporter.h" - "src/TextExporterWidget.cpp" - "src/TextExporterWidget.h" + "src/TextExporter.cpp" + "src/TextExporter.h" + "src/TextExporterWidget.cpp" + "src/TextExporterWidget.h" "src/TextPreview.cpp" "src/TextPreview.h" "src/VarManager.cpp" @@ -303,16 +307,39 @@ if(APPLE) ) endif() +set(PROJECT_CLI_SOURCES + "src/main.cpp" + "src/ArchiveObserver.h" + "src/Arguments.cpp" + "src/Arguments.h" + "src/ArgumentsExport.cpp" + "src/ArgumentsExport.h" + "src/ArgumentsImport.cpp" + "src/ArgumentsImport.h" + "src/CLI.cpp" + "src/CLI.h" + "src/Config.cpp" + "src/Config.h" + "src/FsArchive.cpp" + "src/FsArchive.h" + "src/LZS.cpp" + "src/LZS.h" + "src/QLZ4.cpp" + "src/QLZ4.h" +) + set(RESOURCES "src/qt/${RELEASE_NAME}.qrc") if(APPLE) set(ICON_FILE "deploy/macosx/${RELEASE_NAME}.icns") set(EXTRA_RESOURCES_GUI ${ICON_FILE}) + set(EXTRA_RESOURCES_CLI "") set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) elseif (WIN32) set(ICON_FILE "src/qt/images/deling.ico") set(EXTRA_RESOURCES_GUI "src/qt/${RELEASE_NAME}.rc") + set(EXTRA_RESOURCES_CLI "${EXTRA_RESOURCES_GUI}") endif() function(add_translations target) @@ -329,39 +356,53 @@ function(add_translations target) add_dependencies(${target}_lrelease ${target}_lupdate) endfunction() -qt_add_executable(${GUI_TARGET} MANUAL_FINALIZATION MACOSX_BUNDLE WIN32 ${PROJECT_SOURCES} ${QM_FILES} ${RESOURCES} ${EXTRA_RESOURCES_GUI}) -target_include_directories(${GUI_TARGET} PRIVATE "${CMAKE_SOURCE_DIR}/src") -target_link_libraries(${GUI_TARGET} PRIVATE - Qt::OpenGL - Qt::Widgets - Qt::OpenGLWidgets - ZLIB::ZLIB - LZ4::lz4_static -) - -if(${QT_VERSION_MAJOR} EQUAL 6) - target_compile_definitions(${GUI_TARGET} - PRIVATE TASKBAR_BUTTON=1 - ) +if(GUI) + qt_add_executable(${GUI_TARGET} MANUAL_FINALIZATION MACOSX_BUNDLE WIN32 ${PROJECT_SOURCES} ${QM_FILES} ${RESOURCES} ${EXTRA_RESOURCES_GUI}) + target_include_directories(${GUI_TARGET} PRIVATE "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(${GUI_TARGET} PRIVATE + Qt::OpenGL + Qt::Widgets + Qt::OpenGLWidgets + ZLIB::ZLIB + LZ4::lz4_static + ) + + if(${QT_VERSION_MAJOR} EQUAL 6) + target_compile_definitions(${GUI_TARGET} + PRIVATE TASKBAR_BUTTON=1 + ) + endif() + + set_target_properties(${GUI_TARGET} PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME "Deling" + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" + MACOSX_BUNDLE_GUI_IDENTIFIER org.myst6re.deling + MACOSX_BUNDLE_ICON_FILE ${RELEASE_NAME}.icns + ) + + if(APPLE OR WIN32) + set_target_properties(${GUI_TARGET} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") + endif() + + add_translations(${GUI_TARGET} + TS_FILES ${TS_FILES} + QM_FILES_OUTPUT_VARIABLE QM_FILES) endif() -set_target_properties(${GUI_TARGET} PROPERTIES - MACOSX_BUNDLE_BUNDLE_NAME "Deling" - MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} - MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} - MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" - MACOSX_BUNDLE_GUI_IDENTIFIER org.myst6re.deling - MACOSX_BUNDLE_ICON_FILE ${RELEASE_NAME}.icns -) - -if(APPLE OR WIN32) - set_target_properties(${GUI_TARGET} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") +if(CLI) + qt_add_executable(${CLI_TARGET} MANUAL_FINALIZATION ${PROJECT_CLI_SOURCES} ${RESOURCES} ${EXTRA_RESOURCES_CLI}) + target_include_directories(${CLI_TARGET} PRIVATE "${CMAKE_SOURCE_DIR}/src") + target_link_libraries(${CLI_TARGET} PRIVATE + ZLIB::ZLIB + LZ4::lz4_static + ) + target_compile_definitions(${CLI_TARGET} + PRIVATE DELING_CONSOLE=1 QT_NO_DEBUG_OUTPUT=1 + ) endif() -add_translations(${GUI_TARGET} - TS_FILES ${TS_FILES} - QM_FILES_OUTPUT_VARIABLE QM_FILES) - include(GNUInstallDirs) if(APPLE) @@ -379,13 +420,23 @@ elseif(WIN32) if(NOT QT_DEPLOY_TMP_DIR) set(QT_DEPLOY_TMP_DIR "${CMAKE_BINARY_DIR}/win32-deploy" CACHE PATH "Directory to run deployqt scripts") endif() - install(TARGETS ${GUI_TARGET} RUNTIME DESTINATION "${QT_DEPLOY_TMP_DIR}") + if(GUI) + install(TARGETS ${GUI_TARGET} RUNTIME DESTINATION "${QT_DEPLOY_TMP_DIR}") + endif() + if(CLI) + install(TARGETS ${CLI_TARGET} RUNTIME DESTINATION "${QT_DEPLOY_TMP_DIR}") + endif() endif() add_subdirectory(deploy) if(APPLE) - install(TARGETS ${GUI_TARGET} BUNDLE DESTINATION ".") + if(GUI) + install(TARGETS ${GUI_TARGET} BUNDLE DESTINATION ".") + endif() + if(CLI) + install(TARGETS ${CLI_TARGET} RUNTIME EXCLUDE_FROM_ALL) + endif() elseif(WIN32) install(DIRECTORY "${QT_DEPLOY_TMP_DIR}/" DESTINATION ".") install(CODE "file(REMOVE_RECURSE \"${QT_DEPLOY_TMP_DIR}\")") @@ -393,10 +444,20 @@ else() install(FILES ${CMAKE_SOURCE_DIR}/src/qt/images/Deling.png DESTINATION share/icons/hicolor/256x256/apps RENAME io.github.myst6re.deling.png) install(FILES ${CMAKE_SOURCE_DIR}/deploy/linux/io.github.myst6re.deling.desktop DESTINATION share/applications) install(FILES ${QM_FILES} DESTINATION share/deling/translations) - install(TARGETS ${GUI_TARGET} RUNTIME) + if(GUI) + install(TARGETS ${GUI_TARGET} RUNTIME) + endif() + if(CLI) + install(TARGETS ${CLI_TARGET} RUNTIME) + endif() endif() -qt_finalize_executable(${GUI_TARGET}) +if(GUI) + qt_finalize_executable(${GUI_TARGET}) +endif() +if(CLI) + qt_finalize_executable(${CLI_TARGET}) +endif() if(CMAKE_SYSTEM_NAME MATCHES "Darwin") set(CPACK_SYSTEM_NAME "macos") diff --git a/deploy/CMakeLists.txt b/deploy/CMakeLists.txt index 147a187..44040aa 100644 --- a/deploy/CMakeLists.txt +++ b/deploy/CMakeLists.txt @@ -10,7 +10,7 @@ endif() install(FILES ${QM_FILES} DESTINATION "${TRANSLATIONS_PATH}") # Deploy Qt using macdeployqt and windeployqt scripts -if(APPLE OR WIN32) +if((APPLE AND GUI) OR WIN32) install(CODE "set(_target_file_dir \"${QT_DEPLOY_TMP_DIR}\")") install(CODE "set(_target_bundle_name \"${PROJECT_NAME}.app\")") install(CODE "set(_qt_translations_dir \"${_qt_translations_dir}\")") diff --git a/src/Arguments.cpp b/src/Arguments.cpp new file mode 100644 index 0000000..e6258a2 --- /dev/null +++ b/src/Arguments.cpp @@ -0,0 +1,186 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "Arguments.h" + +HelpArguments::HelpArguments() +{ + _ADD_FLAG(_OPTION_NAMES("h", "help"), "Displays help."); +} + +bool HelpArguments::help() const +{ + return _parser.isSet("help"); +} + +[[ noreturn ]] void HelpArguments::showHelp(int exitCode) +{ + QRegularExpression usage("Usage: .* \\[options\\]"); + qInfo("%s", qPrintable(_parser.helpText().replace(usage, "Usage: %1 [options]") + .arg(QFileInfo(qApp->arguments().first()).fileName()))); + ::exit(exitCode); +} + +CommonArguments::CommonArguments() +{ + _ADD_ARGUMENT("input-format", "Input format (fs).", "input-format", ""); + _ADD_ARGUMENT("include", "Include file name (without prefix c:\\ff8\\data\\). Repeat this argument to include multiple names. " + "If empty, all will be included by default.", "include", ""); + _ADD_ARGUMENT("exclude", "Exclude file name (without prefix c:\\ff8\\data\\). Repeat this argument to exclude multiple names. " + "Exclude has the priority over the --include argument.", "exclude", ""); + _ADD_ARGUMENT("include-from", "Include file names from file. The file format is one name per line.", "include", ""); + _ADD_ARGUMENT("exclude-from", "Exclude file names from file. The file format is one name per line.", "exclude", ""); +} + +QString CommonArguments::inputFormat() const +{ + QString inputFormat = _parser.value("input-format"); + if(inputFormat.isEmpty() && !_path.isEmpty()) { + qsizetype index = _path.lastIndexOf('.'); + if(index > -1) { + return _path.mid(index + 1); + } + return QString(); + } + return inputFormat; +} + +QStringList CommonArguments::includes() const +{ + return _parser.values("include") + _includesFromFile; +} + +QStringList CommonArguments::excludes() const +{ + return _parser.values("exclude") + _excludesFromFile; +} + +QStringList CommonArguments::searchFiles(const QString &path) +{ + qsizetype index = path.lastIndexOf('/'); + QString dirname, filename; + + if (index > 0) { + dirname = path.left(index); + filename = path.mid(index + 1); + } else { + filename = path; + } + + QDir dir(dirname); + QStringList entryList = dir.entryList(QStringList(filename), QDir::Files); + int i = 0; + for (const QString &entry: entryList) { + entryList.replace(i++, dir.filePath(entry)); + } + return entryList; +} + +QStringList CommonArguments::wilcardParse() +{ + QStringList paths, args = _parser.positionalArguments(); + + args.removeFirst(); + + for (const QString &path: args) { + if (path.contains('*') || path.contains('?')) { + paths.append(searchFiles(QDir::fromNativeSeparators(path))); + } else { + paths.append(QDir::fromNativeSeparators(path)); + } + } + + return paths; +} + +QStringList CommonArguments::mapNamesFromFile(const QString &path) +{ + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Warning: cannot open file")) + << qPrintable(path) << qPrintable(f.errorString()); + exit(1); + } + + QStringList ret; + + while (f.canReadLine()) { + ret.append(QString::fromUtf8(f.readLine())); + } + + return ret; +} + +void CommonArguments::mapNamesFromFiles() +{ + QStringList pathsInclude = _parser.values("include-from"); + + for (const QString &path: pathsInclude) { + _includesFromFile.append(mapNamesFromFile(path)); + } + + QStringList pathsExclude = _parser.values("exclude-from"); + + for (const QString &path: pathsExclude) { + _excludesFromFile.append(mapNamesFromFile(path)); + } +} + +Arguments::Arguments() : + _command(None) +{ + _parser.addVersionOption(); + _parser.addPositionalArgument( + "command", QCoreApplication::translate("Arguments", "Command to execute"), "" + ); + _parser.addPositionalArgument("args", "", "[]"); + _parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); + _parser.setApplicationDescription( + QCoreApplication::translate( + "Arguments", + "\nList of available commands:\n" + " export Export various assets from archive to files\n" + "\n" + "\"%1 export --help\" to see help of the specific subcommand" + ).arg(QFileInfo(qApp->arguments().first()).fileName()) + ); + + parse(); +} + +void Arguments::parse() +{ + _parser.process(*qApp); + + QStringList args = _parser.positionalArguments(); + + if (args.isEmpty()) { + qWarning() << qPrintable(QCoreApplication::translate("Arguments", "Please specify a command")); + return; + } + + const QString &command = args.first(); + + if (command == "export") { + _command = Export; + } else if (command == "import") { + _command = Import; + } else { + qWarning() << qPrintable(QCoreApplication::translate("Arguments", "Unknown command type:")) << qPrintable(command); + return; + } +} diff --git a/src/Arguments.h b/src/Arguments.h new file mode 100644 index 0000000..d5515c6 --- /dev/null +++ b/src/Arguments.h @@ -0,0 +1,74 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#define _ADD_ARGUMENT(names, description, valueName, defaultValue) \ + _parser.addOption(QCommandLineOption(names, description, valueName, defaultValue)) + +#define _ADD_FLAG(names, description) \ + _parser.addOption(QCommandLineOption(names, description)) + +#define _OPTION_NAMES(shortName, fullName) \ + (QStringList() << shortName << fullName) + +#include + +class HelpArguments +{ +public: + HelpArguments(); + [[ noreturn ]] void showHelp(int exitCode = EXIT_SUCCESS); + bool help() const; +protected: + QCommandLineParser _parser; +}; + +class CommonArguments : public HelpArguments +{ +public: + CommonArguments(); + inline QString path() const { + return _path; + } + QString inputFormat() const; + QStringList includes() const; + QStringList excludes() const; +protected: + QStringList wilcardParse(); + void mapNamesFromFiles(); + static QStringList mapNamesFromFile(const QString &path); + static QStringList searchFiles(const QString &path); + QString _path; + QStringList _includesFromFile, _excludesFromFile; +}; + +class Arguments : public HelpArguments +{ +public: + enum Command { + None, + Export, + Import + }; + Arguments(); + inline Command command() const { + return _command; + } +private: + void parse(); + Command _command; +}; diff --git a/src/ArgumentsExport.cpp b/src/ArgumentsExport.cpp new file mode 100644 index 0000000..d09f659 --- /dev/null +++ b/src/ArgumentsExport.cpp @@ -0,0 +1,54 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "ArgumentsExport.h" + +ArgumentsExport::ArgumentsExport() : CommonArguments() +{ + _parser.addPositionalArgument("file", QCoreApplication::translate("Arguments", "Input file or directory.")); + _parser.addPositionalArgument("directory", QCoreApplication::translate("ArgumentsExport", "Output directory.")); + + parse(); +} + +void ArgumentsExport::parse() +{ + _parser.process(*qApp); + + if (_parser.positionalArguments().size() > 3) { + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Error: too much parameters")); + exit(1); + } + + QStringList paths = wilcardParse(); + if (paths.size() == 2) { + // Output directory + if (QDir(paths.last()).exists()) { + _directory = paths.takeLast(); + } else { + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Error: target directory does not exist:")) + << qPrintable(paths.last()); + exit(1); + } + + if (!paths.isEmpty()) { + _path = paths.first(); + } + } + mapNamesFromFiles(); +} diff --git a/src/ArgumentsExport.h b/src/ArgumentsExport.h new file mode 100644 index 0000000..85c26e4 --- /dev/null +++ b/src/ArgumentsExport.h @@ -0,0 +1,32 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#include +#include "Arguments.h" + +class ArgumentsExport : public CommonArguments +{ +public: + ArgumentsExport(); + inline QString destination() const { + return _directory; + } +private: + void parse(); + QString _directory; +}; diff --git a/src/ArgumentsImport.cpp b/src/ArgumentsImport.cpp new file mode 100644 index 0000000..4affdcf --- /dev/null +++ b/src/ArgumentsImport.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "ArgumentsImport.h" + +ArgumentsImport::ArgumentsImport() : CommonArguments() +{ + _ADD_FLAG(_OPTION_NAMES("f", "force"), + "Overwrite destination file if exists."); + _ADD_ARGUMENT(_OPTION_NAMES("c", "compression"), "Compression format (lzs, lz4, none).", "compression-format", "lzs"); + + _parser.addPositionalArgument("directory", QCoreApplication::translate("ArgumentsImport", "Input directory.")); + _parser.addPositionalArgument("file", QCoreApplication::translate("Arguments", "Input file or directory.")); + + parse(); +} + +bool ArgumentsImport::force() const +{ + return _parser.isSet("force"); +} + +FiCompression ArgumentsImport::compressionFormat() const +{ + QString compression = _parser.value("compression"); + if (compression.isEmpty() || compression.toLower() == "lzs") { + return FiCompression::CompressionLzs; + } + if (compression.toLower() == "lz4") { + return FiCompression::CompressionLz4; + } + if (compression.toLower() == "none") { + return FiCompression::CompressionNone; + } + + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Error: unknown compression, available values: lzs, lz4, none")); + exit(1); +} + +void ArgumentsImport::parse() +{ + _parser.process(*qApp); + + if (_parser.positionalArguments().size() > 3) { + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Error: too much parameters")); + exit(1); + } + + QStringList paths = wilcardParse(); + if (paths.size() == 2) { + // Source directory + if (QDir(paths.first()).exists()) { + _directory = paths.takeFirst(); + } else { + qWarning() << qPrintable( + QCoreApplication::translate("Arguments", "Error: source directory does not exist:")) + << qPrintable(paths.first()); + exit(1); + } + + if (!paths.isEmpty()) { + _path = paths.first(); + } + } + mapNamesFromFiles(); +} diff --git a/src/ArgumentsImport.h b/src/ArgumentsImport.h new file mode 100644 index 0000000..d061213 --- /dev/null +++ b/src/ArgumentsImport.h @@ -0,0 +1,35 @@ +/**************************************************************************** + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#include +#include "Arguments.h" +#include "FsArchive.h" + +class ArgumentsImport : public CommonArguments +{ +public: + ArgumentsImport(); + bool force() const; + FiCompression compressionFormat() const; + inline QString source() const { + return _directory; + } +private: + void parse(); + QString _directory; +}; diff --git a/src/CLI.cpp b/src/CLI.cpp new file mode 100644 index 0000000..87136b9 --- /dev/null +++ b/src/CLI.cpp @@ -0,0 +1,228 @@ +/**************************************************************************** + ** Makou Reactor Final Fantasy VII Field Script Editor + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#include "CLI.h" +#include "Arguments.h" +#include "ArgumentsExport.h" +#include "ArgumentsImport.h" +#include "FsArchive.h" +#include "LZS.h" +#include "QLZ4.h" +#include +#include + +constexpr int BUFFER_SIZE = 4000000; + +void CLIObserver::setObserverValue(int value) +{ + quint8 percent = quint8(value * 100.0 / double(_maximum)); + + if (percent != _lastPercent) { + _lastPercent = percent; + setPercent(percent); + } +} + +void CLIObserver::setPercent(quint8 percent) +{ + printf("[%d%%] %s\r", percent, qPrintable(_filename)); + fflush(stdout); +} + +void CLIObserver::setObserverCanCancel(bool canCancel) const +{ + Q_UNUSED(canCancel) +} + +CLIObserver CLI::observer; + +void CLI::commandExport() +{ + ArgumentsExport args; + if (args.help() || args.destination().isEmpty()) { + args.showHelp(); + } + + FsArchive *archive = openArchive(args.inputFormat(), args.path()); + if (archive == nullptr) { + return; + } + + QString commonPath = "c:\\ff8\\data\\"; + QStringList fileList = archive->tocInDirectory(commonPath); + QStringList selectedFiles = filteredFiles(fileList, commonPath.size(), args.includes(), args.excludes()); + FsArchive::Error error = archive->extractFiles(selectedFiles, commonPath, args.destination(), &observer); + if (error != FsArchive::Ok) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "An error occured when exporting")) << qPrintable(FsArchive::errorString(error, args.path())); + } + + delete archive; +} + +void CLI::commandImport() +{ + ArgumentsImport args; + if (args.help() || args.source().isEmpty()) { + args.showHelp(); + } + FiCompression compressionFormat = args.compressionFormat(); + + QString path = args.path().left(args.path().size() - 1), + fsPath = FsArchive::fsPath(path), + fiPath = FsArchive::fiPath(path), + flPath = FsArchive::flPath(path); + if (!args.force() && (QFile::exists(fsPath) || QFile::exists(fiPath) || QFile::exists(flPath))) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "Destination file already exist, use --force to override")); + return; + } + + QString commonPath = "c:\\ff8\\data\\"; + QStringList fileList; + QDirIterator it(args.source(), QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + fileList.append(it.filePath()); + } + QStringList selectedFiles = filteredFiles(fileList, 0, args.includes(), args.excludes()); + QSaveFile fsFile(fsPath), fiFile(fiPath), flFile(flPath); + if (!fsFile.open(QIODevice::WriteOnly | QIODevice::Truncate) || !fiFile.open(QIODevice::WriteOnly | QIODevice::Truncate) || !flFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "An error occured when opening target files")) << qPrintable(fsFile.errorString()) << qPrintable(fiFile.errorString()) << qPrintable(flFile.errorString()); + return; + } + qDebug() << selectedFiles << fileList << args.source() << QDir::tempPath(); + observer.setObserverMaximum(selectedFiles.size()); + int i = 0; + for (QString fileName: selectedFiles) { + observer.setFilename(fileName); + if (observer.observerWasCanceled()) { + return; + } + observer.setObserverValue(i++); + QString fullName = commonPath + fileName.replace('/', '\\'); + flFile.write(fullName.toLatin1() + "\r\n"); + quint32 pos = quint32(fsFile.pos()); + QFile f(fileName); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "An error occured when exporting")) << qPrintable(f.errorString()); + return; + } + QByteArray data = f.readAll(), compressedData; + f.close(); + quint32 uncompressedSize = quint32(data.size()); + + switch (compressionFormat) { + case FiCompression::CompressionLzs: + compressedData = LZS::compress(data); + break; + case FiCompression::CompressionLz4: + compressedData = QLZ4::compress(data); + break; + case FiCompression::CompressionNone: + case FiCompression::CompressionUnknown: + compressedData = data; + break; + } + + quint32 compression = quint32(compressionFormat); + + if (compressedData.size() >= data.size()) { + compression = quint32(FiCompression::CompressionNone); + compressedData = data; + } + + fsFile.write(compressedData); + + fiFile.write((const char *)&uncompressedSize, 4); + fiFile.write((const char *)&pos, 4); + fiFile.write((const char *)&compression, 4); + } + + if (observer.observerWasCanceled()) { + return; + } + + if (!fsFile.commit() || !fiFile.commit() || !flFile.commit()) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "An error occured when exporting")) << qPrintable(fsFile.errorString()) << qPrintable(fiFile.errorString()) << qPrintable(flFile.errorString()); + } +} + +FsArchive *CLI::openArchive(const QString &ext, const QString &path) +{ + Q_UNUSED(ext) + FsArchive *archive = new FsArchive(path.left(path.size() - 1)); + if (!archive->isOpen()) { + qWarning() << qPrintable(QCoreApplication::translate("CLI", "Error")) << qPrintable(QCoreApplication::translate("CLI", "Cannot open archive")); + delete archive; + return nullptr; + } + + return archive; +} + +QStringList CLI::filteredFiles(const QStringList &fileList, int offset, const QStringList &includePatterns, const QStringList &excludePatterns) +{ + QStringList selectedFiles; + QList includes, excludes; + + for (const QString &pattern: includePatterns) { + includes.append(QRegularExpression(QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(pattern)))); + } + for (const QString &pattern: excludePatterns) { + excludes.append(QRegularExpression(QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(pattern)))); + } + + for (const QString &entry: fileList) { + bool found = includes.isEmpty(); + for (const QRegularExpression ®Exp: includes) { + if (regExp.match(entry, offset).hasMatch()) { + found = true; + break; + } + } + for (const QRegularExpression ®Exp: excludes) { + if (regExp.match(entry, offset).hasMatch()) { + found = false; + break; + } + } + + if (found) { + selectedFiles.append(entry); + } + } + + return selectedFiles; +} + +void CLI::exec() +{ + Arguments args; + if (args.help()) { + args.showHelp(); + } + + switch (args.command()) { + case Arguments::None: + args.showHelp(); + case Arguments::Export: + commandExport(); + break; + case Arguments::Import: + commandImport(); + break; + } +} diff --git a/src/CLI.h b/src/CLI.h new file mode 100644 index 0000000..432f96a --- /dev/null +++ b/src/CLI.h @@ -0,0 +1,56 @@ +/**************************************************************************** + ** Makou Reactor Final Fantasy VII Field Script Editor + ** Copyright (C) 2009-2021 Arzel Jérôme + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +#pragma once + +#include +#include "ArchiveObserver.h" + +class FsArchive; + +struct CLIObserver : public ArchiveObserver +{ + CLIObserver() {} + inline void setFilename(const QString &filename) { + _filename = filename; + } + inline bool observerWasCanceled() const override { + return false; + } + inline void setObserverMaximum(unsigned int max) override { + _maximum = max; + } + virtual void setObserverValue(int value) override; + virtual void setObserverCanCancel(bool canCancel) const override; +private: + void setPercent(quint8 percent); + qint64 _maximum; + quint8 _lastPercent; + QString _filename; +}; + +class CLI +{ +public: + static void exec(); +private: + static void commandExport(); + static void commandImport(); + static FsArchive *openArchive(const QString &ext, const QString &path); + static QStringList filteredFiles(const QStringList &fileList, int offset, const QStringList &includePatterns, const QStringList &excludePatterns); + static CLIObserver observer; +}; diff --git a/src/FsArchive.cpp b/src/FsArchive.cpp index b9976cc..ae3e79c 100644 --- a/src/FsArchive.cpp +++ b/src/FsArchive.cpp @@ -629,7 +629,7 @@ FsArchive::Error FsArchive::extractFiles(const QStringList &fileNames, const QSt fic.write(fileData(fileName, uncompress)); fic.close(); - progress->setObserverValue(++i); + progress->setObserverValue(i++); } return Ok; @@ -681,7 +681,7 @@ FsArchive::Error FsArchive::replaceFile(const QString &source, const QString &de } setFilePosition(entry, pos); - progress->setObserverValue(++i); + progress->setObserverValue(i++); } if (!saveAs(temp_path)) { @@ -863,7 +863,7 @@ FsArchive::Error FsArchive::remove(QStringList destinations, ArchiveObserver *pr } setFilePosition(entry, pos); - progress->setObserverValue(++i); + progress->setObserverValue(i++); } rebuildInfos(); @@ -970,6 +970,7 @@ bool FsArchive::open(const QString &path) fl.setFileName(FsArchive::flPath(path)); fi.setFileName(FsArchive::fiPath(path)); fs.setFileName(FsArchive::fsPath(path)); + qDebug() << fl.fileName() << fi.fileName() << fs.fileName(); if (!fl.open(QIODevice::ReadWrite)) { if (!fl.open(QIODevice::ReadOnly)) return false; diff --git a/src/LZS.h b/src/LZS.h index d67e4c6..725fcc5 100644 --- a/src/LZS.h +++ b/src/LZS.h @@ -25,7 +25,7 @@ **************************************************************/ #pragma once -#include +#include class LZS { diff --git a/src/main.cpp b/src/main.cpp index fe80549..33e86aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,17 +19,34 @@ //#define QT_NO_DEBUG_OUTPUT 1 //#define QT_NO_WARNING_OUTPUT 1 +#ifdef DELING_CONSOLE +#include +#include "CLI.h" +#else #include #include +#include "MainWindow.h" +#endif #include "Config.h" #include "FF8Font.h" -#include "MainWindow.h" // Only for static compilation //Q_IMPORT_PLUGIN(qjpcodecs) // jp encoding int main(int argc, char *argv[]) { +#ifdef DELING_CONSOLE + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName(DELING_NAME); + QCoreApplication::setApplicationVersion(DELING_VERSION); +#ifdef Q_OS_WIN + // QTextCodec::setCodecForLocale(QTextCodec::codecForName("IBM 850")); +#endif + Config::set(); + CLI::exec(); + + QTimer::singleShot(0, &app, &QCoreApplication::quit); +#else QApplication app(argc, argv); app.setWindowIcon(QIcon(":/images/deling.png")); @@ -72,6 +89,7 @@ int main(int argc, char *argv[]) if (argc > 1) { window->openFile(argv[1]); } +#endif return app.exec(); }