diff --git a/CMakeLists.txt b/CMakeLists.txt index 185c2a3..08f2565 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ add_subdirectory(argcracker) add_subdirectory(sagasu) add_subdirectory(parts) add_subdirectory(common) +add_subdirectory(mapeditor) add_subdirectory(mdlviewer) add_subdirectory(launcher) diff --git a/launcher/src/mainwindow.cpp b/launcher/src/mainwindow.cpp index be6a342..a51a78b 100644 --- a/launcher/src/mainwindow.cpp +++ b/launcher/src/mainwindow.cpp @@ -16,6 +16,7 @@ static QMap> applications = { {QStringLiteral("Gear Editor"), {QStringLiteral("zone.xiv.armoury"), QStringLiteral("novus-armoury")}}, + {QStringLiteral("Map Editor"), {QStringLiteral("zone.xiv.mapeditor"), QStringLiteral("novus-mapeditor")}}, {QStringLiteral("Excel Editor"), {QStringLiteral("zone.xiv.karaku"), QStringLiteral("novus-karuku")}}, {QStringLiteral("Data Explorer"), {QStringLiteral("zone.xiv.sagasu"), QStringLiteral("novus-sagasu")}}, {QStringLiteral("Model Viewer"), {QStringLiteral("zone.xiv.mdlviewer"), QStringLiteral("novus-mdlviewer")}}}; diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt new file mode 100644 index 0000000..20a6d40 --- /dev/null +++ b/mapeditor/CMakeLists.txt @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2024 Joshua Goins +# SPDX-License-Identifier: CC0-1.0 + +add_executable(novus-mapeditor) +target_sources(novus-mapeditor + PRIVATE + include/mainwindow.h + include/maplistwidget.h + include/mapview.h + + src/main.cpp + src/mainwindow.cpp + src/maplistwidget.cpp + src/mapview.cpp) +target_include_directories(novus-mapeditor + PUBLIC + include) +target_link_libraries(novus-mapeditor + PRIVATE + Novus::Common + Novus::MdlPart + Physis::Physis + Physis::Logger + Qt6::Core + Qt6::Widgets) + +install(FILES zone.xiv.mapeditor.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +install(FILES zone.xiv.mapeditor.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps) +install(TARGETS novus-mapeditor ${KF${QT_MAJOR_VERSION}_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/mapeditor/README.md b/mapeditor/README.md new file mode 100644 index 0000000..04c9b32 --- /dev/null +++ b/mapeditor/README.md @@ -0,0 +1,3 @@ +# Map Editor + +Can view map files from the game. \ No newline at end of file diff --git a/mapeditor/include/mainwindow.h b/mapeditor/include/mainwindow.h new file mode 100644 index 0000000..541a9dc --- /dev/null +++ b/mapeditor/include/mainwindow.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "filecache.h" +#include "novusmainwindow.h" + +struct GameData; + +class MainWindow : public NovusMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(GameData *data); + +private: + GameData *data = nullptr; + FileCache cache; +}; \ No newline at end of file diff --git a/mapeditor/include/maplistwidget.h b/mapeditor/include/maplistwidget.h new file mode 100644 index 0000000..79f2bae --- /dev/null +++ b/mapeditor/include/maplistwidget.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +class MapListWidget : public QWidget +{ + Q_OBJECT + +public: + explicit MapListWidget(GameData *data, QWidget *parent = nullptr); + +Q_SIGNALS: + void mapSelected(const QString &name); + +private: + QListView *listWidget = nullptr; + + GameData *data = nullptr; +}; diff --git a/mapeditor/include/mapview.h b/mapeditor/include/mapview.h new file mode 100644 index 0000000..04f0df1 --- /dev/null +++ b/mapeditor/include/mapview.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "filecache.h" +#include "mdlpart.h" +#include +#include +#include + +struct GameData; + +class MapView : public QWidget +{ + Q_OBJECT + +public: + explicit MapView(GameData *data, FileCache &cache, QWidget *parent = nullptr); + + MDLPart &part() const; + +public Q_SLOTS: + void addTerrain(QString basePath, physis_Terrain terrain); + +private: + MDLPart *mdlPart = nullptr; + + GameData *data; + FileCache &cache; +}; \ No newline at end of file diff --git a/mapeditor/src/main.cpp b/mapeditor/src/main.cpp new file mode 100644 index 0000000..7b225a3 --- /dev/null +++ b/mapeditor/src/main.cpp @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "aboutdata.h" +#include "mainwindow.h" +#include "settings.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + customizeAboutData(QStringLiteral("mapeditor"), + QStringLiteral("zone.xiv.mapeditor"), + QStringLiteral("Map Editor"), + QStringLiteral("Program to view FFXIV maps.")); + + // Default to a sensible message pattern + if (qEnvironmentVariableIsEmpty("QT_MESSAGE_PATTERN")) { + qputenv("QT_MESSAGE_PATTERN", "[%{time yyyy-MM-dd h:mm:ss.zzz}] %{if-category}[%{category}] %{endif}[%{type}] %{message}"); + } + + setup_physis_logging(); + + const QString gameDir{getGameDirectory()}; + const std::string gameDirStd{gameDir.toStdString()}; + MainWindow w(physis_gamedata_initialize(gameDirStd.c_str())); + w.show(); + + return app.exec(); +} \ No newline at end of file diff --git a/mapeditor/src/mainwindow.cpp b/mapeditor/src/mainwindow.cpp new file mode 100644 index 0000000..72e41b0 --- /dev/null +++ b/mapeditor/src/mainwindow.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "mainwindow.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "maplistwidget.h" +#include "mapview.h" + +MainWindow::MainWindow(GameData *data) + : NovusMainWindow() + , data(data) + , cache(*data) +{ + setMinimumSize(1280, 720); + setupMenubar(); + + auto dummyWidget = new QWidget(); + setCentralWidget(dummyWidget); + + auto layout = new QHBoxLayout(); + dummyWidget->setLayout(layout); + + auto listWidget = new MapListWidget(data); + listWidget->setMaximumWidth(400); + layout->addWidget(listWidget); + + auto mapView = new MapView(data, cache); + layout->addWidget(mapView); + + connect(listWidget, &MapListWidget::mapSelected, this, [data, mapView](const QString &basePath) { + QString base2Path = basePath.left(basePath.lastIndexOf(QStringLiteral("/level/"))); + QString bgPath = QStringLiteral("bg/%1/bgplate/").arg(base2Path); + + std::string bgPathStd = bgPath.toStdString() + "terrain.tera"; + + auto tera_buffer = physis_gamedata_extract_file(data, bgPathStd.c_str()); + + auto tera = physis_parse_tera(tera_buffer); + mapView->addTerrain(bgPath, tera); + }); +} + +#include "moc_mainwindow.cpp" \ No newline at end of file diff --git a/mapeditor/src/maplistwidget.cpp b/mapeditor/src/maplistwidget.cpp new file mode 100644 index 0000000..032c07c --- /dev/null +++ b/mapeditor/src/maplistwidget.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "maplistwidget.h" + +#include +#include +#include +#include + +MapListWidget::MapListWidget(GameData *data, QWidget *parent) + : QWidget(parent) + , data(data) +{ + auto layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + auto searchModel = new QSortFilterProxyModel(); + searchModel->setRecursiveFilteringEnabled(true); + searchModel->setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + + auto searchEdit = new QLineEdit(); + searchEdit->setPlaceholderText(QStringLiteral("Search...")); + searchEdit->setClearButtonEnabled(true); + connect(searchEdit, &QLineEdit::textChanged, this, [=](const QString &text) { + searchModel->setFilterRegularExpression(text); + }); + layout->addWidget(searchEdit); + + auto originalModel = new QStandardItemModel(); + searchModel->setSourceModel(originalModel); + + auto exh = physis_parse_excel_sheet_header(physis_gamedata_extract_file(data, "exd/Map.exh")); + auto nameExh = physis_parse_excel_sheet_header(physis_gamedata_extract_file(data, "exd/PlaceName.exh")); + auto territoryExh = physis_parse_excel_sheet_header(physis_gamedata_extract_file(data, "exd/TerritoryType.exh")); + + // Only one page, it seems? + auto exd = physis_gamedata_read_excel_sheet(data, "Map", exh, Language::None, 0); + auto nameExd = physis_gamedata_read_excel_sheet(data, "PlaceName", nameExh, Language::English, 0); + auto territoryExd = physis_gamedata_read_excel_sheet(data, "TerritoryType", territoryExh, Language::None, 0); + + for (uint32_t i = 0; i < exd.row_count; i++) { + const char *id = exd.row_data[i].column_data[6].string._0; + + int territoryTypeKey = exd.row_data[i].column_data[15].u_int16._0; + if (territoryTypeKey > 0 && territoryTypeKey < territoryExd.row_count) { + const char *bg = territoryExd.row_data[territoryTypeKey].column_data[1].string._0; + + int placeRegionKey = territoryExd.row_data[territoryTypeKey].column_data[3].u_int16._0; + const char *placeRegion = nameExd.row_data[placeRegionKey].column_data[0].string._0; + + int placeZoneKey = territoryExd.row_data[territoryTypeKey].column_data[4].u_int16._0; + const char *placeZone = nameExd.row_data[placeZoneKey].column_data[0].string._0; + + int placeNameKey = territoryExd.row_data[territoryTypeKey].column_data[5].u_int16._0; + const char *placeName = nameExd.row_data[placeNameKey].column_data[0].string._0; + + QStandardItem *item = new QStandardItem(); + item->setData(QString::fromStdString(bg)); + item->setText(QStringLiteral("%1 (%2, %3, %4)") + .arg(QString::fromStdString(id), + QString::fromStdString(placeRegion), + QString::fromStdString(placeZone), + QString::fromStdString(placeName))); + + originalModel->insertRow(originalModel->rowCount(), item); + } + } + + listWidget = new QListView(); + listWidget->setModel(searchModel); + + connect(listWidget, &QListView::clicked, [this, searchModel](const QModelIndex &index) { + Q_EMIT mapSelected(searchModel->mapToSource(index).data(Qt::UserRole + 1).toString()); + }); + + layout->addWidget(listWidget); +} + +#include "moc_maplistwidget.cpp" \ No newline at end of file diff --git a/mapeditor/src/mapview.cpp b/mapeditor/src/mapview.cpp new file mode 100644 index 0000000..0eed3bf --- /dev/null +++ b/mapeditor/src/mapview.cpp @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "mapview.h" + +#include +#include + +#include "filecache.h" + +MapView::MapView(GameData *data, FileCache &cache, QWidget *parent) + : QWidget(parent) + , data(data) + , cache(cache) +{ + mdlPart = new MDLPart(data, cache); + mdlPart->enableFreemode(); + + auto layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(mdlPart); + setLayout(layout); +} + +MDLPart &MapView::part() const +{ + return *mdlPart; +} + +void MapView::addTerrain(QString basePath, physis_Terrain terrain) +{ + mdlPart->clear(); + + for (int i = 0; i < terrain.num_plates; i++) { + QString mdlPath = QStringLiteral("%1%2").arg(basePath, QString::fromStdString(terrain.plates[i].filename)); + std::string mdlPathStd = mdlPath.toStdString(); + + auto plateMdlFile = physis_gamedata_extract_file(data, mdlPathStd.c_str()); + auto plateMdl = physis_mdl_parse(plateMdlFile); + + mdlPart->addModel(plateMdl, glm::vec3(terrain.plates[i].position[0], 0.0f, terrain.plates[i].position[1]), QStringLiteral("terapart%1").arg(i), {}, 0); + } +} + +#include "moc_mapview.cpp" diff --git a/mapeditor/zone.xiv.mapeditor.desktop b/mapeditor/zone.xiv.mapeditor.desktop new file mode 100644 index 0000000..fe84670 --- /dev/null +++ b/mapeditor/zone.xiv.mapeditor.desktop @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2024 Joshua Goins +[Desktop Entry] +Name=Novus SDK +Comment=FFXIV Data Editor +Exec=novus-karaku %u +Terminal=false +Icon=zone.xiv.karaku +Type=Application +NoDisplay=true \ No newline at end of file diff --git a/mapeditor/zone.xiv.mapeditor.svg b/mapeditor/zone.xiv.mapeditor.svg new file mode 100644 index 0000000..c3be0e7 --- /dev/null +++ b/mapeditor/zone.xiv.mapeditor.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +