porymap/src/core/regionmap.cpp

736 lines
25 KiB
C++
Raw Normal View History

#include "regionmap.h"
#include "regionmapeditor.h"
#include "paletteutil.h"
#include "project.h"
2019-01-14 00:27:28 +00:00
#include "log.h"
#include "config.h"
#include "regionmapeditcommands.h"
#include <QByteArray>
#include <QFile>
#include <QDebug>
#include <QRegularExpression>
#include <QImage>
2019-01-28 18:47:20 +00:00
#include <math.h>
using std::make_shared;
2022-04-29 01:52:31 +01:00
RegionMap::RegionMap(Project *project) {
this->project = project;
}
bool RegionMap::loadMapData(poryjson::Json data) {
poryjson::Json::object mapObject = data.object_items();
2019-01-14 00:27:28 +00:00
this->alias = mapObject["alias"].string_value();
poryjson::Json tilemapJson = mapObject["tilemap"];
poryjson::Json layoutJson = mapObject["layout"];
this->tilemap.clear();
this->layout_layers.clear();
this->layouts.clear();
2022-04-29 01:52:31 +01:00
return loadTilemap(tilemapJson) && loadLayout(layoutJson);
}
int RegionMap::tilemapBytes() {
// bytes per tile multiplier
int multiplier = 1;
switch (tilemap_format) {
case TilemapFormat::Plain:
multiplier = 1;
break;
case TilemapFormat::BPP_4:
multiplier = 2;
break;
case TilemapFormat::BPP_8:
multiplier = 2;
break;
2019-03-25 04:10:57 +00:00
}
return tilemapSize() * multiplier;
2019-03-25 04:10:57 +00:00
}
bool RegionMap::loadTilemap(poryjson::Json tilemapJson) {
bool errored = false;
poryjson::Json::object tilemapObject = tilemapJson.object_items();
this->tilemap_width = tilemapObject["width"].int_value();
this->tilemap_height = tilemapObject["height"].int_value();
QString tilemapFormat = tilemapObject["format"].string_value();
QMap<QString, TilemapFormat> formatsMap = { {"plain", TilemapFormat::Plain}, {"4bpp", TilemapFormat::BPP_4}, {"8bpp", TilemapFormat::BPP_8} };
this->tilemap_format = formatsMap[tilemapFormat];
this->tileset_path = tilemapObject["tileset_path"].string_value();
this->tilemap_path = tilemapObject["tilemap_path"].string_value();
if (tilemapObject.contains("palette")) {
this->palette_path = tilemapObject["palette"].string_value();
}
QFile tilemapFile(fullPath(this->tilemap_path));
if (!tilemapFile.open(QIODevice::ReadOnly)) {
logError(QString("Failed to open region map tilemap file %1.").arg(tilemap_path));
return false;
}
2019-01-05 04:04:14 +00:00
if (tilemapFile.size() < tilemapBytes()) {
logError(QString("The region map tilemap at %1 is too small.").arg(tilemap_path));
return false;
}
QByteArray newTilemap = tilemapFile.readAll();
this->setTilemap(newTilemap);
tilemapFile.close();
return !errored;
}
bool RegionMap::loadLayout(poryjson::Json layoutJson) {
if (layoutJson.is_null()) {
this->layout_format = LayoutFormat::None;
return true;
}
2019-01-05 04:04:14 +00:00
this->layout_constants.clear();
poryjson::Json::object layoutObject = layoutJson.object_items();
2019-01-05 04:04:14 +00:00
QString layoutFormat = layoutObject["format"].string_value();
QMap<QString, LayoutFormat> layoutFormatMap = { {"binary", LayoutFormat::Binary}, {"C array", LayoutFormat::CArray} };
this->layout_format = layoutFormatMap[layoutFormat];
this->layout_path = layoutObject["path"].string_value();
this->layout_width = layoutObject["width"].int_value();
this->layout_height = layoutObject["height"].int_value();
this->offset_left = layoutObject["offset_left"].int_value();
this->offset_top = layoutObject["offset_top"].int_value();
2019-04-13 04:07:00 +01:00
bool errored = false;
switch (this->layout_format) {
case LayoutFormat::Binary:
{
2022-04-29 02:42:47 +01:00
// only one layer supported for binary layouts
QFile binFile(fullPath(this->layout_path));
if (!binFile.open(QIODevice::ReadOnly)) {
logError(QString("Failed to read region map layout binary file %1").arg(this->layout_path));
return false;
}
QByteArray mapBinData = binFile.readAll();
binFile.close();
2019-01-05 04:04:14 +00:00
if (mapBinData.size() != this->layout_width * this->layout_height) {
logError("Region map layout file size does not match given dimensions for " + this->alias);
return false;
}
// for layouts with only a single layer, it is called main
this->layout_layers.append("main");
QList<LayoutSquare> layout;
for (int y = 0; y < this->layout_height; y++) {
for (int x = 0; x < this->layout_width; x++) {
int bin_index = x + y * this->layout_width;
uint8_t square_section_id = mapBinData.at(bin_index);
QString square_section_name = project->mapSectionValueToName.value(square_section_id);
LayoutSquare square;
square.map_section = square_section_name;
square.has_map = (square_section_name != "MAPSEC_NONE" && !square_section_name.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
}
}
setLayout("main", layout);
break;
2019-03-25 04:10:57 +00:00
}
case LayoutFormat::CArray:
{
ParseUtil parser;
QString text = parser.readTextFile(fullPath(this->layout_path));
QRegularExpression re("(?<qual_1>static)?\\s?(?<qual_2>const)?\\s?(?<type>[A-Za-z0-9_]+)?\\s+(?<label>[A-Za-z0-9_]+)"
"(\\[(?<const_1>[A-Za-z0-9_]+)\\])?(\\[(?<const_2>[A-Za-z0-9_]+)\\])?(\\[(?<const_3>[A-Za-z0-9_]+)\\])?\\s+=");
// check for layers, extract info
QRegularExpressionMatch match = re.match(text);
if (match.hasMatch()) {
QString qualifiers = match.captured("qual_1") + " " + match.captured("qual_2");
QString type = match.captured("type");
QString label = match.captured("label");
QStringList constants;
if (!match.captured("const_1").isNull()) constants.append(match.captured("const_1"));
if (!match.captured("const_2").isNull()) constants.append(match.captured("const_2"));
if (!match.captured("const_3").isNull()) constants.append(match.captured("const_3"));
this->layout_constants = constants;
this->layout_qualifiers = qualifiers + " " + type;
this->layout_array_label = label;
// find layers
QRegularExpression reLayers("(?<layer>\\[(?<label>LAYER_[A-Za-z0-9_]+)\\][^\\[\\]]+)");
QRegularExpressionMatchIterator i = reLayers.globalMatch(text);
while (i.hasNext()) {
QRegularExpressionMatch m = i.next();
QString layerName = m.captured("label");
QString layerLayout = m.captured("layer");
QRegularExpression rowRe("{(?<row>[A-Z0-9_, ]+)}");
QRegularExpressionMatchIterator j = rowRe.globalMatch(layerLayout);
this->layout_layers.append(layerName);
QList<LayoutSquare> layout;
int y = 0;
while (j.hasNext()) {
QRegularExpressionMatch n = j.next();
QString row = n.captured("row");
QStringList rowSections = row.split(',');
int x = 0;
for (QString section : rowSections) {
QString square_section_name = section.trimmed();
LayoutSquare square;
square.map_section = square_section_name;
square.has_map = (square_section_name != "MAPSEC_NONE" && !square_section_name.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
x++;
}
y++;
}
setLayout(layerName, layout);
}
} else {
2022-04-29 01:52:31 +01:00
// try single-layered
QRegularExpression reAlt("(?<qual_1>static)?\\s?(?<qual_2>const)?\\s?(?<type>[A-Za-z0-9_]+)?\\s+(?<label>[A-Za-z0-9_]+)\\[\\]");
QRegularExpressionMatch matchAlt = reAlt.match(text);
if (matchAlt.hasMatch()) {
QString qualifiers = matchAlt.captured("qual_1") + " " + matchAlt.captured("qual_2");
QString type = matchAlt.captured("type");
QString label = matchAlt.captured("label");
this->layout_constants.append("");
this->layout_qualifiers = qualifiers + " " + type;
this->layout_array_label = label;
QRegularExpression reSec("(?<sec>MAPSEC_[A-Za-z0-9_]+)");
QRegularExpressionMatchIterator k = reSec.globalMatch(text);
QList<LayoutSquare> layout;
int l = 0;
while (k.hasNext()) {
QRegularExpressionMatch p = k.next();
QString sec = p.captured("sec");
int x = l % this->layout_width;
int y = l / this->layout_width;
LayoutSquare square;
square.map_section = sec;
square.has_map = (sec != "MAPSEC_NONE" && !sec.isEmpty());
square.x = x;
square.y = y;
layout.append(square);
l++;
}
this->layout_layers.append("main");
setLayout("main", layout);
} else {
logError("Region map layout is not readable.");
return false;
}
2019-01-14 00:27:28 +00:00
}
break;
2019-01-14 00:27:28 +00:00
}
2022-04-29 02:42:47 +01:00
case LayoutFormat::None:
default:
break;
2019-01-05 18:02:14 +00:00
}
this->current_layer = this->layout_layers.first();
return !errored;
}
void RegionMap::commit(QUndoCommand *command) {
editHistory.push(command);
}
void RegionMap::undo() {
editHistory.undo();
}
void RegionMap::redo() {
editHistory.redo();
}
void RegionMap::save() {
saveTilemap();
saveLayout();
2022-04-29 01:52:31 +01:00
this->editHistory.setClean();
}
2022-04-28 18:21:36 +01:00
poryjson::Json::object RegionMap::config() {
poryjson::Json::object config;
config["alias"] = this->alias;
poryjson::Json::object tilemapObject;
tilemapObject["width"] = this->tilemap_width;
tilemapObject["height"] = this->tilemap_height;
QMap<TilemapFormat, QString> tilemapFormatMap = { {TilemapFormat::Plain, "plain"}, {TilemapFormat::BPP_4, "4bpp"}, {TilemapFormat::BPP_8, "8bpp"} };
tilemapObject["format"] = tilemapFormatMap[this->tilemap_format];
tilemapObject["tileset_path"] = this->tileset_path;
tilemapObject["tilemap_path"] = this->tilemap_path;
if (!this->palette_path.isEmpty()) {
tilemapObject["palette"] = this->palette_path;
}
config["tilemap"] = tilemapObject;
if (this->layout_format != LayoutFormat::None) {
poryjson::Json::object layoutObject;
layoutObject["width"] = this->layout_width;
layoutObject["height"] = this->layout_height;
layoutObject["offset_left"] = this->offset_left;
layoutObject["offset_top"] = this->offset_top;
QMap<LayoutFormat, QString> layoutFormatMap = { {LayoutFormat::Binary, "binary"}, {LayoutFormat::CArray, "C array"} };
layoutObject["format"] = layoutFormatMap[this->layout_format];
layoutObject["path"] = this->layout_path;
config["layout"] = layoutObject;
} else {
config["layout"] = nullptr;
}
return config;
}
void RegionMap::saveTilemap() {
QFile tilemapFile(fullPath(this->tilemap_path));
if (!tilemapFile.open(QIODevice::WriteOnly)) {
logError(QString("Failed to open region map tilemap file %1 for writing.").arg(this->tilemap_path));
return;
}
tilemapFile.write(this->getTilemap());
tilemapFile.close();
}
void RegionMap::saveLayout() {
switch (this->layout_format) {
case LayoutFormat::Binary:
{
QByteArray data;
for (int m = 0; m < this->layout_height; m++) {
for (int n = 0; n < this->layout_width; n++) {
int i = n + this->layout_width * m;
data.append(this->project->mapSectionNameToValue.value(this->layouts["main"][i].map_section));
}
}
QFile bfile(fullPath(this->layout_path));
if (!bfile.open(QIODevice::WriteOnly)) {
logError("Failed to open region map layout binary for writing.");
}
bfile.write(data);
bfile.close();
break;
}
case LayoutFormat::CArray:
{
QString text = QString("%1 %2").arg(this->layout_qualifiers).arg(this->layout_array_label);
for (QString label : this->layout_constants) {
text += QString("[%1]").arg(label);
}
text += " = {\n";
if (this->layout_layers.size() == 1) {
2022-04-29 01:52:31 +01:00
for (LayoutSquare s : this->getLayout("main")) {
text += " " + s.map_section + ",\n";
}
text.chop(2);
text += "\n";
} else {
// multi layered
for (auto layoutName : this->layout_layers) {
text += QString(" [%1] =\n {\n").arg(layoutName);
for (int row = 0; row < this->layout_height; row++) {
text += " {";
for (int col = 0; col < this->layout_width; col++) {
int i = col + row * this->layout_width;
text += this->layouts[layoutName][i].map_section + ", ";
}
text.chop(2);
text += "},\n";
}
text += " },\n";
}
text.chop(2);
text += "\n";
}
text += "};\n";
this->project->saveTextFile(fullPath(this->layout_path), text);
break;
}
2022-04-29 02:42:47 +01:00
case LayoutFormat::None:
default:
break;
}
}
2019-01-14 00:27:28 +00:00
void RegionMap::resetSquare(int index) {
this->layouts[this->current_layer][index].map_section = "MAPSEC_NONE";
this->layouts[this->current_layer][index].has_map = false;
}
void RegionMap::clearLayout() {
for (int i = 0; i < this->layout_width * this->layout_height; i++) {
resetSquare(i);
}
}
void RegionMap::clearImage() {
QByteArray zeros(this->tilemapSize(), 0);
this->setTilemap(zeros);
2019-01-14 00:27:28 +00:00
}
2022-04-27 21:00:47 +01:00
void RegionMap::replaceSection(QString oldSection, QString newSection) {
for (auto &square : this->layouts[this->current_layer]) {
if (square.map_section == oldSection) {
square.map_section = newSection;
square.has_map = (newSection != "MAPSEC_NONE");
}
}
}
void RegionMap::swapSections(QString secA, QString secB) {
for (auto &square : this->layouts[this->current_layer]) {
if (square.map_section == secA) {
square.map_section = secB;
square.has_map = (square.map_section != "MAPSEC_NONE");
}
else if (square.map_section == secB) {
square.map_section = secA;
square.has_map = (square.map_section != "MAPSEC_NONE");
}
}
}
2022-04-28 18:21:36 +01:00
void RegionMap::resizeTilemap(int newWidth, int newHeight, bool update) {
auto tilemapCopy = this->tilemap;
int oldWidth = this->tilemap_width;
int oldHeight = this->tilemap_height;
this->tilemap_width = newWidth;
this->tilemap_height = newHeight;
if (update) {
QByteArray tilemapArray;
QDataStream dataStream(&tilemapArray, QIODevice::WriteOnly);
dataStream.setByteOrder(QDataStream::LittleEndian);
switch (this->tilemap_format) {
case TilemapFormat::Plain:
for (int y = 0; y < newHeight; y++)
for (int x = 0; x < newWidth; x++) {
if (y < oldHeight && x < oldWidth) {
int i = x + y * oldWidth;
uint8_t tile = tilemapCopy[i]->raw();
dataStream << tile;
} else {
uint8_t tile = 0;
dataStream << tile;
}
}
break;
case TilemapFormat::BPP_4:
case TilemapFormat::BPP_8:
for (int y = 0; y < newHeight; y++)
for (int x = 0; x < newWidth; x++) {
if (y < oldHeight && x < oldWidth) {
int i = x + y * oldWidth;
uint16_t tile = tilemapCopy[i]->raw();
dataStream << tile;
} else {
uint16_t tile = 0;
dataStream << tile;
}
}
break;
}
setTilemap(tilemapArray);
}
}
2022-04-28 18:21:36 +01:00
void RegionMap::emitDisplay() {
emit mapNeedsDisplaying();
2019-01-14 00:27:28 +00:00
}
2019-01-28 18:47:20 +00:00
QByteArray RegionMap::getTilemap() {
QByteArray tilemapArray;
QDataStream dataStream(&tilemapArray, QIODevice::WriteOnly);
dataStream.setByteOrder(QDataStream::LittleEndian);
switch (this->tilemap_format) {
case TilemapFormat::Plain:
for (int i = 0; i < tilemapSize(); i++) {
uint8_t tile = this->tilemap[i]->raw();
dataStream << tile;
}
break;
case TilemapFormat::BPP_4:
for (int i = 0; i < tilemapSize(); i++) {
uint16_t tile = this->tilemap[i]->raw();
dataStream << tile;
}
break;
case TilemapFormat::BPP_8:
for (int i = 0; i < tilemapSize(); i++) {
uint16_t tile = this->tilemap[i]->raw();
dataStream << tile;
}
break;
}
return tilemapArray;
}
void RegionMap::setTilemap(QByteArray newTilemap) {
QDataStream dataStream(newTilemap);
dataStream.setByteOrder(QDataStream::LittleEndian);
this->tilemap.clear();
this->tilemap.resize(tilemapSize());
switch (this->tilemap_format) {
case TilemapFormat::Plain:
for (int i = 0; i < tilemapBytes(); i++) {
uint8_t tile;
dataStream >> tile;
this->tilemap[i] = make_shared<PlainTile>(tile);
}
break;
case TilemapFormat::BPP_4:
for (int i = 0; i < tilemapSize(); i++) {
uint16_t tile;
dataStream >> tile;
this->tilemap[i] = make_shared<BPP4Tile>(tile & 0xffff);
}
break;
case TilemapFormat::BPP_8:
for (int i = 0; i < tilemapSize(); i++) {
uint16_t tile;
dataStream >> tile;
this->tilemap[i] = make_shared<BPP8Tile>(tile & 0xffff);
}
break;
}
}
QList<LayoutSquare> RegionMap::getLayout(QString layer) {
return this->layouts[layer];
}
void RegionMap::setLayout(QString layer, QList<LayoutSquare> layout) {
this->layouts[layer] = layout;
}
QMap<QString, QList<LayoutSquare>> RegionMap::getAllLayouts() {
return this->layouts;
}
void RegionMap::setAllLayouts(QMap<QString, QList<LayoutSquare>> newLayouts) {
this->layouts = newLayouts;
}
void RegionMap::setLayoutDimensions(int width, int height, bool update) {
2022-04-29 02:42:47 +01:00
// for each layer
if (update) {
for (QString layer : this->layout_layers) {
QList<LayoutSquare> oldLayout = this->getLayout(layer);
QList<LayoutSquare> newLayout;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) {
LayoutSquare newSquare;
2022-05-06 02:40:13 +01:00
newSquare.x = x;
newSquare.y = y;
if (x < this->layout_width && y < this->layout_height) {
// within old layout
int oldIndex = this->get_layout_index(x, y);
newSquare = oldLayout[oldIndex];
}
newLayout.append(newSquare);
}
this->setLayout(layer, newLayout);
}
}
this->layout_width = width;
this->layout_height = height;
2019-01-28 18:47:20 +00:00
}
// Layout coords to image index.
int RegionMap::get_tilemap_index(int x, int y) {
return ((x + this->offset_left) + (y + this->offset_top) * this->tilemap_width);
2019-01-28 18:47:20 +00:00
}
// Layout coords to layout index.
int RegionMap::get_layout_index(int x, int y) {
return x + y * this->layout_width;
2019-01-28 18:47:20 +00:00
}
unsigned RegionMap::getTileId(int index) {
if (index < tilemap.size()) {
return tilemap[index]->id();
}
2019-01-28 18:47:20 +00:00
return 0;
2019-01-28 18:47:20 +00:00
}
shared_ptr<TilemapTile> RegionMap::getTile(int index) {
if (index < tilemap.size()) {
return tilemap[index];
}
return nullptr;
2019-01-28 18:47:20 +00:00
}
unsigned RegionMap::getTileId(int x, int y) {
int index = x + y * tilemap_width;
return getTileId(index);
2019-01-28 18:47:20 +00:00
}
shared_ptr<TilemapTile> RegionMap::getTile(int x, int y) {
int index = x + y * tilemap_width;
return getTile(index);
2019-01-28 18:47:20 +00:00
}
void RegionMap::setTileId(int index, unsigned id) {
if (index < tilemap.size()) {
tilemap[index]->setId(id);
}
2019-03-25 04:10:57 +00:00
}
void RegionMap::setTile(int index, TilemapTile &tile) {
if (index < tilemap.size()) {
tilemap[index]->copy(tile);
}
2019-01-28 18:47:20 +00:00
}
void RegionMap::setTileData(int index, unsigned id, bool hFlip, bool vFlip, int palette) {
if (index < tilemap.size()) {
tilemap[index]->setId(id);
tilemap[index]->setHFlip(hFlip);
tilemap[index]->setVFlip(vFlip);
tilemap[index]->setPalette(palette);
}
}
int RegionMap::tilemapToLayoutIndex(int index) {
int x = index % this->tilemap_width;
if (x < this->offset_left) return -1;
int y = index / this->tilemap_width;
if (y < this->offset_top) return -1;
int layoutX = x - this->offset_left;
if (layoutX >= this->layout_width) return -1;
int layoutY = y - this->offset_top;
if (layoutY >= this->layout_height) return -1;
return layoutX + layoutY * this->layout_width;
}
bool RegionMap::squareHasMap(int index) {
int layoutIndex = tilemapToLayoutIndex(index);
return (layoutIndex < 0 || !this->layouts.contains(this->current_layer)) ? false : this->layouts[this->current_layer][layoutIndex].has_map;
}
QString RegionMap::squareMapSection(int index) {
int layoutIndex = tilemapToLayoutIndex(index);
return (layoutIndex < 0 || !this->layouts.contains(this->current_layer)) ? QString() : this->layouts[this->current_layer][layoutIndex].map_section;
}
void RegionMap::setSquareMapSection(int index, QString section) {
int layoutIndex = tilemapToLayoutIndex(index);
if (!(layoutIndex < 0 || !this->layouts.contains(this->current_layer))) {
this->layouts[this->current_layer][layoutIndex].map_section = section;
this->layouts[this->current_layer][layoutIndex].has_map = !(section == "MAPSEC_NONE" || section.isEmpty());
}
}
int RegionMap::squareX(int index) {
int layoutIndex = tilemapToLayoutIndex(index);
return (layoutIndex < 0 || !this->layouts.contains(this->current_layer)) ? -1 : this->layouts[this->current_layer][layoutIndex].x;
}
int RegionMap::squareY(int index) {
int layoutIndex = tilemapToLayoutIndex(index);
return (layoutIndex < 0 || !this->layouts.contains(this->current_layer)) ? -1 : this->layouts[this->current_layer][layoutIndex].y;
}
bool RegionMap::squareInLayout(int x, int y) {
return !((x < this->offset_left) || (x >= (this->offset_left + this->layout_width))
|| (y < this->offset_top) || (y >= (this->offset_top + this->layout_height)));
}
MapSectionEntry RegionMap::getEntry(QString section) {
if (this->region_map_entries->contains(section))
return this->region_map_entries->operator[](section);
else
return MapSectionEntry();
}
void RegionMap::setEntry(QString section, MapSectionEntry entry) {
this->region_map_entries->operator[](section) = entry;
}
void RegionMap::removeEntry(QString section) {
this->region_map_entries->erase(section);
}
QString RegionMap::palPath() {
return this->palette_path.isEmpty() ? QString() : this->project->root + "/" + this->palette_path;
}
QString RegionMap::pngPath() {
return this->project->root + "/" + this->tileset_path;
}
2019-01-28 18:47:20 +00:00
// From x, y of image.
int RegionMap::getMapSquareIndex(int x, int y) {
int index = (x + y * this->tilemap_width);
return ((index < tilemap.length()) && (index >= 0)) ? index : 0;
2019-01-28 18:47:20 +00:00
}
// For turning a MAPSEC_NAME into a unique identifier sMapName-style variable.
// CAPS_WITH_UNDERSCORE to CamelCase
2019-04-13 04:07:00 +01:00
QString RegionMap::fixCase(QString caps) {
2019-01-28 18:47:20 +00:00
bool big = true;
QString camel;
for (auto ch : caps.remove(QRegularExpression("({.*})")).remove("MAPSEC")) {
if (ch == '_' || ch == ' ') {
big = true;
continue;
}
if (big) {
camel += ch.toUpper();
big = false;
}
else camel += ch.toLower();
}
return camel;
}
2019-04-13 02:25:43 +01:00
QString RegionMap::fullPath(QString local) {
return this->project->root + "/" + local;
2019-04-13 02:25:43 +01:00
}