From f0310d4a63d027e30057a73b6391076eb04cd936 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 13 Dec 2023 02:30:22 -0500 Subject: [PATCH] Generalize bit packing utility for Block --- include/config.h | 1 + include/core/bitpacker.h | 28 ++++ include/core/block.h | 10 +- include/core/metatile.h | 106 +++++---------- include/project.h | 11 +- porymap.pro | 2 + src/config.cpp | 9 +- src/core/bitpacker.cpp | 51 +++++++ src/core/block.cpp | 61 +++++++-- src/core/metatile.cpp | 226 +++++++++++++------------------ src/editor.cpp | 8 +- src/mainwindow.cpp | 8 +- src/project.cpp | 70 ++++++---- src/scriptapi/apimap.cpp | 8 +- src/ui/imageproviders.cpp | 2 +- src/ui/projectsettingseditor.cpp | 10 +- src/ui/tileseteditor.cpp | 18 +-- 17 files changed, 347 insertions(+), 282 deletions(-) create mode 100644 include/core/bitpacker.h create mode 100644 src/core/bitpacker.cpp diff --git a/include/config.h b/include/config.h index 4191bc98..dd6c90c5 100644 --- a/include/config.h +++ b/include/config.h @@ -213,6 +213,7 @@ enum ProjectFilePath { constants_metatile_behaviors, constants_species, constants_fieldmap, + global_fieldmap, initial_facing_table, pokemon_icon_table, pokemon_gfx, diff --git a/include/core/bitpacker.h b/include/core/bitpacker.h new file mode 100644 index 00000000..e6f6e845 --- /dev/null +++ b/include/core/bitpacker.h @@ -0,0 +1,28 @@ +#ifndef BITPACKER_H +#define BITPACKER_H + +#include +//#include + +class BitPacker +{ +public: + BitPacker() = default; + BitPacker(uint32_t mask); + +public: + void setMask(uint32_t mask); + uint32_t mask() const { return m_mask; } + uint32_t maxValue() const { return m_maxValue; } + + uint32_t unpack(uint32_t data) const; + uint32_t pack(uint32_t value) const; + uint32_t clamp(uint32_t value) const; + +private: + uint32_t m_mask = 0; + uint32_t m_maxValue = 0; + QList m_setBits; +}; + +#endif // BITPACKER_H diff --git a/include/core/block.h b/include/core/block.h index 312893f5..827ac963 100644 --- a/include/core/block.h +++ b/include/core/block.h @@ -14,13 +14,17 @@ public: Block &operator=(const Block &); bool operator ==(Block) const; bool operator !=(Block) const; - void setMetatileId(uint16_t metatileId) { m_metatileId = metatileId; } - void setCollision(uint16_t collision) { m_collision = collision; } - void setElevation(uint16_t elevation) { m_elevation = elevation; } + void setMetatileId(uint16_t metatileId); + void setCollision(uint16_t collision); + void setElevation(uint16_t elevation); uint16_t metatileId() const { return m_metatileId; } uint16_t collision() const { return m_collision; } uint16_t elevation() const { return m_elevation; } uint16_t rawValue() const; + static void setLayout(); + static uint16_t getMaxMetatileId(); + static uint16_t getMaxCollision(); + static uint16_t getMaxElevation(); private: uint16_t m_metatileId; // 10 diff --git a/include/core/metatile.h b/include/core/metatile.h index ed1f84ee..5ef86a3e 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -4,12 +4,14 @@ #include "tile.h" #include "config.h" +#include "bitpacker.h" #include #include #include class Project; +// TODO: Reevaluate enums enum { METATILE_LAYER_MIDDLE_TOP, METATILE_LAYER_BOTTOM_MIDDLE, @@ -32,67 +34,46 @@ enum { NUM_METATILE_TERRAIN_TYPES }; -class MetatileAttr -{ -public: - MetatileAttr(); - MetatileAttr(uint32_t mask, int shift); - -public: - uint32_t mask; - int shift; - - // Given the raw value for all attributes of a metatile - // Returns the extracted value for this attribute - uint32_t fromRaw(uint32_t raw) const { return (raw & this->mask) >> this->shift; } - - // Given a value for this attribute - // Returns the raw value to OR together with the other attributes - uint32_t toRaw(uint32_t value) const { return (value << this->shift) & this->mask; } - - // Given an arbitrary value to set for an attribute - // Returns a bounded value for that attribute - uint32_t getClamped(int value) const { return static_cast(value) & (this->mask >> this->shift); } -}; - class Metatile { public: - Metatile(); + Metatile() = default; Metatile(const Metatile &other) = default; Metatile &operator=(const Metatile &other) = default; Metatile(const int numTiles); + enum Attr { + Behavior, + TerrainType, + EncounterType, + LayerType, + Unused, // Preserve bits not used by the other attributes + }; + public: QList tiles; - uint32_t behavior; - uint32_t terrainType; - uint32_t encounterType; - uint32_t layerType; - uint32_t unusedAttributes; - uint32_t getAttributes(); + uint32_t getAttributes() const; + uint32_t getAttribute(Metatile::Attr attr) const { return this->attributes.value(attr, 0); } void setAttributes(uint32_t data); void setAttributes(uint32_t data, BaseGameVersion version); + void setAttribute(Metatile::Attr attr, uint32_t value); - void setBehavior(int value) { this->behavior = behaviorAttr.getClamped(value); } - void setTerrainType(int value) { this->terrainType = terrainTypeAttr.getClamped(value); } - void setEncounterType(int value) { this->encounterType = encounterTypeAttr.getClamped(value); } - void setLayerType(int value) { this->layerType = layerTypeAttr.getClamped(value); } - - static uint32_t getBehaviorMask() { return behaviorAttr.mask; } - static uint32_t getTerrainTypeMask() { return terrainTypeAttr.mask; } - static uint32_t getEncounterTypeMask() { return encounterTypeAttr.mask; } - static uint32_t getLayerTypeMask() { return layerTypeAttr.mask; } - static uint32_t getBehaviorMask(BaseGameVersion version); - static uint32_t getTerrainTypeMask(BaseGameVersion version); - static uint32_t getEncounterTypeMask(BaseGameVersion version); - static uint32_t getLayerTypeMask(BaseGameVersion version); + // For convenience + uint32_t behavior() const { return this->getAttribute(Attr::Behavior); } + uint32_t terrainType() const { return this->getAttribute(Attr::TerrainType); } + uint32_t encounterType() const { return this->getAttribute(Attr::EncounterType); } + uint32_t layerType() const { return this->getAttribute(Attr::LayerType); } + void setBehavior(int value) { this->setAttribute(Attr::Behavior, static_cast(value)); } + void setTerrainType(int value) { this->setAttribute(Attr::TerrainType, static_cast(value)); } + void setEncounterType(int value) { this->setAttribute(Attr::EncounterType, static_cast(value)); } + void setLayerType(int value) { this->setAttribute(Attr::LayerType, static_cast(value)); } static int getIndexInTileset(int); static QPoint coordFromPixmapCoord(const QPointF &pixelCoord); + static uint32_t getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr); static int getDefaultAttributesSize(BaseGameVersion version); - static void setCustomLayout(Project*); + static void setLayout(Project*); static QString getMetatileIdString(uint16_t metatileId) { return "0x" + QString("%1").arg(metatileId, 3, 16, QChar('0')).toUpper(); }; @@ -103,37 +84,18 @@ public: return metatiles.join(","); }; + inline bool operator==(const Metatile &other) { + return this->tiles == other.tiles && this->attributes == other.attributes; + } + + inline bool operator!=(const Metatile &other) { + return !(operator==(other)); + } + private: - // Stores how each attribute should be laid out for all metatiles, according to the user's config - static MetatileAttr behaviorAttr; - static MetatileAttr terrainTypeAttr; - static MetatileAttr encounterTypeAttr; - static MetatileAttr layerTypeAttr; + QMap attributes; - static uint32_t unusedAttrMask; - - // Stores how each attribute should be laid out for all metatiles, according to the vanilla games - // Used to set default config values and import maps with AdvanceMap - static const QHash defaultLayoutFRLG; - static const QHash defaultLayoutRSE; - static const QHash*> defaultLayouts; - - static void setCustomAttributeLayout(MetatileAttr *, uint32_t, uint32_t); - static bool isMaskTooSmall(MetatileAttr *, int); static bool doMasksOverlap(QList); }; -inline bool operator==(const Metatile &a, const Metatile &b) { - return a.behavior == b.behavior && - a.layerType == b.layerType && - a.encounterType == b.encounterType && - a.terrainType == b.terrainType && - a.unusedAttributes == b.unusedAttributes && - a.tiles == b.tiles; -} - -inline bool operator!=(const Metatile &a, const Metatile &b) { - return !(operator==(a, b)); -} - #endif // METATILE_H diff --git a/include/project.h b/include/project.h index e9f41267..39bd6fe7 100644 --- a/include/project.h +++ b/include/project.h @@ -85,6 +85,11 @@ public: QMap modifiedFileTimestamps; bool usingAsmTilesets; QString importExportPath; + bool parsedMetatileIdMask; + bool parsedCollisionMask; + bool parsedElevationMask; + bool parsedBehaviorMask; + bool parsedLayerTypeMask; void set_root(QString); @@ -196,6 +201,7 @@ public: bool readObjEventGfxConstants(); bool readSongNames(); bool readEventGraphics(); + bool readFieldmapMasks(); QMap> readObjEventGfxInfo(); void setEventPixmap(Event *event, bool forceLoad = false); @@ -229,8 +235,6 @@ public: static bool mapDimensionsValid(int width, int height); bool calculateDefaultMapSize(); static int getMaxObjectEvents(); - static int getMaxCollision(); - static int getMaxElevation(); private: void updateMapLayout(Map*); @@ -248,14 +252,11 @@ private: static int num_tiles_primary; static int num_tiles_total; static int num_metatiles_primary; - static int num_metatiles_total; static int num_pals_primary; static int num_pals_total; static int max_map_data_size; static int default_map_size; static int max_object_events; - static int max_collision; - static int max_elevation; QStringListModel eventScriptLabelModel; QCompleter eventScriptLabelCompleter; diff --git a/porymap.pro b/porymap.pro index 63b97c1a..ddb3f621 100644 --- a/porymap.pro +++ b/porymap.pro @@ -16,6 +16,7 @@ QMAKE_CXXFLAGS += -std=c++17 -Wall QMAKE_TARGET_BUNDLE_PREFIX = com.pret SOURCES += src/core/block.cpp \ + src/core/bitpacker.cpp \ src/core/blockdata.cpp \ src/core/events.cpp \ src/core/heallocation.cpp \ @@ -104,6 +105,7 @@ SOURCES += src/core/block.cpp \ src/ui/uintspinbox.cpp HEADERS += include/core/block.h \ + include/core/bitpacker.h \ include/core/blockdata.h \ include/core/events.h \ include/core/heallocation.h \ diff --git a/src/config.cpp b/src/config.cpp index e82a5dd3..2517406e 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -59,6 +59,7 @@ const QMap> ProjectConfig::defaultP {ProjectFilePath::constants_metatile_behaviors, { "constants_metatile_behaviors", "include/constants/metatile_behaviors.h"}}, {ProjectFilePath::constants_species, { "constants_species", "include/constants/species.h"}}, {ProjectFilePath::constants_fieldmap, { "constants_fieldmap", "include/fieldmap.h"}}, + {ProjectFilePath::global_fieldmap, { "global_fieldmap", "include/global.fieldmap.h"}}, {ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}}, {ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}}, {ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}}, @@ -766,10 +767,10 @@ void ProjectConfig::setUnreadKeys() { if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? DEFAULT_BORDER_FRLG : DEFAULT_BORDER_RSE; if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg"; if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion); - if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getBehaviorMask(this->baseGameVersion); - if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getTerrainTypeMask(this->baseGameVersion); - if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getEncounterTypeMask(this->baseGameVersion); - if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getLayerTypeMask(this->baseGameVersion); + if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::Behavior); + if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::TerrainType); + if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::EncounterType); + if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::LayerType); if (!readKeys.contains("enable_map_allow_flags")) this->enableMapAllowFlags = (this->baseGameVersion != BaseGameVersion::pokeruby); } diff --git a/src/core/bitpacker.cpp b/src/core/bitpacker.cpp new file mode 100644 index 00000000..dd8815bb --- /dev/null +++ b/src/core/bitpacker.cpp @@ -0,0 +1,51 @@ +#include "bitpacker.h" +#include + +// Sometimes we can't explicitly define bitfields because we need to allow users to +// change the size and arrangement of its members. In those cases we use this +// convenience class to handle packing and unpacking each member. + +BitPacker::BitPacker(uint32_t mask) { + this->setMask(mask); +} + +void BitPacker::setMask(uint32_t mask) { + m_mask = mask; + + // Precalculate the number and positions of the mask bits + m_setBits.clear(); + for (int i = 0; mask != 0; mask >>= 1, i++) + if (mask & 1) m_setBits.append(1 << i); + + // For masks with only contiguous bits m_maxValue is equivalent to (m_mask >> n), where n is the number of trailing 0's in m_mask. + m_maxValue = (m_setBits.length() >= 32) ? UINT_MAX : ((1 << m_setBits.length()) - 1); +} + +// Given an arbitrary value to set for this bitfield member, returns a bounded value that can later be packed losslessly. +uint32_t BitPacker::clamp(uint32_t value) const { + return (m_maxValue == UINT_MAX) ? value : (value % (m_maxValue + 1)); +} + +// Given packed data, returns the extracted value for the bitfield member. +// For masks with only contiguous bits this is equivalent to ((data & m_mask) >> n), where n is the number of trailing 0's in m_mask. +uint32_t BitPacker::unpack(uint32_t data) const { + uint32_t value = 0; + data &= m_mask; + for (int i = 0; i < m_setBits.length(); i++) { + if (data & m_setBits.at(i)) + value |= (1 << i); + } + return value; +} + +// Given a value for the bitfield member, returns the value to OR together with the other members. +// For masks with only contiguous bits this is equivalent to ((value << n) & m_mask), where n is the number of trailing 0's in m_mask. +uint32_t BitPacker::pack(uint32_t value) const { + uint32_t data = 0; + for (int i = 0; i < m_setBits.length(); i++) { + if (value == 0) return data; + if (value & 1) data |= m_setBits.at(i); + value >>= 1; + } + return data; +} diff --git a/src/core/block.cpp b/src/core/block.cpp index 2f79af8c..26eee906 100644 --- a/src/core/block.cpp +++ b/src/core/block.cpp @@ -1,4 +1,10 @@ #include "block.h" +#include "bitpacker.h" +#include "config.h" + +static BitPacker bitsMetatileId = BitPacker(0x3FF); +static BitPacker bitsCollision = BitPacker(0xC00); +static BitPacker bitsElevation = BitPacker(0xF000); Block::Block() : m_metatileId(0), @@ -12,10 +18,10 @@ Block::Block(uint16_t metatileId, uint16_t collision, uint16_t elevation) : m_elevation(elevation) { } -Block::Block(uint16_t word) : - m_metatileId(word & 0x3ff), - m_collision((word >> 10) & 0x3), - m_elevation((word >> 12) & 0xf) +Block::Block(uint16_t data) : + m_metatileId(bitsMetatileId.unpack(data)), + m_collision(bitsCollision.unpack(data)), + m_elevation(bitsElevation.unpack(data)) { } Block::Block(const Block &other) : @@ -32,16 +38,53 @@ Block &Block::operator=(const Block &other) { } uint16_t Block::rawValue() const { - return static_cast( - (m_metatileId & 0x3ff) + - ((m_collision & 0x3) << 10) + - ((m_elevation & 0xf) << 12)); + return bitsMetatileId.pack(m_metatileId) + | bitsCollision.pack(m_collision) + | bitsElevation.pack(m_elevation); +} + +// TODO: Resolve TODOs for max block limits, and disable collision tab if collision and elevation are 0 +// TODO: After parsing, recalc max collision/elevation for selector image (in Metatile::setLayout?) +// TODO: More generous config limits +// TODO: Settings editor -- disable UI & restore after refresh, red flag overlapping masks +// TODO: Generalize API tab disabling, i.e. check if disabled before allowing selection +// TODO: Metatile selector looks like it's having a fit during group block select +void Block::setLayout() { + bitsMetatileId.setMask(projectConfig.getBlockMetatileIdMask()); + bitsCollision.setMask(projectConfig.getBlockCollisionMask()); + bitsElevation.setMask(projectConfig.getBlockElevationMask()); } bool Block::operator ==(Block other) const { - return (m_metatileId == other.m_metatileId) && (m_collision == other.m_collision) && (m_elevation == other.m_elevation); + return (m_metatileId == other.m_metatileId) + && (m_collision == other.m_collision) + && (m_elevation == other.m_elevation); } bool Block::operator !=(Block other) const { return !(operator ==(other)); } + +void Block::setMetatileId(uint16_t metatileId) { + m_metatileId = bitsMetatileId.clamp(metatileId); +} + +void Block::setCollision(uint16_t collision) { + m_collision = bitsCollision.clamp(collision); +} + +void Block::setElevation(uint16_t elevation) { + m_elevation = bitsElevation.clamp(elevation); +} + +uint16_t Block::getMaxMetatileId() { + return bitsMetatileId.maxValue(); +} + +uint16_t Block::getMaxCollision() { + return bitsCollision.maxValue(); +} + +uint16_t Block::getMaxElevation() { + return bitsElevation.maxValue(); +} diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 160b0c42..ca706bda 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -2,58 +2,24 @@ #include "tileset.h" #include "project.h" -const QHash Metatile::defaultLayoutFRLG = { - {"behavior", MetatileAttr(0x000001FF, 0) }, - {"terrainType", MetatileAttr(0x00003E00, 9) }, - {"encounterType", MetatileAttr(0x07000000, 24) }, - {"layerType", MetatileAttr(0x60000000, 29) }, +// Stores how each attribute should be laid out for all metatiles, according to the vanilla games. +// Used to set default config values and import maps with AdvanceMap. +static const QMap attributePackersFRLG = { + {Metatile::Attr::Behavior, BitPacker(0x000001FF) }, + {Metatile::Attr::TerrainType, BitPacker(0x00003E00) }, + {Metatile::Attr::EncounterType, BitPacker(0x07000000) }, + {Metatile::Attr::LayerType, BitPacker(0x60000000) }, + //{Metatile::Attr::Unused, BitPacker(0x98FFC000) }, +}; +static const QMap attributePackersRSE = { + {Metatile::Attr::Behavior, BitPacker(0x00FF) }, + //{Metatile::Attr::Unused, BitPacker(0x0F00) }, + {Metatile::Attr::LayerType, BitPacker(0xF000) }, }; -const QHash Metatile::defaultLayoutRSE = { - {"behavior", MetatileAttr(0x00FF, 0) }, - {"terrainType", MetatileAttr() }, - {"encounterType", MetatileAttr() }, - {"layerType", MetatileAttr(0xF000, 12) }, -}; +static QMap attributePackers; -const QHash*> Metatile::defaultLayouts = { - { BaseGameVersion::pokeruby, &defaultLayoutRSE }, - { BaseGameVersion::pokefirered, &defaultLayoutFRLG }, - { BaseGameVersion::pokeemerald, &defaultLayoutRSE }, -}; - -MetatileAttr Metatile::behaviorAttr; -MetatileAttr Metatile::terrainTypeAttr; -MetatileAttr Metatile::encounterTypeAttr; -MetatileAttr Metatile::layerTypeAttr; - -uint32_t Metatile::unusedAttrMask = 0; - -MetatileAttr::MetatileAttr() : - mask(0), - shift(0) -{ } - -MetatileAttr::MetatileAttr(uint32_t mask, int shift) : - mask(mask), - shift(shift) -{ } - -Metatile::Metatile() : - behavior(0), - terrainType(0), - encounterType(0), - layerType(0), - unusedAttributes(0) -{ } - -Metatile::Metatile(const int numTiles) : - behavior(0), - terrainType(0), - encounterType(0), - layerType(0), - unusedAttributes(0) -{ +Metatile::Metatile(const int numTiles) { Tile tile = Tile(); for (int i = 0; i < numTiles; i++) { this->tiles.append(tile); @@ -74,31 +40,46 @@ QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { return QPoint(x, y); } -// Set the layout of a metatile attribute using the mask read from the config file -void Metatile::setCustomAttributeLayout(MetatileAttr * attr, uint32_t mask, uint32_t max) { - if (mask > max) { - uint32_t oldMask = mask; - mask &= max; - logWarn(QString("Metatile attribute mask '0x%1' has been truncated to '0x%2'") - .arg(QString::number(oldMask, 16).toUpper()) - .arg(QString::number(mask, 16).toUpper())); +// Read and pack together this metatile's attributes. +uint32_t Metatile::getAttributes() const { + uint32_t data = 0; + for (auto i = this->attributes.cbegin(), end = this->attributes.cend(); i != end; i++){ + const auto packer = attributePackers.value(i.key()); + data |= packer.pack(i.value()); } - attr->mask = mask; - attr->shift = mask ? log2(mask & ~(mask - 1)) : 0; // Get position of the least significant set bit + return data; } -// For checking whether a metatile attribute mask can contain all the available hard-coded options -bool Metatile::isMaskTooSmall(MetatileAttr * attr, int max) { - if (attr->mask == 0 || max <= 0) return false; +// Unpack and insert metatile attributes from the given data. +void Metatile::setAttributes(uint32_t data) { + for (auto i = attributePackers.cbegin(), end = attributePackers.cend(); i != end; i++){ + const auto packer = i.value(); + this->setAttribute(i.key(), packer.unpack(data)); + } +} - // Get position of the most significant set bit - uint32_t n = log2(max); +// Unpack and insert metatile attributes from the given data using a vanilla layout. For AdvanceMap import +void Metatile::setAttributes(uint32_t data, BaseGameVersion version) { + const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE; + for (auto i = vanillaPackers.cbegin(), end = vanillaPackers.cend(); i != end; i++){ + const auto packer = i.value(); + this->setAttribute(i.key(), packer.unpack(data)); + } +} - // Get a mask for all values 0 to max. - // This may fail for n > 30, but that's not possible here. - uint32_t rangeMask = (1 << (n + 1)) - 1; +// Set the value for a metatile attribute, and fit it within the valid value range. +void Metatile::setAttribute(Metatile::Attr attr, uint32_t value) { + const auto packer = attributePackers.value(attr); + this->attributes.insert(attr, packer.clamp(value)); +} - return attr->getClamped(rangeMask) != rangeMask; +int Metatile::getDefaultAttributesSize(BaseGameVersion version) { + return (version == BaseGameVersion::pokefirered) ? 4 : 2; +} + +uint32_t Metatile::getDefaultAttributesMask(BaseGameVersion version, Metatile::Attr attr) { + const auto vanillaPackers = (version == BaseGameVersion::pokefirered) ? attributePackersFRLG : attributePackersRSE; + return vanillaPackers.value(attr).mask(); } bool Metatile::doMasksOverlap(QList masks) { @@ -110,84 +91,63 @@ bool Metatile::doMasksOverlap(QList masks) { return false; } -void Metatile::setCustomLayout(Project * project) { - // Get the maximum size of any attribute mask +void Metatile::setLayout(Project * project) { + // Read masks from the config and limit them based on the specified attribute size. const QHash maxMasks = { {1, 0xFF}, {2, 0xFFFF}, {4, 0xFFFFFFFF}, }; - const uint32_t maxMask = maxMasks.value(projectConfig.getMetatileAttributesSize(), 0); - - // Set custom attribute masks from the config file - setCustomAttributeLayout(&Metatile::behaviorAttr, projectConfig.getMetatileBehaviorMask(), maxMask); - setCustomAttributeLayout(&Metatile::terrainTypeAttr, projectConfig.getMetatileTerrainTypeMask(), maxMask); - setCustomAttributeLayout(&Metatile::encounterTypeAttr, projectConfig.getMetatileEncounterTypeMask(), maxMask); - setCustomAttributeLayout(&Metatile::layerTypeAttr, projectConfig.getMetatileLayerTypeMask(), maxMask); - - // Set mask for preserving any attribute bits not used by Porymap - Metatile::unusedAttrMask = ~(getBehaviorMask() | getTerrainTypeMask() | getEncounterTypeMask() | getLayerTypeMask()); - Metatile::unusedAttrMask &= maxMask; + uint32_t maxMask = maxMasks.value(projectConfig.getMetatileAttributesSize(), 0); + uint32_t behaviorMask = projectConfig.getMetatileBehaviorMask() & maxMask; + uint32_t terrainTypeMask = projectConfig.getMetatileTerrainTypeMask() & maxMask; + uint32_t encounterTypeMask = projectConfig.getMetatileEncounterTypeMask() & maxMask; + uint32_t layerTypeMask = projectConfig.getMetatileLayerTypeMask() & maxMask; // Overlapping masks are technically ok, but probably not intended. // Additionally, Porymap will not properly reflect that the values are linked. - if (doMasksOverlap({getBehaviorMask(), getTerrainTypeMask(), getEncounterTypeMask(), getLayerTypeMask()})) { + if (doMasksOverlap({behaviorMask, terrainTypeMask, encounterTypeMask, layerTypeMask})) { logWarn("Metatile attribute masks are overlapping. This may result in unexpected attribute values."); } - // Warn the user if they have set a nonzero mask that is too small to contain its available options. - // They'll be allowed to select the options, but they'll be truncated to a different value when revisited. - if (!project->metatileBehaviorMapInverse.isEmpty()) { - int maxBehavior = project->metatileBehaviorMapInverse.lastKey(); - if (isMaskTooSmall(&Metatile::behaviorAttr, maxBehavior)) - logWarn(QString("Metatile Behavior mask is too small to contain all %1 available options.").arg(maxBehavior)); + + // Calculate mask of bits not used by standard behaviors so we can preserve this data. + uint32_t unusedMask = ~(behaviorMask | terrainTypeMask | encounterTypeMask | layerTypeMask); + unusedMask &= maxMask; + + BitPacker packer = BitPacker(unusedMask); + attributePackers.clear(); + attributePackers.insert(Metatile::Attr::Unused, unusedMask); + + // TODO: Test displaying 32 bit behavior + // TODO: Logging masks to hex + // Validate metatile behavior mask + packer.setMask(behaviorMask); + if (behaviorMask && !project->metatileBehaviorMapInverse.isEmpty()) { + uint32_t maxBehavior = project->metatileBehaviorMapInverse.lastKey(); + if (packer.clamp(maxBehavior) != maxBehavior) + logWarn(QString("Metatile Behavior mask '%1' is insufficient to contain all available options.").arg(behaviorMask)); } - if (isMaskTooSmall(&Metatile::terrainTypeAttr, NUM_METATILE_TERRAIN_TYPES - 1)) - logWarn(QString("Metatile Terrain Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_TERRAIN_TYPES)); - if (isMaskTooSmall(&Metatile::encounterTypeAttr, NUM_METATILE_ENCOUNTER_TYPES - 1)) - logWarn(QString("Metatile Encounter Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_ENCOUNTER_TYPES)); - if (isMaskTooSmall(&Metatile::layerTypeAttr, NUM_METATILE_LAYER_TYPES - 1)) - logWarn(QString("Metatile Layer Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_LAYER_TYPES)); -} + attributePackers.insert(Metatile::Attr::Behavior, packer); -uint32_t Metatile::getAttributes() { - uint32_t attributes = this->unusedAttributes & Metatile::unusedAttrMask; - attributes |= Metatile::behaviorAttr.toRaw(this->behavior); - attributes |= Metatile::terrainTypeAttr.toRaw(this->terrainType); - attributes |= Metatile::encounterTypeAttr.toRaw(this->encounterType); - attributes |= Metatile::layerTypeAttr.toRaw(this->layerType); - return attributes; -} + // Validate terrain type mask + packer.setMask(terrainTypeMask); + const uint32_t maxTerrainType = NUM_METATILE_TERRAIN_TYPES - 1; + if (terrainTypeMask && packer.clamp(maxTerrainType) != maxTerrainType) + logWarn(QString("Metatile Terrain Type mask '%1' is insufficient to contain all %2 available options.").arg(terrainTypeMask).arg(maxTerrainType + 1)); + attributePackers.insert(Metatile::Attr::TerrainType, packer); -void Metatile::setAttributes(uint32_t data) { - this->behavior = Metatile::behaviorAttr.fromRaw(data); - this->terrainType = Metatile::terrainTypeAttr.fromRaw(data); - this->encounterType = Metatile::encounterTypeAttr.fromRaw(data); - this->layerType = Metatile::layerTypeAttr.fromRaw(data); - this->unusedAttributes = data & Metatile::unusedAttrMask; -} + // Validate encounter type mask + packer.setMask(encounterTypeMask); + const uint32_t maxEncounterType = NUM_METATILE_ENCOUNTER_TYPES - 1; + if (encounterTypeMask && packer.clamp(maxEncounterType) != maxEncounterType) + logWarn(QString("Metatile Encounter Type mask '%1' is insufficient to contain all %2 available options.").arg(encounterTypeMask).arg(maxEncounterType + 1)); + attributePackers.insert(Metatile::Attr::EncounterType, packer); -// Read attributes using a vanilla layout, then set them using the user's layout. For AdvanceMap import -void Metatile::setAttributes(uint32_t data, BaseGameVersion version) { - const auto defaultLayout = Metatile::defaultLayouts.value(version); - this->setBehavior(defaultLayout->value("behavior").fromRaw(data)); - this->setTerrainType(defaultLayout->value("terrainType").fromRaw(data)); - this->setEncounterType(defaultLayout->value("encounterType").fromRaw(data)); - this->setLayerType(defaultLayout->value("layerType").fromRaw(data)); -} - -int Metatile::getDefaultAttributesSize(BaseGameVersion version) { - return (version == BaseGameVersion::pokefirered) ? 4 : 2; -} -uint32_t Metatile::getBehaviorMask(BaseGameVersion version) { - return Metatile::defaultLayouts.value(version)->value("behavior").mask; -} -uint32_t Metatile::getTerrainTypeMask(BaseGameVersion version) { - return Metatile::defaultLayouts.value(version)->value("terrainType").mask; -} -uint32_t Metatile::getEncounterTypeMask(BaseGameVersion version) { - return Metatile::defaultLayouts.value(version)->value("encounterType").mask; -} -uint32_t Metatile::getLayerTypeMask(BaseGameVersion version) { - return Metatile::defaultLayouts.value(version)->value("layerType").mask; + // Validate terrain type mask + packer.setMask(layerTypeMask); + const uint32_t maxLayerType = NUM_METATILE_LAYER_TYPES - 1; + if (layerTypeMask && packer.clamp(maxLayerType) != maxLayerType) + logWarn(QString("Metatile Layer Type mask '%1' is insufficient to contain all %2 available options.").arg(layerTypeMask).arg(maxLayerType + 1)); + attributePackers.insert(Metatile::Attr::LayerType, packer); } diff --git a/src/editor.cpp b/src/editor.cpp index dde705e4..b21a7749 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -941,8 +941,8 @@ QString Editor::getMetatileDisplayMessage(uint16_t metatileId) { QString message = QString("Metatile: %1").arg(Metatile::getMetatileIdString(metatileId)); if (label.size()) message += QString(" \"%1\"").arg(label); - if (metatile && metatile->behavior) // Skip MB_NORMAL - message += QString(", Behavior: %1").arg(this->project->metatileBehaviorMapInverse.value(metatile->behavior, QString::number(metatile->behavior))); + if (metatile && metatile->behavior()) // Skip MB_NORMAL + message += QString(", Behavior: %1").arg(this->project->metatileBehaviorMapInverse.value(metatile->behavior(), QString::number(metatile->behavior()))); return message; } @@ -2284,14 +2284,14 @@ void Editor::setCollisionGraphics() { // Any icons for combinations that aren't provided by the image sheet are also created now using default graphics. const int w = 16, h = 16; imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows); - for (int collision = 0; collision <= Project::getMaxCollision(); collision++) { + for (int collision = 0; collision <= Block::getMaxCollision(); collision++) { // If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet. // In this case we just use the rightmost collision icon. This is mostly to support the vanilla case, where technically 0-3 // are valid collision values, but 1-3 have the same meaning, so the vanilla collision selector image only has 2 columns. int x = ((collision < imgColumns) ? collision : (imgColumns - 1)) * w; QList sublist; - for (int elevation = 0; elevation <= Project::getMaxElevation(); elevation++) { + for (int elevation = 0; elevation <= Block::getMaxElevation(); elevation++) { if (elevation < imgRows) { // This elevation has an icon on the image sheet, add it to the list int y = elevation * h; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 714f6335..f0d166a7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -395,8 +395,8 @@ void MainWindow::setProjectSpecificUI() Event::setIcons(); editor->setCollisionGraphics(); - ui->spinBox_SelectedElevation->setMaximum(Project::getMaxElevation()); - ui->spinBox_SelectedCollision->setMaximum(Project::getMaxCollision()); + ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); + ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); } void MainWindow::mapSortOrder_changed(QAction *action) @@ -936,6 +936,7 @@ bool MainWindow::loadDataStructures() { && project->readTilesetProperties() && project->readTilesetLabels() && project->readTilesetMetatileLabels() + && project->readFieldmapMasks() && project->readMaxMapDataSize() && project->readHealLocations() && project->readMiscellaneousConstants() @@ -946,7 +947,8 @@ bool MainWindow::loadDataStructures() { && project->readEventGraphics() && project->readSongNames(); - Metatile::setCustomLayout(project); + Block::setLayout(); + Metatile::setLayout(project); Scripting::populateGlobalObject(this); return success && loadProjectCombos(); diff --git a/src/project.cpp b/src/project.cpp index 9533789a..80168726 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -27,18 +27,13 @@ using OrderedJsonDoc = poryjson::JsonDoc; int Project::num_tiles_primary = 512; int Project::num_tiles_total = 1024; -int Project::num_metatiles_primary = 512; -int Project::num_metatiles_total = 1024; +int Project::num_metatiles_primary = 512; // TODO: Verify fits within max int Project::num_pals_primary = 6; int Project::num_pals_total = 13; int Project::max_map_data_size = 10240; // 0x2800 int Project::default_map_size = 20; int Project::max_object_events = 64; -// TODO: Replace once Block layout can be edited -int Project::max_collision = 3; -int Project::max_elevation = 15; - Project::Project(QWidget *parent) : QObject(parent), eventScriptLabelModel(this), @@ -1870,8 +1865,7 @@ bool Project::readTilesetLabels() { } bool Project::readTilesetProperties() { - QStringList definePrefixes; - definePrefixes << "\\bNUM_"; + static const QStringList definePrefixes{ "\\bNUM_" }; QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); fileWatcher.addPath(root + "/" + filename); QMap defines = parser.readCDefines(filename, definePrefixes); @@ -1900,14 +1894,6 @@ bool Project::readTilesetProperties() { logWarn(QString("Value for tileset property 'NUM_METATILES_IN_PRIMARY' not found. Using default (%1) instead.") .arg(Project::num_metatiles_primary)); } - it = defines.find("NUM_METATILES_TOTAL"); - if (it != defines.end()) { - Project::num_metatiles_total = it.value(); - } - else { - logWarn(QString("Value for tileset property 'NUM_METATILES_TOTAL' not found. Using default (%1) instead.") - .arg(Project::num_metatiles_total)); - } it = defines.find("NUM_PALS_IN_PRIMARY"); if (it != defines.end()) { Project::num_pals_primary = it.value(); @@ -1927,9 +1913,43 @@ bool Project::readTilesetProperties() { return true; } +// Read data masks for Blocks and metatile attributes. +// These settings are exposed in the settings window. If any are parsed from +// the project they'll be visible in the settings window but not editable. +bool Project::readFieldmapMasks() { + // We're looking for the suffix "_MASK". Technically our "prefix" is the whole define. + static const QStringList definePrefixes{ "\\b\\w+_MASK" }; + QString filename = projectConfig.getFilePath(ProjectFilePath::global_fieldmap); + fileWatcher.addPath(root + "/" + filename); + QMap defines = parser.readCDefines(filename, definePrefixes); + + auto it = defines.find("MAPGRID_METATILE_ID_MASK"); + if ((parsedMetatileIdMask = (it != defines.end()))) + projectConfig.setBlockMetatileIdMask(static_cast(it.value())); + + it = defines.find("MAPGRID_COLLISION_MASK"); + if ((parsedCollisionMask = (it != defines.end()))) + projectConfig.setBlockCollisionMask(static_cast(it.value())); + + it = defines.find("MAPGRID_ELEVATION_MASK"); + if ((parsedElevationMask = (it != defines.end()))) + projectConfig.setBlockElevationMask(static_cast(it.value())); + + // TODO: For FRLG, parse from fieldmap.c? + + it = defines.find("METATILE_ATTR_BEHAVIOR_MASK"); + if ((parsedBehaviorMask = (it != defines.end()))) + projectConfig.setMetatileBehaviorMask(static_cast(it.value())); + + it = defines.find("METATILE_ATTR_LAYER_MASK"); + if ((parsedLayerTypeMask = (it != defines.end()))) + projectConfig.setMetatileLayerTypeMask(static_cast(it.value())); + + return true; +} + bool Project::readMaxMapDataSize() { - QStringList definePrefixes; - definePrefixes << "\\bMAX_"; + static const QStringList definePrefixes{ "\\bMAX_" }; QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); // already in fileWatcher from readTilesetProperties QMap defines = parser.readCDefines(filename, definePrefixes); @@ -2272,7 +2292,7 @@ bool Project::readMiscellaneousConstants() { QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global); fileWatcher.addPath(root + "/" + filename); - QStringList definePrefixes("\\bOBJECT_"); + static const QStringList definePrefixes("\\bOBJECT_"); QMap defines = parser.readCDefines(filename, definePrefixes); auto it = defines.find("OBJECT_EVENT_TEMPLATES_COUNT"); @@ -2573,7 +2593,7 @@ int Project::getNumMetatilesPrimary() int Project::getNumMetatilesTotal() { - return Project::num_metatiles_total; + return Block::getMaxMetatileId() + 1; } int Project::getNumPalettesPrimary() @@ -2640,16 +2660,6 @@ int Project::getMaxObjectEvents() return Project::max_object_events; } -int Project::getMaxCollision() -{ - return Project::max_collision; -} - -int Project::getMaxElevation() -{ - return Project::max_elevation; -} - void Project::setImportExportPath(QString filename) { this->importExportPath = QFileInfo(filename).absolutePath(); diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 67f51046..14a6a558 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -625,7 +625,7 @@ int MainWindow::getMetatileLayerType(int metatileId) { Metatile * metatile = this->getMetatile(metatileId); if (!metatile) return -1; - return metatile->layerType; + return metatile->layerType(); } void MainWindow::setMetatileLayerType(int metatileId, int layerType) { @@ -640,7 +640,7 @@ int MainWindow::getMetatileEncounterType(int metatileId) { Metatile * metatile = this->getMetatile(metatileId); if (!metatile) return -1; - return metatile->encounterType; + return metatile->encounterType(); } void MainWindow::setMetatileEncounterType(int metatileId, int encounterType) { @@ -655,7 +655,7 @@ int MainWindow::getMetatileTerrainType(int metatileId) { Metatile * metatile = this->getMetatile(metatileId); if (!metatile) return -1; - return metatile->terrainType; + return metatile->terrainType(); } void MainWindow::setMetatileTerrainType(int metatileId, int terrainType) { @@ -670,7 +670,7 @@ int MainWindow::getMetatileBehavior(int metatileId) { Metatile * metatile = this->getMetatile(metatileId); if (!metatile) return -1; - return metatile->behavior; + return metatile->behavior(); } void MainWindow::setMetatileBehavior(int metatileId, int behavior) { diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index e2419e5c..f52bf590 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -50,7 +50,7 @@ QImage getMetatileImage( QPainter metatile_painter(&metatile_image); bool isTripleLayerMetatile = projectConfig.getTripleLayerMetatilesEnabled(); const int numLayers = 3; // When rendering, metatiles always have 3 layers - int layerType = metatile->layerType; + uint32_t layerType = metatile->layerType(); for (int layer = 0; layer < numLayers; layer++) for (int y = 0; y < 2; y++) for (int x = 0; x < 2; x++) { diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index ce9a139c..e8d676a8 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -103,16 +103,16 @@ void ProjectSettingsEditor::initUi() { this->setBorderMetatilesUi(projectConfig.getUseCustomBorderSize()); // Set spin box limits - int maxMetatileId = Project::getNumMetatilesTotal() - 1; + int maxMetatileId = Block::getMaxMetatileId(); ui->spinBox_FillMetatile->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile1->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile2->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile3->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile4->setMaximum(maxMetatileId); - ui->spinBox_Elevation->setMaximum(Project::getMaxElevation()); - ui->spinBox_Collision->setMaximum(Project::getMaxCollision()); - ui->spinBox_MaxElevation->setMaximum(Project::getMaxElevation()); - ui->spinBox_MaxCollision->setMaximum(Project::getMaxCollision()); + ui->spinBox_Elevation->setMaximum(Block::getMaxElevation()); + ui->spinBox_Collision->setMaximum(Block::getMaxCollision()); + ui->spinBox_MaxElevation->setMaximum(Block::getMaxElevation()); + ui->spinBox_MaxCollision->setMaximum(Block::getMaxCollision()); // TODO: Move to a global ui->spinBox_MetatileIdMask->setMinimum(0x1); ui->spinBox_MetatileIdMask->setMaximum(0xFFFF); // Metatile IDs can use all 16 bits of a block diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 2ff85d96..34e24b3f 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -113,7 +113,7 @@ void TilesetEditor::initUi() { void TilesetEditor::setAttributesUi() { // Behavior - if (Metatile::getBehaviorMask()) { + if (projectConfig.getMetatileBehaviorMask() != 0) { for (int num : project->metatileBehaviorMapInverse.keys()) { this->ui->comboBox_metatileBehaviors->addItem(project->metatileBehaviorMapInverse[num], num); } @@ -124,7 +124,7 @@ void TilesetEditor::setAttributesUi() { } // Terrain Type - if (Metatile::getTerrainTypeMask()) { + if (projectConfig.getMetatileTerrainTypeMask()) { this->ui->comboBox_terrainType->addItem("Normal", TERRAIN_NONE); this->ui->comboBox_terrainType->addItem("Grass", TERRAIN_GRASS); this->ui->comboBox_terrainType->addItem("Water", TERRAIN_WATER); @@ -137,7 +137,7 @@ void TilesetEditor::setAttributesUi() { } // Encounter Type - if (Metatile::getEncounterTypeMask()) { + if (projectConfig.getMetatileEncounterTypeMask()) { this->ui->comboBox_encounterType->addItem("None", ENCOUNTER_NONE); this->ui->comboBox_encounterType->addItem("Land", ENCOUNTER_LAND); this->ui->comboBox_encounterType->addItem("Water", ENCOUNTER_WATER); @@ -155,7 +155,7 @@ void TilesetEditor::setAttributesUi() { this->ui->comboBox_layerType->addItem("Split - Bottom/Top", METATILE_LAYER_BOTTOM_TOP); this->ui->comboBox_layerType->setEditable(false); this->ui->comboBox_layerType->setMinimumContentsLength(0); - if (!Metatile::getLayerTypeMask()) { + if (!projectConfig.getMetatileLayerTypeMask()) { // User doesn't have triple layer metatiles, but has no layer type attribute. // Porymap is still using the layer type value to render metatiles, and with // no mask set every metatile will be "Middle/Top", so just display the combo @@ -373,10 +373,10 @@ void TilesetEditor::onSelectedMetatileChanged(uint16_t metatileId) { this->ui->lineEdit_metatileLabel->setText(labels.owned); this->ui->lineEdit_metatileLabel->setPlaceholderText(labels.shared); - this->ui->comboBox_metatileBehaviors->setNumberItem(this->metatile->behavior); - this->ui->comboBox_layerType->setNumberItem(this->metatile->layerType); - this->ui->comboBox_encounterType->setNumberItem(this->metatile->encounterType); - this->ui->comboBox_terrainType->setNumberItem(this->metatile->terrainType); + this->ui->comboBox_metatileBehaviors->setNumberItem(this->metatile->behavior()); + this->ui->comboBox_layerType->setNumberItem(this->metatile->layerType()); + this->ui->comboBox_encounterType->setNumberItem(this->metatile->encounterType()); + this->ui->comboBox_terrainType->setNumberItem(this->metatile->terrainType()); } void TilesetEditor::onHoveredTileChanged(uint16_t tile) { @@ -505,7 +505,7 @@ void TilesetEditor::on_comboBox_metatileBehaviors_currentTextChanged(const QStri // This function can also be called when the user selects // a different metatile. Stop this from being considered a change. - if (this->metatile->behavior == static_cast(behavior)) + if (this->metatile->behavior() == static_cast(behavior)) return; Metatile *prevMetatile = new Metatile(*this->metatile);