From 93cf929f29dea490ed60e5300cacdd99886c988e Mon Sep 17 00:00:00 2001 From: Chris Xiong Date: Sat, 9 Sep 2023 20:09:50 -0400 Subject: Add the standalone portion of mapman. --- .gitignore | 2 + mapman/CMakeLists.txt | 52 ++++++++ mapman/src/diffview.cpp | 37 ++++++ mapman/src/diffview.hpp | 21 +++ mapman/src/groupview.cpp | 237 +++++++++++++++++++++++++++++++++ mapman/src/groupview.hpp | 48 +++++++ mapman/src/library.cpp | 329 ++++++++++++++++++++++++++++++++++++++++++++++ mapman/src/library.hpp | 41 ++++++ mapman/src/main.cpp | 13 ++ mapman/src/mainwindow.cpp | 133 +++++++++++++++++++ mapman/src/mainwindow.hpp | 26 ++++ mapman/src/mapdump.cpp | 52 ++++++++ mapman/src/mapdump.hpp | 31 +++++ mapman/src/painter.cpp | 113 ++++++++++++++++ mapman/src/painter.hpp | 34 +++++ mapman/src/sliceview.cpp | 101 ++++++++++++++ mapman/src/sliceview.hpp | 32 +++++ mapman/src/utils.cpp | 40 ++++++ mapman/src/utils.hpp | 82 ++++++++++++ 19 files changed, 1424 insertions(+) create mode 100644 mapman/CMakeLists.txt create mode 100644 mapman/src/diffview.cpp create mode 100644 mapman/src/diffview.hpp create mode 100644 mapman/src/groupview.cpp create mode 100644 mapman/src/groupview.hpp create mode 100644 mapman/src/library.cpp create mode 100644 mapman/src/library.hpp create mode 100644 mapman/src/main.cpp create mode 100644 mapman/src/mainwindow.cpp create mode 100644 mapman/src/mainwindow.hpp create mode 100644 mapman/src/mapdump.cpp create mode 100644 mapman/src/mapdump.hpp create mode 100644 mapman/src/painter.cpp create mode 100644 mapman/src/painter.hpp create mode 100644 mapman/src/sliceview.cpp create mode 100644 mapman/src/sliceview.hpp create mode 100644 mapman/src/utils.cpp create mode 100644 mapman/src/utils.hpp diff --git a/.gitignore b/.gitignore index 09cd281..b3a7cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ bin/ # fabric run/ + +mapman/build diff --git a/mapman/CMakeLists.txt b/mapman/CMakeLists.txt new file mode 100644 index 0000000..99f1d67 --- /dev/null +++ b/mapman/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.14.0) +project(mapman CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +option(USE_QT6 "Build with Qt 6" OFF) +if (USE_QT6) + find_package(Qt6 REQUIRED COMPONENTS Widgets) +else() + find_package(Qt5 REQUIRED COMPONENTS Widgets) +endif() + +find_package(ZLIB REQUIRED) +find_package(SQLite3 REQUIRED) +include_directories(${ZLIB_INCLUDE_DIRS}) +include_directories(${SQLite3_INCLUDE_DIRS}) + +INCLUDE (CheckTypeSize) + +SET(CMAKE_EXTRA_INCLUDE_FILES "filesystem") +check_type_size("std::filesystem::path::value_type" PATH_VALSIZE LANGUAGE CXX) +SET(CMAKE_EXTRA_INCLUDE_FILES) + +add_compile_definitions(PATH_VALSIZE=${PATH_VALSIZE}) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(mapman_SOURCES + src/main.cpp + src/mapdump.cpp + src/utils.cpp + src/library.cpp + src/painter.cpp + src/sliceview.cpp + src/groupview.cpp + src/diffview.cpp + src/mainwindow.cpp +) +add_executable(mapman ${mapman_SOURCES}) + +target_link_libraries(mapman + ${ZLIB_LIBRARIES} +) + +if (USE_QT6) + target_link_libraries(mapman Qt6::Widgets ${SQLite3_LIBRARIES}) +else() + target_link_libraries(mapman Qt5::Widgets ${SQLite3_LIBRARIES}) +endif() diff --git a/mapman/src/diffview.cpp b/mapman/src/diffview.cpp new file mode 100644 index 0000000..3c437c4 --- /dev/null +++ b/mapman/src/diffview.cpp @@ -0,0 +1,37 @@ +#include "diffview.hpp" + +#include +#include +#include +#include +#include + +diff_view::diff_view(QWidget *par) : QWidget(par) +{ + this->setWindowTitle("Map tally comparison results"); + tea_b = new QTextEdit(); + teb_a = new QTextEdit(); + tea_b->setReadOnly(true); + teb_a->setReadOnly(true); + auto hl = new QHBoxLayout(); + auto lvl = new QVBoxLayout(); + lvl->addWidget(new QLabel("Maps present in library but not in tally")); + lvl->addWidget(tea_b); + auto rvl = new QVBoxLayout(); + rvl->addWidget(new QLabel("Maps present in tally but not in library")); + rvl->addWidget(teb_a); + hl->addLayout(lvl); + hl->addLayout(rvl); + this->setLayout(hl); +} + +void diff_view::set_results(const std::vector &a_b, const std::vector &b_a) +{ + tea_b->clear(); + teb_a->clear(); + QString sa_b, sb_a; + for (auto &i : a_b) sa_b.append(QString("%1\n").arg(i)); + for (auto &i : b_a) sb_a.append(QString("%1\n").arg(i)); + tea_b->setText(sa_b); + teb_a->setText(sb_a); +} diff --git a/mapman/src/diffview.hpp b/mapman/src/diffview.hpp new file mode 100644 index 0000000..d506f3a --- /dev/null +++ b/mapman/src/diffview.hpp @@ -0,0 +1,21 @@ +#ifndef DIFFVIEW_HPP +#define DIFFVIEW_HPP + +#include + +#include + +class QTextEdit; + +class diff_view : public QWidget +{ + Q_OBJECT +public: + diff_view(QWidget* par = nullptr); + void set_results(const std::vector &a_b, const std::vector &b_a); +private: + QTextEdit *tea_b; + QTextEdit *teb_a; +}; + +#endif diff --git a/mapman/src/groupview.cpp b/mapman/src/groupview.cpp new file mode 100644 index 0000000..32917d4 --- /dev/null +++ b/mapman/src/groupview.cpp @@ -0,0 +1,237 @@ +#include "groupview.hpp" +#include "library.hpp" +#include "painter.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +group_view::group_view() : QMdiSubWindow() +{ + auto sp = new QSplitter(this); + this->setWidget(sp); + auto leftpane = new QWidget(); + auto l1 = new QVBoxLayout(); + leftpane->setLayout(l1); + tv = new QTableView(); + m = new QStandardItemModel(); + tv->setModel(m); + tv->setColumnHidden(3, true); + tv->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + tv->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); + connect(tv->selectionModel(), &QItemSelectionModel::currentRowChanged, + [this](const QModelIndex &curidx, const QModelIndex &oldidx) { + if (oldidx.isValid() && curidx.row() == oldidx.row()) return; + bool dirty = oldidx.isValid() && this->dirty; + if (oldidx.isValid()) + { + int64_t oldgid = m->item(oldidx.row(), 3)->data(Qt::ItemDataRole::DisplayRole).toLongLong(); + auto oldgroup = l->get_group(oldgid); + dirty |= tetitle->text() != QString::fromStdString(oldgroup.title); + dirty |= teauthor->text() != QString::fromStdString(oldgroup.author); + dirty |= sbh->value() != oldgroup.hc; + dirty |= sbv->value() != oldgroup.vc; + } + if (dirty) + { + if (QMessageBox::question(this, "Unsaved changes", "Changes to map art not saved! Discard and switch to another map art?") != QMessageBox::StandardButton::Yes) + { + int row = oldidx.row(); + QMetaObject::invokeMethod(this, [this, row]() { + QSignalBlocker b(tv->selectionModel()); + tv->selectRow(row); + }, Qt::ConnectionType::QueuedConnection); + return; + } + } + this->update_fields(); + }); + l1->addWidget(tv); + auto l2 = new QHBoxLayout(); + pbadd = new QPushButton("+"); + pbrem = new QPushButton("-"); + connect(pbadd, &QPushButton::pressed, this, &group_view::add_group); + connect(pbrem, &QPushButton::pressed, this, &group_view::rem_group); + l2->addWidget(pbadd); + l2->addWidget(pbrem); + l1->addLayout(l2); + sp->addWidget(leftpane); + + auto rightpane = new QWidget(); + auto l3 = new QVBoxLayout(); + rightpane->setLayout(l3); + p = new map_painter(); + l3->addWidget(p->view()); + connect(p, &map_painter::map_id_changed, this, &group_view::painter_drop); + tetitle = new QLineEdit(); + teauthor = new QLineEdit(); + tetitle->setPlaceholderText("Title"); + teauthor->setPlaceholderText("Author(s)"); + l3->addWidget(tetitle); + l3->addWidget(teauthor); + auto l4 = new QHBoxLayout(); + sbv = new QSpinBox(); + sbh = new QSpinBox(); + sbv->setValue(1); + sbv->setMinimum(1); + sbv->setMaximum(100); + connect(sbv, QOverload::of(&QSpinBox::valueChanged), this, &group_view::reset_dim); + sbh->setValue(1); + sbh->setMinimum(1); + sbh->setMaximum(100); + connect(sbh, QOverload::of(&QSpinBox::valueChanged), this, &group_view::reset_dim); + l4->addWidget(sbh); + l4->addWidget(new QLabel("x")); + l4->addWidget(sbv); + l4->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding)); + pbapply = new QPushButton("Save"); + connect(pbapply, &QPushButton::pressed, this, &group_view::update_library); + + l4->addWidget(pbapply); + l3->addLayout(l4); + sp->addWidget(rightpane); + + sp->setStretchFactor(0, 1); + sp->setStretchFactor(1, 3); + sp->setCollapsible(0, false); + sp->setCollapsible(1, false); + l = nullptr; + dirty = false; + this->setWindowTitle("Map art listings"); + this->setAttribute(Qt::WA_DeleteOnClose, false); +} + +group_view::~group_view() +{ + delete p; +} + +void group_view::set_library(map_library *lib) +{ + l = lib; + p->set_map_library(l); + refresh_list(); +} + +void group_view::add_group() +{ + map_group_t g { + std::string(), + std::string(), + 1, 1, + {0}, + {false} + }; + l->new_group(g); + refresh_list(); +} + +void group_view::rem_group() +{ + if (!tv->currentIndex().isValid()) + return; + int64_t curgid = m->item(tv->currentIndex().row(), 3)->data(Qt::ItemDataRole::DisplayRole).toLongLong(); + l->remove_group(curgid); + refresh_list(); +} + +void group_view::update_fields() +{ + if (!tv->currentIndex().isValid()) + return; + int64_t curgid = m->item(tv->currentIndex().row(), 3)->data(Qt::ItemDataRole::DisplayRole).toLongLong(); + current_group = l->get_group(curgid); + auto &g = current_group; + tetitle->setText(QString::fromStdString(g.title)); + teauthor->setText(QString::fromStdString(g.author)); + QSignalBlocker bh(sbh); + QSignalBlocker bv(sbv); + sbh->setValue(g.hc); + sbv->setValue(g.vc); + update_map_view(); + dirty = false; +} + +void group_view::painter_drop(int pos, bool populated, int id) +{ + if (!tv->currentIndex().isValid()) + return; + auto &g = current_group; + g.populated[pos] = populated; + g.ids[pos] = id; + dirty = true; +} + +void group_view::update_library() +{ + if (!tv->currentIndex().isValid()) + return; + int64_t curgid = m->item(tv->currentIndex().row(), 3)->data(Qt::ItemDataRole::DisplayRole).toLongLong(); + auto &g = current_group; + g.title = tetitle->text().toStdString(); + g.author = teauthor->text().toStdString(); + g.hc = sbh->value(); + g.vc = sbv->value(); + l->set_group(curgid, g); + refresh_list(); + dirty = false; +} + +void group_view::refresh_list() +{ + int64_t curgid = -1; + if (tv->currentIndex().isValid()) + curgid = m->item(tv->currentIndex().row(), 3)->data(Qt::ItemDataRole::DisplayRole).toLongLong(); + m->clear(); + m->setHorizontalHeaderLabels({"Title", "Author(s)", "Dimension", "id"}); + tv->setColumnHidden(3, true); + auto gids = l->groups(); + for (auto gid : gids) + { + map_group_t g = l->get_group(gid); + QStandardItem *t = new QStandardItem(QString::fromStdString(g.title)); + QStandardItem *a = new QStandardItem(QString::fromStdString(g.author)); + QStandardItem *d = new QStandardItem(QString("%1x%2").arg(g.hc).arg(g.vc)); + QStandardItem *i = new QStandardItem(); + i->setData(QVariant((qlonglong)gid), Qt::ItemDataRole::DisplayRole); + m->appendRow({t, a, d, i}); + if (gid == curgid) + tv->setCurrentIndex(t->index()); + } + tv->resizeColumnsToContents(); +} + +void group_view::reset_dim() +{ + p->set_dimension(sbh->value(), sbv->value()); + if (!tv->currentIndex().isValid()) + return; + auto &g = current_group; + g.hc = sbh->value(); + g.vc = sbv->value(); + g.ids.resize(g.hc * g.vc); + g.populated.resize(g.hc * g.vc); + std::fill(g.populated.begin(), g.populated.end(), false); + dirty = true; +} + +void group_view::update_map_view() +{ + auto &g = current_group; + p->set_dimension(g.hc, g.vc); + for (int i = 0; i < g.hc * g.vc; ++i) + p->set_map_id(i, g.populated[i], g.ids[i]); +} diff --git a/mapman/src/groupview.hpp b/mapman/src/groupview.hpp new file mode 100644 index 0000000..ca4cf30 --- /dev/null +++ b/mapman/src/groupview.hpp @@ -0,0 +1,48 @@ +#ifndef GROUPVIEW_HPP +#define GROUPVIEW_HPP + +#include +#include "mapdump.hpp" + +class QTableView; +class QPushButton; +class QLineEdit; +class QSpinBox; +class QLabel; +class QStandardItemModel; +class map_library; +class map_painter; + +class group_view : public QMdiSubWindow +{ +Q_OBJECT +public: + group_view(); + ~group_view(); + void set_library(map_library *lib); +public slots: + void add_group(); + void rem_group(); + void update_fields(); + void update_library(); + void painter_drop(int pos, bool populated, int id); + void refresh_list(); + void reset_dim(); + void update_map_view(); +private: + QTableView *tv; + QStandardItemModel *m; + QPushButton *pbadd; + QPushButton *pbrem; + QLineEdit *tetitle; + QLineEdit *teauthor; + QSpinBox *sbh; + QSpinBox *sbv; + QPushButton *pbapply; + bool dirty; + map_library *l; + map_painter *p; + map_group_t current_group; +}; + +#endif diff --git a/mapman/src/library.cpp b/mapman/src/library.cpp new file mode 100644 index 0000000..2361441 --- /dev/null +++ b/mapman/src/library.cpp @@ -0,0 +1,329 @@ +#include "library.hpp" +#include "mapdump.hpp" + +#include +#include +#include + +const int MAPDB_VERSION = 1; + +map_library::map_library() : db(nullptr) {} + +map_library::~map_library() +{ + if (db) + sqlite3_close(db); +} + +std::vector map_library::map_ids() const +{ + std::vector ret; + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select id from maps;", -1, &st, 0); + while (1) + { + int r = sqlite3_step(st); + if (r != SQLITE_ROW) break; + ret.push_back(sqlite3_column_int(st, 0)); + } + sqlite3_finalize(st); + return ret; +} + +bool map_library::has_map(int id) const +{ + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select id from maps where id = ?;", -1, &st, 0); + sqlite3_bind_int(st, 1, id); + int r = sqlite3_step(st); + sqlite3_finalize(st); + return r == SQLITE_ROW; +} + +void map_library::set_map(const map_t &map) +{ + sqlite3_stmt *st = nullptr; + if (has_map(map.id)) + { + sqlite3_prepare_v2(db, "update maps set custom_name = ?, data = ? where id = ?;", -1, &st, 0); + sqlite3_bind_text(st, 1, map.custom_name.c_str(), map.custom_name.length(), SQLITE_STATIC); + sqlite3_bind_blob(st, 2, map.map_data.data(), map.map_data.size(), SQLITE_STATIC); + sqlite3_bind_int(st, 3, map.id); + sqlite3_step(st); + } + else + { + sqlite3_prepare_v2(db, "insert into maps (id, custom_name, data) values(?, ?, ?);", -1, &st, 0); + sqlite3_bind_int(st, 1, map.id); + sqlite3_bind_text(st, 2, map.custom_name.c_str(), map.custom_name.length(), SQLITE_STATIC); + sqlite3_bind_blob(st, 3, map.map_data.data(), map.map_data.size(), SQLITE_STATIC); + sqlite3_step(st); + } + sqlite3_finalize(st); +} + +map_t map_library::get_map(int id) const +{ + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select custom_name, data from maps where id = ?;", -1, &st, 0); + sqlite3_bind_int(st, 1, id); + map_t ret{id, std::string(), map_data_t()}; + if (sqlite3_step(st) == SQLITE_ROW) + { + ret.custom_name = std::string((char*)sqlite3_column_text(st, 0)); + memcpy(ret.map_data.data(), sqlite3_column_blob(st, 1), ret.map_data.size()); + } + sqlite3_finalize(st); + return ret; +} + +std::vector map_library::groups() const +{ + std::vector ret; + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select rowid from groups;", -1, &st, 0); + while (1) + { + int r = sqlite3_step(st); + if (r != SQLITE_ROW) break; + ret.push_back(sqlite3_column_int64(st, 0)); + } + sqlite3_finalize(st); + return ret; +} + +int64_t map_library::new_group(const map_group_t &g) +{ + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "insert into groups (title, author, horizontal_count, vertical_count) values(?, ?, ?, ?);", -1, &st, 0); + sqlite3_bind_text(st, 1, g.title.c_str(), g.title.length(), SQLITE_STATIC); + sqlite3_bind_text(st, 2, g.author.c_str(), g.author.length(), SQLITE_STATIC); + sqlite3_bind_int(st, 3, g.hc); + sqlite3_bind_int(st, 4, g.vc); + sqlite3_step(st); + sqlite3_finalize(st); + int64_t id = sqlite3_last_insert_rowid(db); + sqlite3_prepare_v2(db, "insert into group_maps (gid, pos, map_id) values(?, ?, ?);", -1, &st, 0); + sqlite3_bind_int64(st, 1, id); + for (size_t i = 0; i < g.ids.size(); ++i) + { + if (!g.populated[i]) + continue; + sqlite3_bind_int(st, 2, i); + sqlite3_bind_int(st, 3, g.ids[i]); + sqlite3_step(st); + if (i + 1 < g.ids.size()) + sqlite3_reset(st); + } + sqlite3_finalize(st); + return id; +} + +bool map_library::has_group(int64_t gid) const +{ + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select rowid from groups where rowid = ?;", -1, &st, 0); + sqlite3_bind_int(st, 1, gid); + int r = sqlite3_step(st); + sqlite3_finalize(st); + return r == SQLITE_ROW; +} + +void map_library::set_group(int64_t gid, const map_group_t &g) +{ + if (!has_group(gid)) + return; + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, R"sql( + update groups set title = ?, author = ?, horizontal_count = ?, vertical_count = ? where rowid = ?; + )sql", -1, &st, 0); + sqlite3_bind_text(st, 1, g.title.c_str(), g.title.length(), SQLITE_STATIC); + sqlite3_bind_text(st, 2, g.author.c_str(), g.author.length(), SQLITE_STATIC); + sqlite3_bind_int(st, 3, g.hc); + sqlite3_bind_int(st, 4, g.vc); + sqlite3_bind_int64(st, 5, gid); + sqlite3_step(st); + sqlite3_finalize(st); + sqlite3_prepare_v2(db, R"sql( + delete from group_maps where gid = ?; + )sql", -1, &st, 0); + sqlite3_bind_int64(st, 1, gid); + sqlite3_step(st); + sqlite3_finalize(st); + sqlite3_prepare_v2(db, "insert into group_maps (gid, pos, map_id) values(?, ?, ?);", -1, &st, 0); + sqlite3_bind_int(st, 1, gid); + for (size_t i = 0; i < g.ids.size(); ++i) + { + if (!g.populated[i]) + continue; + sqlite3_bind_int(st, 2, i); + sqlite3_bind_int(st, 3, g.ids[i]); + sqlite3_step(st); + if (i + 1 < g.ids.size()) + sqlite3_reset(st); + } + sqlite3_finalize(st); +} + +map_group_t map_library::get_group(int64_t gid) +{ + map_group_t ret{}; + if (!has_group(gid)) + return ret; + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "select title, author, horizontal_count, vertical_count from groups where rowid = ?;", -1, &st, 0); + sqlite3_bind_int64(st, 1, gid); + if (sqlite3_step(st) == SQLITE_ROW) + { + ret.title = std::string((char*)sqlite3_column_text(st, 0)); + ret.author = std::string((char*)sqlite3_column_text(st, 1)); + ret.hc = sqlite3_column_int(st, 2); + ret.vc = sqlite3_column_int(st, 3); + } + sqlite3_finalize(st); + ret.ids.resize(ret.hc * ret.vc); + ret.populated.resize(ret.hc * ret.vc, false); + sqlite3_prepare_v2(db, "select pos, map_id from group_maps where gid = ?;", -1, &st, 0); + sqlite3_bind_int(st, 1, gid); + while (sqlite3_step(st) == SQLITE_ROW) + { + int pos = sqlite3_column_int(st, 0); + int id = sqlite3_column_int(st, 1); + if (pos < ret.hc * ret.vc) + { + ret.ids[pos] = id; + ret.populated[pos] = true; + } + } + sqlite3_finalize(st); + return ret; +} + +void map_library::remove_group(int64_t gid) const +{ + if (!has_group(gid)) + return; + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, R"sql( + delete from groups where rowid = ?; + delete from group_maps where gid = ?1; + )sql", -1, &st, 0); + sqlite3_bind_int64(st, 1, gid); + sqlite3_step(st); + sqlite3_finalize(st); +} + +/* + * tally (in): list of maps in storage containers + * a_b (out): maps that are in the library but not in containers + * b_a (out): maps that are in containers but not in the library + */ +void map_library::tally_diff(const std::vector &tally, + std::vector &a_b, + std::vector &b_a) const +{ + sqlite3_exec(db, "create table temp_tally(id int);", nullptr, nullptr, nullptr); + sqlite3_stmt *st = nullptr; + sqlite3_prepare_v2(db, "insert into temp_tally (id) values(?);", -1, &st, 0); + for (auto &id : tally) + { + sqlite3_bind_int(st, 1, id); + sqlite3_step(st); + sqlite3_reset(st); + } + sqlite3_finalize(st); + sqlite3_prepare_v2(db, R"sql( + select maps.id as mid, temp_tally.id as tid + from maps full outer join temp_tally on maps.id = temp_tally.id + where mid is NULL or tid is NULL; + )sql", -1, &st, 0); + a_b.clear(); + b_a.clear(); + while (sqlite3_step(st) == SQLITE_ROW) + { + int mid = sqlite3_column_int(st, 0); + int tid = sqlite3_column_int(st, 1); + bool mid_null = sqlite3_column_type(st, 0) == SQLITE_NULL; + bool tid_null = sqlite3_column_type(st, 1) == SQLITE_NULL; + if (mid_null) + b_a.push_back(tid); + if (tid_null) + a_b.push_back(mid); + } + sqlite3_finalize(st); + sqlite3_exec(db, "drop table temp_tally;", nullptr, nullptr, nullptr); + std::sort(a_b.begin(), a_b.end()); + std::sort(b_a.begin(), b_a.end()); +} + + +bool map_library::open_db(const std::filesystem::path &p) +{ + bool needs_creation = !std::filesystem::is_regular_file(p); +#if PATH_VALSIZE == 2 + sqlite3_open16(p.c_str(), &db); +#else + sqlite3_open(p.c_str(), &db); +#endif + if (needs_creation) + init_db(); + else + { + if (!verify_db()) + { + sqlite3_close(db); + db = nullptr; + return false; + } + } + return true; +} + +void map_library::init_db() +{ + sqlite3_exec(db, R"sql( + create table mapdbinfo( + version int, + name text + ); + )sql", nullptr, nullptr, nullptr); + + sqlite3_stmt *vst; + sqlite3_prepare_v2(db, "insert into mapdbinfo (version) values(?);", -1, &vst, 0); + sqlite3_bind_int(vst, 1, MAPDB_VERSION); + sqlite3_step(vst); + sqlite3_finalize(vst); + + sqlite3_exec(db, R"sql( + create table maps( + id integer primary key, + custom_name text, + data blob + ); + )sql", nullptr, nullptr, nullptr); + sqlite3_exec(db, R"sql( + create table groups( + title text, + author text, + horizontal_count integer, + vertical_count integer + ); + )sql", nullptr, nullptr, nullptr); + sqlite3_exec(db, R"sql( + create table group_maps( + gid integer, + pos integer, + map_id integer + ); + )sql", nullptr, nullptr, nullptr); +} + +bool map_library::verify_db() +{ + sqlite3_stmt *vst; + sqlite3_prepare_v2(db, "select version from mapdbinfo;", -1, &vst, 0); + if (sqlite3_step(vst) != SQLITE_ROW) {sqlite3_finalize(vst); return false;} + if (MAPDB_VERSION != sqlite3_column_int(vst, 0)) {sqlite3_finalize(vst); return false;} + sqlite3_finalize(vst); + return true; +} diff --git a/mapman/src/library.hpp b/mapman/src/library.hpp new file mode 100644 index 0000000..82babe0 --- /dev/null +++ b/mapman/src/library.hpp @@ -0,0 +1,41 @@ +#ifndef LIBRARY_HPP +#define LIBRARY_HPP + +#include "mapdump.hpp" + +#include +#include +#include + +struct sqlite3; + +class map_library +{ +public: + map_library(); + ~map_library(); + + std::vector map_ids() const; + bool has_map(int id) const; + void set_map(const map_t &map); + map_t get_map(int id) const; + + std::vector groups() const; + int64_t new_group(const map_group_t &g); + bool has_group(int64_t gid) const; + void set_group(int64_t gid, const map_group_t &g); + map_group_t get_group(int64_t gid); + void remove_group(int64_t gid) const; + + void tally_diff(const std::vector &tally, + std::vector &a_b, + std::vector &b_a) const; + + bool open_db(const std::filesystem::path &p); +private: + void init_db(); + bool verify_db(); + sqlite3 *db; +}; + +#endif diff --git a/mapman/src/main.cpp b/mapman/src/main.cpp new file mode 100644 index 0000000..b30b0f2 --- /dev/null +++ b/mapman/src/main.cpp @@ -0,0 +1,13 @@ +#include "mainwindow.hpp" +#include + +int main(int argc, char **argv) +{ + QApplication a(argc, argv); + + mapman_main_window mw; + mw.show(); + + a.exec(); + return 0; +} diff --git a/mapman/src/mainwindow.cpp b/mapman/src/mainwindow.cpp new file mode 100644 index 0000000..83d70c0 --- /dev/null +++ b/mapman/src/mainwindow.cpp @@ -0,0 +1,133 @@ +#include "mainwindow.hpp" +#include "groupview.hpp" +#include "mapdump.hpp" +#include "sliceview.hpp" +#include "diffview.hpp" +#include "library.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MAPMAN_VERSION "pre-alpha" + +mapman_main_window::mapman_main_window() : QMainWindow() +{ + l = nullptr; + cw = new QMdiArea(); + setCentralWidget(cw); + gv = new group_view(); + sv = new slice_view(); + dv = new diff_view(); + cw->addSubWindow(gv); + cw->addSubWindow(sv); + dv->hide(); + this->setWindowTitle("Mapman"); + cw->cascadeSubWindows(); + auto fm = this->menuBar()->addMenu("&File"); + auto cra = fm->addAction("Create / L&oad MapDB..."); + auto cla = fm->addAction("&Close MapDB"); + fm->addSeparator(); + auto lda = fm->addAction("&Load Map Dump..."); + fm->addSeparator(); + auto cta = fm->addAction("&Compare Map Tally..."); + fm->addSeparator(); + auto qa = fm->addAction("&Quit"); + cla->setEnabled(false); + lda->setEnabled(false); + cta->setEnabled(false); + connect(cra, &QAction::triggered, [this, cla, lda, cta] { + QString fn = QFileDialog::getSaveFileName(this, "Create / Load MapDB", QString(), "*.mapdb", nullptr, QFileDialog::Option::DontConfirmOverwrite); + if (fn.length()) + { + if (l) delete l; + l = new map_library(); + std::filesystem::path p(fn.toStdWString()); + if (!l->open_db(p)) + { + delete l; + l = nullptr; + return; + } + sv->set_library(l); + gv->set_library(l); + sv->refresh(); + gv->refresh_list(); + cla->setEnabled(true); + lda->setEnabled(true); + cta->setEnabled(true); + } + }); + connect(cla, &QAction::triggered, [this, cla, lda, cta] { + if (l) delete l; + l = nullptr; + cla->setEnabled(false); + lda->setEnabled(false); + cta->setEnabled(false); + }); + connect(lda, &QAction::triggered, [this] { + if (!l) return; + QString fn = QFileDialog::getOpenFileName(this, "Load Map Dump", QString(), "*.gz"); + if (fn.length()) + { + std::vector m; + if (load_dumps(fn.toStdString().c_str(), m)) + { + for (auto &map : m) + l->set_map(map); + sv->refresh(); + } + } + }); + connect(cta, &QAction::triggered, [this] { + if (!l) return; + QString fn = QFileDialog::getOpenFileName(this, "Select Map Tally", QString(), "*.gz"); + if (fn.length()) + { + auto tally = load_tally(fn.toStdString().c_str()); + std::vector a_b, b_a; + l->tally_diff(tally, a_b, b_a); + dv->set_results(a_b, b_a); + dv->show(); + } + }); + connect(qa, &QAction::triggered, [] { + QApplication::exit(); + }); + auto wm = this->menuBar()->addMenu("&Windows"); + auto swa = wm->addAction("Map &listings"); + auto gwa = wm->addAction("Map &art listings"); + connect(swa, &QAction::triggered, [this]{ sv->widget()->show(); sv->show(); }); + connect(gwa, &QAction::triggered, [this]{ gv->widget()->show(); gv->show(); }); + auto hm = this->menuBar()->addMenu("&Help"); + auto aba = hm->addAction("&About"); + auto aqa = hm->addAction("About &Qt"); + connect(aba, &QAction::triggered, [this]{ + QMessageBox::about(this, "About Mapman", QString(R"( +Mapman +A minecraft map art manager. + +%1 + +Chris Xiong 2023 + +License: MIT (expat) +)") + .arg(MAPMAN_VERSION).trimmed()); + }); + connect(aqa, &QAction::triggered, []{ QApplication::aboutQt(); }); +} + +mapman_main_window::~mapman_main_window() +{ + if (l) + delete l; + delete dv; +} diff --git a/mapman/src/mainwindow.hpp b/mapman/src/mainwindow.hpp new file mode 100644 index 0000000..647fcc9 --- /dev/null +++ b/mapman/src/mainwindow.hpp @@ -0,0 +1,26 @@ +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP + +#include + +class map_library; +class group_view; +class slice_view; +class diff_view; +class QMdiArea; + +class mapman_main_window : public QMainWindow +{ + Q_OBJECT +public: + mapman_main_window(); + ~mapman_main_window(); +private: + map_library *l; + group_view *gv; + slice_view *sv; + diff_view *dv; + QMdiArea *cw; +}; + +#endif diff --git a/mapman/src/mapdump.cpp b/mapman/src/mapdump.cpp new file mode 100644 index 0000000..6d8fbcc --- /dev/null +++ b/mapman/src/mapdump.cpp @@ -0,0 +1,52 @@ +#include "mapdump.hpp" + +#include + +bool load_dump(gzFile f, map_t &d) +{ + map_t ret; + if (gzread(f, &d.id, 4) < 4) return false; + int name_len; + if (gzread(f, &name_len, 4) < 4) return false; + if (name_len) + { + char *name = new char[name_len]; + if (gzread(f, name, name_len) != name_len) + { + delete[] name; + return false; + } + d.custom_name = std::string(name); + delete[] name; + } else d.custom_name = std::string(); + if (gzread(f, d.map_data.data(), 128 * 128) < 128 * 128) + return false; + return true; +} + +bool load_dumps(const char *fn, std::vector &dumps) +{ + gzFile f = gzopen(fn, "rb"); + dumps.clear(); + while (!gzeof(f)) + { + map_t d; + if (load_dump(f, d)) + dumps.emplace_back(std::move(d)); + } + gzclose(f); + return dumps.size() != 0; +} + +std::vector load_tally(const char *fn) +{ + std::vector ret; + gzFile f = gzopen(fn, "rb"); + while (!gzeof(f)) + { + int t; + gzread(f, &t, 4); + ret.push_back(t); + } + return ret; +} diff --git a/mapman/src/mapdump.hpp b/mapman/src/mapdump.hpp new file mode 100644 index 0000000..aa771e6 --- /dev/null +++ b/mapman/src/mapdump.hpp @@ -0,0 +1,31 @@ +#ifndef MAPDUMP_HPP +#define MAPDUMP_HPP + +#include +#include +#include +#include + +#include "utils.hpp" + +struct map_t +{ + int id; + std::string custom_name; + map_data_t map_data; +}; + +struct map_group_t +{ + std::string title; + std::string author; + int hc; + int vc; + std::vector ids; + std::vector populated; +}; + +bool load_dumps(const char *fn, std::vector &dumps); +std::vector load_tally(const char *fn); + +#endif diff --git a/mapman/src/painter.cpp b/mapman/src/painter.cpp new file mode 100644 index 0000000..876184e --- /dev/null +++ b/mapman/src/painter.cpp @@ -0,0 +1,113 @@ +#include "painter.hpp" +#include "library.hpp" +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +class drop_rect : public QGraphicsRectItem +{ +public: + drop_rect(int pos, map_painter *painter); +signals: + void dropped(int pos, int id); +protected: + void dragEnterEvent(QGraphicsSceneDragDropEvent *e); + void dropEvent(QGraphicsSceneDragDropEvent *e); +private: + int p; + map_painter *pt; +}; + +drop_rect::drop_rect(int pos, map_painter *painter) : + QGraphicsRectItem(QRectF(0, 0, 128, 128), nullptr), + p(pos), + pt(painter) +{ + setAcceptDrops(true); + setPen(QPen(Qt::GlobalColor::black, 4)); +} + +void drop_rect::dragEnterEvent(QGraphicsSceneDragDropEvent *e) +{ + auto ba = e->mimeData()->data("application/x-map-id"); + e->setAccepted(ba.length() >= 4); +} + +void drop_rect::dropEvent(QGraphicsSceneDragDropEvent *e) +{ + auto ba = e->mimeData()->data("application/x-map-id"); + if (ba.length() >= 4) + { + e->setDropAction(Qt::DropAction::CopyAction); + pt->set_map_id(p, true, *reinterpret_cast(ba.data()), true); + } + else e->setDropAction(Qt::DropAction::IgnoreAction); +} + +map_painter::map_painter() : l(nullptr) +{ + s = new QGraphicsScene(0, 0, 0, 0); + v = new QGraphicsView(s); + hc = vc = 0; +} + +map_painter::~map_painter() +{ + delete s; + delete v; +} + +void map_painter::set_dimension(int h, int v) +{ + hc = h; + vc = v; + s->setSceneRect(0, 0, hc * 128, vc * 128); + s->clear(); + slices.clear(); + slices.resize(hc * vc, nullptr); + for (int i = 0; i < vc; ++i) + for (int j = 0; j < hc; ++j) + { + auto t = s->addSimpleText(QString::number(i * hc + j)); + t->setPos(j * 128 + 64 - t->boundingRect().width() / 2, i * 128 + 64 - t->boundingRect().height() / 2); + auto r = new drop_rect(i * hc + j, this); + s->addItem(r); + r->setPos(j * 128, i * 128); + } +} + +void map_painter::set_map_id(int pos, bool populated, int id, bool user_input) +{ + if (!l || pos >= hc * vc || pos < 0) return; + if (slices[pos]) + { + s->removeItem(slices[pos]); + delete slices[pos]; + slices[pos] = nullptr; + } + if (populated) + { + QPixmap pm; + if (!QPixmapCache::find(QString("map_%1").arg(id), &pm)) + { + pm = pixmap_of_map_data(l->get_map(id).map_data); + QPixmapCache::insert(QString("map_%1").arg(id), pm); + } + auto p = s->addPixmap(pm); + int x = pos / hc; + int y = pos % hc; + p->setPos(y * 128, x * 128); + slices[pos] = p; + } + if (user_input) + emit map_id_changed(pos, populated, id); +} + +void map_painter::set_map_library(map_library *lib) { l = lib; } diff --git a/mapman/src/painter.hpp b/mapman/src/painter.hpp new file mode 100644 index 0000000..489a87b --- /dev/null +++ b/mapman/src/painter.hpp @@ -0,0 +1,34 @@ +#ifndef MAPPAINTER_HPP +#define MAPPAINTER_HPP + +#include +#include + +class QGraphicsScene; +class QGraphicsView; +class QGraphicsItem; +class map_library; + +class map_painter : public QObject +{ + Q_OBJECT +public: + map_painter(); + ~map_painter(); + void set_dimension(int h, int v); + void set_map_library(map_library *lib); + void set_map_id(int pos, bool populated, int id, bool user_input = false); + + QGraphicsView* view() { return v; } +signals: + void map_id_changed(int pos, bool populated, int id); +private: + QGraphicsView *v; + QGraphicsScene *s; + map_library *l; + int hc; + int vc; + std::vector slices; +}; + +#endif diff --git a/mapman/src/sliceview.cpp b/mapman/src/sliceview.cpp new file mode 100644 index 0000000..1dc385b --- /dev/null +++ b/mapman/src/sliceview.cpp @@ -0,0 +1,101 @@ +#include "sliceview.hpp" +#include "library.hpp" +#include "painter.hpp" +#include "utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +slice_view::slice_view() : l(nullptr) +{ + lv = new QListView(this); + m = new QStandardItemModel(this); + lv->setModel(m); + lv->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + lv->setDragDropMode(QAbstractItemView::DragDropMode::NoDragDrop); + lv->viewport()->installEventFilter(this); + p = new map_painter(); + p->set_dimension(1, 1); + auto layout = new QSplitter(Qt::Orientation::Horizontal, this); + layout->setContentsMargins(6, 6, 6, 6); + layout->addWidget(lv); + layout->addWidget(p->view()); + layout->setStretchFactor(0, 1); + layout->setStretchFactor(1, 3); + layout->setCollapsible(0, false); + layout->setCollapsible(1, false); + this->setWidget(layout); + connect(lv->selectionModel(), &QItemSelectionModel::currentChanged, + [this](const QModelIndex &cur, const QModelIndex&) { + if (this->l) + this->p->set_map_id(0, true, cur.data(Qt::UserRole + 1).toInt()); + }); + connect(lv, &QAbstractItemView::pressed, + [this](const QModelIndex &idx) { + dragidx = idx; + }); + this->setWindowTitle("Map listings"); + this->setAttribute(Qt::WA_DeleteOnClose, false); +} + +slice_view::~slice_view() +{ + delete p; +} + +void slice_view::set_library(map_library *lib) +{ + l = lib; + p->set_map_library(l); + refresh(); +} + +bool slice_view::eventFilter(QObject *o, QEvent *e) +{ + if (e->type() == QEvent::MouseButtonRelease) + dragidx = QModelIndex(); + if (e->type() == QEvent::MouseButtonPress) + dragpos = static_cast(e)->screenPos(); + if (e->type() == QEvent::MouseMove) + { + auto pos = static_cast(e)->screenPos(); + if (dragidx.isValid() && (pos - dragpos).manhattanLength() >= QApplication::startDragDistance()) + { + auto *d = new QDrag(lv); + int mapid = dragidx.data(Qt::ItemDataRole::UserRole + 1).toInt(); + d->setPixmap(qvariant_cast(dragidx.data(Qt::ItemDataRole::DecorationRole)).pixmap(128, 128)); + auto *m = new QMimeData(); + m->setData("application/x-map-id", QByteArray(reinterpret_cast(&mapid), 4)); + d->setMimeData(m); + d->exec(Qt::DropAction::CopyAction); + } + } + return false; +} + +void slice_view::refresh() +{ + int curid = lv->currentIndex().data().toInt(); + m->clear(); + auto ids = l->map_ids(); + for (auto id : ids) + { + map_t map = l->get_map(id); + QPixmap pm = pixmap_of_map_data(map.map_data); + QString text = QString("(%1)").arg(id); + if (map.custom_name.length()) text = QString::fromStdString(map.custom_name) + " " + text; + QStandardItem *itm = new QStandardItem(QIcon(pm), text); + itm->setData(QVariant(id)); + m->appendRow(itm); + if (id == curid) + lv->setCurrentIndex(itm->index()); + } +} diff --git a/mapman/src/sliceview.hpp b/mapman/src/sliceview.hpp new file mode 100644 index 0000000..3f7ee79 --- /dev/null +++ b/mapman/src/sliceview.hpp @@ -0,0 +1,32 @@ +#ifndef SLICEVIEW_HPP +#define SLICEVIEW_HPP + +#include +#include + +class map_library; +class map_painter; +class QListView; +class QStandardItemModel; + +class slice_view : public QMdiSubWindow +{ + Q_OBJECT +public: + slice_view(); + ~slice_view(); + void set_library(map_library *lib); +public slots: + void refresh(); +protected: + bool eventFilter(QObject *o, QEvent *e); +private: + QListView *lv; + QStandardItemModel *m; + map_painter *p; + map_library *l; + QModelIndex dragidx; + QPointF dragpos; +}; + +#endif diff --git a/mapman/src/utils.cpp b/mapman/src/utils.cpp new file mode 100644 index 0000000..f01d946 --- /dev/null +++ b/mapman/src/utils.cpp @@ -0,0 +1,40 @@ +#include "utils.hpp" + +#include +#include + +rgb_t modify_color(rgb_t c, uint8_t variant) +{ + int m; + switch (variant) + { + case 0: m = 180; break; + case 1: m = 200; break; + case 2: m = 255; break; + case 3: m = 135; break; + default: m = 0; break; + } + return {uint8_t(m * c.r / 255), + uint8_t(m * c.g / 255), + uint8_t(m * c.b / 255)}; +} + +QColor rgb2qcolor(rgb_t c) {return QColor(c.r, c.g, c.b);} + +QPixmap pixmap_of_map_data(const std::array &map_data) +{ + QImage ret(128, 128, QImage::Format_ARGB32); + for (size_t i = 0; i < 128; ++i) + for (size_t j = 0; j < 128; ++j) + { + uint8_t d = map_data[j * 128 + i]; + uint8_t b = d >> 2; + uint8_t v = d & 3; + QColor color = Qt::GlobalColor::transparent; + if (b > 0 && b < 62) + color = rgb2qcolor(modify_color(MAP_COLORS[b], v)); + ret.setPixelColor(i, j, color); + } + return QPixmap::fromImage(ret); +} + diff --git a/mapman/src/utils.hpp b/mapman/src/utils.hpp new file mode 100644 index 0000000..47df676 --- /dev/null +++ b/mapman/src/utils.hpp @@ -0,0 +1,82 @@ +#ifndef UTILS_HPP +#define UTILS_HPP + +#include +#include + +#include + +struct rgb_t +{ + uint8_t r,g,b; +}; + +typedef std::array map_data_t; + +const rgb_t MAP_COLORS[62] = { + {0, 0, 0}, + {127, 178, 56}, + {247, 233, 163}, + {199, 199, 199}, + {255, 0, 0}, + {160, 160, 255}, + {167, 167, 167}, + {0, 124, 0}, + {255, 255, 255}, + {164, 168, 184}, + {151, 109, 77}, + {112, 112, 112}, + {64, 64, 255}, + {143, 119, 72}, + {255, 252, 245}, + {216, 127, 51}, + {178, 76, 216}, + {102, 153, 216}, + {229, 229, 51}, + {127, 204, 25}, + {242, 127, 165}, + {76, 76, 76}, + {153, 153, 153}, + {76, 127, 153}, + {127, 63, 178}, + {51, 76, 178}, + {102, 76, 51}, + {102, 127, 51}, + {153, 51, 51}, + {25, 25, 25}, + {250, 238, 77}, + {92, 219, 213}, + {74, 128, 255}, + {0, 217, 58}, + {129, 86, 49}, + {112, 2, 0}, + {209, 177, 161}, + {159, 82, 36}, + {149, 87, 108}, + {112, 108, 138}, + {186, 133, 36}, + {103, 117, 53}, + {160, 77, 78}, + {57, 41, 35}, + {135, 107, 98}, + {87, 92, 92}, + {122, 73, 88}, + {76, 62, 92}, + {76, 50, 35}, + {76, 82, 42}, + {142, 60, 46}, + {37, 22, 16}, + {189, 48, 49}, + {148, 63, 97}, + {92, 25, 29}, + {22, 126, 134}, + {58, 142, 140}, + {86, 44, 62}, + {20, 180, 133}, + {100, 100, 100}, + {216, 175, 147}, + {127, 167, 150} +}; + +QPixmap pixmap_of_map_data(const map_data_t &map_data); +#endif -- cgit v1.2.3