porymap/src/core/tileset.cpp

368 lines
16 KiB
C++
Raw Normal View History

2018-09-25 01:12:29 +01:00
#include "tileset.h"
#include "metatile.h"
#include "project.h"
2019-03-21 23:50:50 +00:00
#include "log.h"
#include "config.h"
#include <QPainter>
#include <QImage>
2018-10-03 01:01:15 +01:00
Tileset::Tileset(const Tileset &other)
: name(other.name),
is_secondary(other.is_secondary),
tiles_label(other.tiles_label),
palettes_label(other.palettes_label),
metatiles_label(other.metatiles_label),
metatiles_path(other.metatiles_path),
metatile_attrs_label(other.metatile_attrs_label),
metatile_attrs_path(other.metatile_attrs_path),
tilesImagePath(other.tilesImagePath),
2022-10-28 03:11:10 +01:00
tilesImage(other.tilesImage.copy()),
palettePaths(other.palettePaths),
2023-02-14 19:10:05 +00:00
metatileLabels(other.metatileLabels),
palettes(other.palettes),
2023-06-16 12:39:32 +01:00
palettePreviews(other.palettePreviews),
hasUnsavedTilesImage(false)
{
2022-10-28 03:11:10 +01:00
for (auto tile : other.tiles) {
tiles.append(tile.copy());
}
for (auto *metatile : other.metatiles) {
metatiles.append(new Metatile(*metatile));
}
}
Tileset &Tileset::operator=(const Tileset &other) {
name = other.name;
is_secondary = other.is_secondary;
tiles_label = other.tiles_label;
palettes_label = other.palettes_label;
metatiles_label = other.metatiles_label;
metatiles_path = other.metatiles_path;
metatile_attrs_label = other.metatile_attrs_label;
metatile_attrs_path = other.metatile_attrs_path;
tilesImagePath = other.tilesImagePath;
2022-10-28 03:11:10 +01:00
tilesImage = other.tilesImage.copy();
palettePaths = other.palettePaths;
2023-02-14 19:10:05 +00:00
metatileLabels = other.metatileLabels;
palettes = other.palettes;
palettePreviews = other.palettePreviews;
2022-10-28 03:11:10 +01:00
tiles.clear();
for (auto tile : other.tiles) {
tiles.append(tile.copy());
}
metatiles.clear();
for (auto *metatile : other.metatiles) {
metatiles.append(new Metatile(*metatile));
}
return *this;
}
Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
if (tileId < Project::getNumTilesPrimary()) {
return primaryTileset;
} else if (tileId < Project::getNumTilesTotal()) {
return secondaryTileset;
} else {
return nullptr;
}
}
Tileset* Tileset::getMetatileTileset(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
if (metatileId < Project::getNumMetatilesPrimary()) {
return primaryTileset;
} else if (metatileId < Project::getNumMetatilesTotal()) {
return secondaryTileset;
} else {
return nullptr;
}
}
Metatile* Tileset::getMetatile(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset);
2021-02-17 02:45:54 +00:00
if (!tileset) {
return nullptr;
}
2023-02-14 19:10:05 +00:00
int index = Metatile::getIndexInTileset(metatileId);
return tileset->metatiles.value(index, nullptr);
}
2023-02-14 19:10:05 +00:00
// Metatile labels are stored per-tileset. When looking for a metatile label, first search in the tileset
// that the metatile belongs to. If one isn't found, search in the other tileset. Labels coming from the
2023-02-14 20:28:18 +00:00
// tileset that the metatile does not belong to are shared and cannot be edited via Porymap.
Tileset* Tileset::getMetatileLabelTileset(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
2023-02-14 19:10:05 +00:00
Tileset *mainTileset = nullptr;
2023-02-14 20:28:18 +00:00
Tileset *alternateTileset = nullptr;
2023-02-14 19:10:05 +00:00
if (metatileId < Project::getNumMetatilesPrimary()) {
mainTileset = primaryTileset;
2023-02-14 20:28:18 +00:00
alternateTileset = secondaryTileset;
2023-02-14 19:10:05 +00:00
} else if (metatileId < Project::getNumMetatilesTotal()) {
mainTileset = secondaryTileset;
2023-02-14 20:28:18 +00:00
alternateTileset = primaryTileset;
2023-02-14 19:10:05 +00:00
}
if (mainTileset && !mainTileset->metatileLabels.value(metatileId).isEmpty()) {
2023-02-14 20:28:18 +00:00
return mainTileset;
} else if (alternateTileset && !alternateTileset->metatileLabels.value(metatileId).isEmpty()) {
2023-02-14 20:28:18 +00:00
return alternateTileset;
2023-02-14 19:10:05 +00:00
}
2023-02-14 20:28:18 +00:00
return nullptr;
}
// Return the pair of possible metatile labels for the specified metatile.
// "owned" is the label for the tileset to which the metatile belongs.
// "shared" is the label for the tileset to which the metatile does not belong.
MetatileLabelPair Tileset::getMetatileLabelPair(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
MetatileLabelPair labels;
QString primaryMetatileLabel = primaryTileset ? primaryTileset->metatileLabels.value(metatileId) : "";
QString secondaryMetatileLabel = secondaryTileset ? secondaryTileset->metatileLabels.value(metatileId) : "";
if (metatileId < Project::getNumMetatilesPrimary()) {
labels.owned = primaryMetatileLabel;
labels.shared = secondaryMetatileLabel;
} else if (metatileId < Project::getNumMetatilesTotal()) {
labels.owned = secondaryMetatileLabel;
labels.shared = primaryMetatileLabel;
}
return labels;
}
// If the metatile has a label in the tileset it belongs to, return that label.
// If it doesn't, and the metatile has a label in the other tileset, return that label.
// Otherwise return an empty string.
QString Tileset::getMetatileLabel(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
MetatileLabelPair labels = Tileset::getMetatileLabelPair(metatileId, primaryTileset, secondaryTileset);
return !labels.owned.isEmpty() ? labels.owned : labels.shared;
}
// Just get the "owned" metatile label, i.e. the one for the tileset that the metatile belongs to.
QString Tileset::getOwnedMetatileLabel(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
MetatileLabelPair labels = Tileset::getMetatileLabelPair(metatileId, primaryTileset, secondaryTileset);
return labels.owned;
}
2023-02-14 19:10:05 +00:00
bool Tileset::setMetatileLabel(int metatileId, QString label, Tileset *primaryTileset, Tileset *secondaryTileset) {
Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset);
if (!tileset)
return false;
static const QRegularExpression expression("[_A-Za-z0-9]*$");
QRegularExpressionValidator validator(expression);
int pos = 0;
if (validator.validate(label, pos) != QValidator::Acceptable)
return false;
tileset->metatileLabels[metatileId] = label;
return true;
}
2023-02-14 20:28:18 +00:00
QString Tileset::getMetatileLabelPrefix()
{
return Tileset::getMetatileLabelPrefix(this->name);
}
QString Tileset::getMetatileLabelPrefix(const QString &name)
{
return QString("METATILE_%1_").arg(QString(name).replace("gTileset_", ""));
}
bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
if (metatileId >= Project::getNumMetatilesTotal())
return false;
2021-02-17 02:45:54 +00:00
if (metatileId < Project::getNumMetatilesPrimary() && metatileId >= primaryTileset->metatiles.length())
return false;
2021-02-17 02:45:54 +00:00
if (metatileId >= Project::getNumMetatilesPrimary() + secondaryTileset->metatiles.length())
return false;
return true;
}
2020-05-03 16:31:44 +01:00
QList<QList<QRgb>> Tileset::getBlockPalettes(Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) {
QList<QList<QRgb>> palettes;
2020-05-03 16:31:44 +01:00
auto primaryPalettes = useTruePalettes ? primaryTileset->palettes : primaryTileset->palettePreviews;
for (int i = 0; i < Project::getNumPalettesPrimary(); i++) {
2021-02-17 02:45:54 +00:00
palettes.append(primaryPalettes.at(i));
}
2020-05-03 16:31:44 +01:00
auto secondaryPalettes = useTruePalettes ? secondaryTileset->palettes : secondaryTileset->palettePreviews;
for (int i = Project::getNumPalettesPrimary(); i < Project::getNumPalettesTotal(); i++) {
2021-02-17 02:45:54 +00:00
palettes.append(secondaryPalettes.at(i));
}
return palettes;
}
2020-05-03 16:31:44 +01:00
QList<QRgb> Tileset::getPalette(int paletteId, Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) {
QList<QRgb> paletteTable;
Tileset *tileset = paletteId < Project::getNumPalettesPrimary()
? primaryTileset
: secondaryTileset;
2020-05-03 16:31:44 +01:00
auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews;
if (paletteId < 0 || paletteId >= palettes.length()){
logError(QString("Invalid tileset palette id '%1' requested.").arg(paletteId));
return paletteTable;
}
2021-02-17 02:45:54 +00:00
for (int i = 0; i < palettes.at(paletteId).length(); i++) {
paletteTable.append(palettes.at(paletteId).at(i));
}
return paletteTable;
}
2019-03-21 23:50:50 +00:00
2022-09-28 01:17:55 +01:00
bool Tileset::appendToHeaders(QString root, QString friendlyName, bool usingAsm) {
QString headersFile = root + "/" + (usingAsm ? projectConfig.getFilePath(ProjectFilePath::tilesets_headers_asm)
2022-10-24 04:38:27 +01:00
: projectConfig.getFilePath(ProjectFilePath::tilesets_headers));
2022-09-28 01:17:55 +01:00
QFile file(headersFile);
2019-03-21 23:50:50 +00:00
if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) {
2022-09-28 01:17:55 +01:00
logError(QString("Could not write to file \"%1\"").arg(headersFile));
2019-03-21 23:50:50 +00:00
return false;
}
2022-10-24 04:38:27 +01:00
QString isSecondaryStr = this->is_secondary ? "TRUE" : "FALSE";
2022-09-28 01:17:55 +01:00
QString dataString = "\n";
if (usingAsm) {
// Append to asm file
2022-09-28 01:17:55 +01:00
dataString.append("\t.align 2\n");
dataString.append(QString("%1::\n").arg(this->name));
dataString.append("\t.byte TRUE @ is compressed\n");
2022-10-24 04:38:27 +01:00
dataString.append(QString("\t.byte %1 @ is secondary\n").arg(isSecondaryStr));
2022-09-28 01:17:55 +01:00
dataString.append("\t.2byte 0 @ padding\n");
dataString.append(QString("\t.4byte gTilesetTiles_%1\n").arg(friendlyName));
dataString.append(QString("\t.4byte gTilesetPalettes_%1\n").arg(friendlyName));
dataString.append(QString("\t.4byte gMetatiles_%1\n").arg(friendlyName));
if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered) {
dataString.append("\t.4byte NULL @ animation callback\n");
dataString.append(QString("\t.4byte gMetatileAttributes_%1\n").arg(friendlyName));
} else {
dataString.append(QString("\t.4byte gMetatileAttributes_%1\n").arg(friendlyName));
dataString.append("\t.4byte NULL @ animation callback\n");
}
} else {
// Append to C file
2022-10-07 01:41:35 +01:00
dataString.append(QString("const struct Tileset %1 =\n{\n").arg(this->name));
if (projectConfig.getTilesetsHaveIsCompressed()) dataString.append(" .isCompressed = TRUE,\n");
2022-10-24 04:38:27 +01:00
dataString.append(QString(" .isSecondary = %1,\n").arg(isSecondaryStr));
2022-10-07 01:41:35 +01:00
dataString.append(QString(" .tiles = gTilesetTiles_%1,\n").arg(friendlyName));
dataString.append(QString(" .palettes = gTilesetPalettes_%1,\n").arg(friendlyName));
dataString.append(QString(" .metatiles = gMetatiles_%1,\n").arg(friendlyName));
dataString.append(QString(" .metatileAttributes = gMetatileAttributes_%1,\n").arg(friendlyName));
if (projectConfig.getTilesetsHaveCallback()) dataString.append(" .callback = NULL,\n");
2022-10-07 01:41:35 +01:00
dataString.append("};\n");
}
2019-03-21 23:50:50 +00:00
file.write(dataString.toUtf8());
file.flush();
file.close();
return true;
}
2022-10-07 19:29:51 +01:00
bool Tileset::appendToGraphics(QString root, QString friendlyName, bool usingAsm) {
2022-09-28 01:17:55 +01:00
QString graphicsFile = root + "/" + (usingAsm ? projectConfig.getFilePath(ProjectFilePath::tilesets_graphics_asm)
: projectConfig.getFilePath(ProjectFilePath::tilesets_graphics));
2019-03-21 23:50:50 +00:00
QFile file(graphicsFile);
if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) {
logError(QString("Could not write to file \"%1\"").arg(graphicsFile));
return false;
}
2022-10-07 01:41:35 +01:00
2022-10-07 19:29:51 +01:00
const QString tilesetDir = this->getExpectedDir();
const QString tilesPath = tilesetDir + "/tiles.4bpp.lz";
const QString palettesPath = tilesetDir + "/palettes/";
2022-10-07 01:41:35 +01:00
2022-09-28 01:17:55 +01:00
QString dataString = "\n";
if (usingAsm) {
// Append to asm file
2022-09-28 01:17:55 +01:00
dataString.append("\t.align 2\n");
dataString.append(QString("gTilesetPalettes_%1::\n").arg(friendlyName));
2022-10-07 01:41:35 +01:00
for (int i = 0; i < Project::getNumPalettesTotal(); i++)
dataString.append(QString("\t.incbin \"%1%2.gbapal\"\n").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0')));
2022-09-28 01:17:55 +01:00
dataString.append("\n\t.align 2\n");
dataString.append(QString("gTilesetTiles_%1::\n").arg(friendlyName));
2022-10-07 01:41:35 +01:00
dataString.append(QString("\t.incbin \"%1\"\n").arg(tilesPath));
2022-09-28 01:17:55 +01:00
} else {
// Append to C file
2022-10-07 01:41:35 +01:00
dataString.append(QString("const u16 gTilesetPalettes_%1[][16] =\n{\n").arg(friendlyName));
for (int i = 0; i < Project::getNumPalettesTotal(); i++)
dataString.append(QString(" INCBIN_U16(\"%1%2.gbapal\"),\n").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0')));
2022-10-07 19:29:51 +01:00
dataString.append("};\n");
2022-10-07 01:41:35 +01:00
dataString.append(QString("\nconst u32 gTilesetTiles_%1[] = INCBIN_U32(\"%2\");\n").arg(friendlyName, tilesPath));
2019-03-21 23:50:50 +00:00
}
file.write(dataString.toUtf8());
file.flush();
file.close();
return true;
}
2022-10-07 19:29:51 +01:00
bool Tileset::appendToMetatiles(QString root, QString friendlyName, bool usingAsm) {
2022-09-28 01:17:55 +01:00
QString metatileFile = root + "/" + (usingAsm ? projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles_asm)
: projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles));
2019-03-21 23:50:50 +00:00
QFile file(metatileFile);
if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) {
logError(QString("Could not write to file \"%1\"").arg(metatileFile));
return false;
}
2022-10-07 01:41:35 +01:00
2022-10-07 19:29:51 +01:00
const QString tilesetDir = this->getExpectedDir();
const QString metatilesPath = tilesetDir + "/metatiles.bin";
const QString metatileAttrsPath = tilesetDir + "/metatile_attributes.bin";
2022-10-07 01:41:35 +01:00
2022-09-28 01:17:55 +01:00
QString dataString = "\n";
if (usingAsm) {
// Append to asm file
2022-09-28 01:17:55 +01:00
dataString.append("\t.align 1\n");
dataString.append(QString("gMetatiles_%1::\n").arg(friendlyName));
2022-10-07 01:41:35 +01:00
dataString.append(QString("\t.incbin \"%1\"\n").arg(metatilesPath));
2022-09-28 01:17:55 +01:00
dataString.append(QString("\n\t.align 1\n"));
dataString.append(QString("gMetatileAttributes_%1::\n").arg(friendlyName));
2022-10-07 01:41:35 +01:00
dataString.append(QString("\t.incbin \"%1\"\n").arg(metatileAttrsPath));
2022-09-28 01:17:55 +01:00
} else {
// Append to C file
2022-10-07 01:41:35 +01:00
dataString.append(QString("const u16 gMetatiles_%1[] = INCBIN_U16(\"%2\");\n").arg(friendlyName, metatilesPath));
QString numBits = QString::number(projectConfig.getMetatileAttributesSize() * 8);
dataString.append(QString("const u%1 gMetatileAttributes_%2[] = INCBIN_U%1(\"%3\");\n").arg(numBits, friendlyName, metatileAttrsPath));
2022-09-28 01:17:55 +01:00
}
2019-03-21 23:50:50 +00:00
file.write(dataString.toUtf8());
file.flush();
file.close();
return true;
}
2022-10-07 19:29:51 +01:00
// The path where Porymap expects a Tileset's graphics assets to be stored (but not necessarily where they actually are)
// Example: for gTileset_DepartmentStore, returns "data/tilesets/secondary/department_store"
2022-10-07 19:29:51 +01:00
QString Tileset::getExpectedDir()
{
2022-10-24 04:38:27 +01:00
return Tileset::getExpectedDir(this->name, this->is_secondary);
2022-10-07 19:29:51 +01:00
}
QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary)
{
static const QRegularExpression re("([a-z])([A-Z0-9])");
2022-10-07 19:29:51 +01:00
const QString category = isSecondary ? "secondary" : "primary";
const QString basePath = projectConfig.getFilePath(ProjectFilePath::data_tilesets_folders) + category + "/";
return basePath + tilesetName.replace("gTileset_", "").replace(re, "\\1_\\2").toLower();
}
// Get the expected positions of the members in struct Tileset.
// Used when parsing asm tileset data, or C tileset data that's missing initializers.
QHash<int, QString> Tileset::getHeaderMemberMap(bool usingAsm)
{
// The asm header has a padding field that needs to be skipped
int paddingOffset = usingAsm ? 1 : 0;
// The position of metatileAttributes changes between games
bool isPokefirered = (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered);
int metatileAttrPosition = (isPokefirered ? 6 : 5) + paddingOffset;
auto map = QHash<int, QString>();
map.insert(1, "isSecondary");
map.insert(2 + paddingOffset, "tiles");
map.insert(3 + paddingOffset, "palettes");
map.insert(4 + paddingOffset, "metatiles");
map.insert(metatileAttrPosition, "metatileAttributes");
return map;
}