1
Fork 0
mirror of https://github.com/redstrate/Novus.git synced 2025-04-24 04:57:45 +00:00

Add mapeditor program

This allows viewing regions in the game. Some of them don't work, not
sure why yet.
This commit is contained in:
Joshua Goins 2024-02-02 14:29:48 -05:00
parent 0f0e6b5f1b
commit c19ff1e132
13 changed files with 508 additions and 0 deletions

View file

@ -47,6 +47,7 @@ add_subdirectory(argcracker)
add_subdirectory(sagasu) add_subdirectory(sagasu)
add_subdirectory(parts) add_subdirectory(parts)
add_subdirectory(common) add_subdirectory(common)
add_subdirectory(mapeditor)
add_subdirectory(mdlviewer) add_subdirectory(mdlviewer)
add_subdirectory(launcher) add_subdirectory(launcher)

View file

@ -16,6 +16,7 @@
static QMap<QString, QPair<QString, QString>> applications = { static QMap<QString, QPair<QString, QString>> applications = {
{QStringLiteral("Gear Editor"), {QStringLiteral("zone.xiv.armoury"), QStringLiteral("novus-armoury")}}, {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("Excel Editor"), {QStringLiteral("zone.xiv.karaku"), QStringLiteral("novus-karuku")}},
{QStringLiteral("Data Explorer"), {QStringLiteral("zone.xiv.sagasu"), QStringLiteral("novus-sagasu")}}, {QStringLiteral("Data Explorer"), {QStringLiteral("zone.xiv.sagasu"), QStringLiteral("novus-sagasu")}},
{QStringLiteral("Model Viewer"), {QStringLiteral("zone.xiv.mdlviewer"), QStringLiteral("novus-mdlviewer")}}}; {QStringLiteral("Model Viewer"), {QStringLiteral("zone.xiv.mdlviewer"), QStringLiteral("novus-mdlviewer")}}};

29
mapeditor/CMakeLists.txt Normal file
View file

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
# 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})

3
mapeditor/README.md Normal file
View file

@ -0,0 +1,3 @@
# Map Editor
Can view map files from the game.

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// 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;
};

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QListView>
#include <QWidget>
#include <physis.hpp>
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;
};

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "filecache.h"
#include "mdlpart.h"
#include <QComboBox>
#include <QWidget>
#include <physis.hpp>
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;
};

34
mapeditor/src/main.cpp Normal file
View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include <physis.hpp>
#include <physis_logger.h>
#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();
}

View file

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "mainwindow.h"
#include <QApplication>
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QListWidget>
#include <QMenuBar>
#include <QUrl>
#include <physis.hpp>
#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"

View file

@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "maplistwidget.h"
#include <QLineEdit>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QVBoxLayout>
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"

45
mapeditor/src/mapview.cpp Normal file
View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "mapview.h"
#include <QThreadPool>
#include <QVBoxLayout>
#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"

View file

@ -0,0 +1,10 @@
# SPDX-License-Identifier: CC0-1.0
# SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
[Desktop Entry]
Name=Novus SDK
Comment=FFXIV Data Editor
Exec=novus-karaku %u
Terminal=false
Icon=zone.xiv.karaku
Type=Application
NoDisplay=true

View file

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="32"
height="32"
viewBox="0 0 8.4666664 8.4666666"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="zone.xiv.karaku.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="11.313709"
inkscape:cx="6.8059028"
inkscape:cy="20.285126"
inkscape:window-width="1493"
inkscape:window-height="1052"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect91"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.29092048,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.24919772,0,1 @ F,0,0,1,0,0.25399137,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect90"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.24942529,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.22500024,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect89"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.20292531,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<linearGradient
id="linearGradient82"
inkscape:collect="always">
<stop
style="stop-color:#499e82;stop-opacity:1;"
offset="0"
id="stop81" />
<stop
style="stop-color:#255d45;stop-opacity:1;"
offset="1"
id="stop82" />
</linearGradient>
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#00db94;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#00a065;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="3.1669343"
y1="0.3943055"
x2="4.2333331"
y2="8.21772"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0177781,0,0,1.0177781,-0.07526033,-0.07526033)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient82"
id="linearGradient42"
x1="1.5205187"
y1="1.2191238"
x2="4.5218091"
y2="7.89182"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient82"
id="linearGradient89"
x1="2.8139815"
y1="1.9010228"
x2="4.5018134"
y2="6.1403022"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="paint-order:fill markers stroke;stroke-opacity:1;stroke-dasharray:none;stroke-linejoin:round;stroke-linecap:round;stroke-width:0.356;stroke:none;fill-opacity:1;fill:url(#linearGradient2);"
id="path1"
cx="4.2333336"
cy="4.2333336"
r="4.055222" />
<circle
style="fill:none;fill-opacity:0.347403;stroke:#361229;stroke-width:0.348515;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.118943;paint-order:fill markers stroke"
id="path1-2-7"
cx="4.3223338"
cy="4.3223338"
r="3.9699643" />
<circle
style="fill:none;fill-opacity:0.347403;stroke:url(#linearGradient42);stroke-width:0.356;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
id="path1-2"
cx="4.2333336"
cy="4.2333336"
r="4.055222" />
<path
id="rect84"
style="fill:url(#linearGradient89);fill-opacity:1;stroke:none;stroke-width:0.325;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.118943;paint-order:fill markers stroke"
d="M 2.8139816,1.9010227 5.0188151,1.8864138 C 5.4299922,2.3321074 5.4277127,2.336656 5.9435992,2.9269531 v 3.3875661 c 0,0.1376281 -0.1115502,0.2471941 -0.2491601,0.2450849 -0.5741006,-0.0088 -2.0102185,-0.026865 -2.9178178,-0.00397 -0.1400606,0.00353 -0.2533481,-0.1056591 -0.253126,-0.2459723 0.00114,-0.7177649 0.00392,-2.7942574 4.799e-4,-4.1158604 -4.176e-4,-0.1606305 0.129339,-0.2917098 0.2900064,-0.2927744 z"
sodipodi:nodetypes="cccccc"
inkscape:path-effect="#path-effect91"
inkscape:original-d="M 2.5230675,1.9029503 5.0188151,1.8864138 C 5.4299922,2.3321074 5.4277127,2.336656 5.9435992,2.9269531 v 3.6367638 c 0,0 -2.3380307,-0.043265 -3.4205317,0 0,0 0.00578,-3.107916 0,-4.6607666 z" />
<path
id="path88"
style="fill:#e5e5c9;fill-opacity:1;stroke:#98934c;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
d="m 3.3696105,5.2560686 3.3327514,0.024805 m -3.29293,-0.9833445 3.2901066,0.024805 M 5.6214416,3.3355553 5.6502729,6.132358 M 4.4825297,3.3272658 4.5018132,6.1403022 M 3.3736367,3.3188255 6.7063882,3.3436302 v 0 L 6.7005417,6.1566667 3.3677902,6.131862 Z"
sodipodi:nodetypes="cccccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB