tibia-rme/source/basemap.cpp
Eduardo Dantas d78a2546c7
feat: add Cyclopedia protobuf export pipeline (#160)
Add editor support for exporting Cyclopedia and CipSoft protobuf assets from the currently loaded map.

Main changes:
- Add File > Client Assets actions for Static House Data export, Cyclopedia minimap and satellite export, and restoring timestamped client asset backups.
- Keep generic minimap and tileset exports under File > Export.
- Export staticdata, staticmapdata, and mapdata protobuf files with content-hashed filenames.
- Update catalog-content.json so generated assets are picked up by the client.
- Merge compatible CipSoft templates when they match the loaded map.
- Fall back to generated data for custom maps instead of failing the export.
- Support filtered house exports.
- Report warnings for missing or failed static map entries.
- Emit aligned MINIMAP and sprite-based SATELLITE assets for the documented scales, including the 1/16 layer.
- Keep Cyclopedia export progress in the status bar instead of using a blocking modal progress dialog.

Performance:
- Precompute floor and chunk plans for global map exports.
- Write BMP output directly.
- Reuse minimap renders for matching satellite assets.
- Parallelize asset hash and LZMA encoding through a bounded worker queue.
- Remove modal progress overhead from the export path.
- Revert the tile-grid satellite optimization after profiling showed it shifted cost without improving total export time.

Documentation:
- Document the export contract in docs/static-data.md.
- Add docs/cyclopedia-export-roadmap.md with deferred optimization candidates and follow-up notes.

Scope:
- Focus this change on the Cyclopedia/staticdata protobuf export path and the export-specific performance work needed for practical global map exports.
- Move unrelated runtime, performance, and UI experiments out of this branch.

Validation:
- Ran static inspection and git diff checks during the export changes.
- Reviewed Visual Studio CPU profiles after each global map export performance pass.
- Verified hash and LZMA work moved into bounded async workers.
- Verified the restore action is labeled as restore and grouped under File > Client Assets, not File > Export.

This adds the editor-side pipeline needed to generate client Cyclopedia assets from a loaded map while keeping the export flow recoverable, documented, and practical for large maps.
2026-05-28 08:35:21 -03:00

348 lines
8.9 KiB
C++

//////////////////////////////////////////////////////////////////////
// This file is part of Remere's Map Editor
//////////////////////////////////////////////////////////////////////
// Remere's Map Editor is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Remere's Map Editor is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//////////////////////////////////////////////////////////////////////
#include "main.h"
#include "tile.h"
#include "basemap.h"
#include <memory>
BaseMap::BaseMap() :
allocator(),
tilecount(0),
root(*this) {
////
}
BaseMap::~BaseMap() {
////
}
void BaseMap::clear(bool del) {
PositionVector pos_vec;
for (MapIterator map_iter = begin(); map_iter != end(); ++map_iter) {
Tile* t = (*map_iter)->get();
pos_vec.push_back(t->getPosition());
}
for (PositionVector::iterator pos_iter = pos_vec.begin(); pos_iter != pos_vec.end(); ++pos_iter) {
setTile(*pos_iter, nullptr, del);
}
}
void BaseMap::clearVisible(uint32_t mask) {
root.clearVisible(mask);
}
Tile* BaseMap::createTile(int x, int y, int z) {
ASSERT(z < rme::MapLayers);
QTreeNode* leaf = root.getLeafForce(x, y);
TileLocation* loc = leaf->createTile(x, y, z);
if (loc->get()) {
return loc->get();
}
Tile* t = allocator(loc);
leaf->setTile(x, y, z, t);
return t;
}
TileLocation* BaseMap::getTileL(int x, int y, int z) {
ASSERT(z < rme::MapLayers);
QTreeNode* leaf = root.getLeaf(x, y);
if (leaf) {
Floor* floor = leaf->getFloor(z);
if (floor) {
return &floor->locs[(x & 3) * 4 + (y & 3)];
}
}
return nullptr;
}
const TileLocation* BaseMap::getTileL(int x, int y, int z) const {
// Don't create static const maps!
BaseMap* self = const_cast<BaseMap*>(this);
return self->getTileL(x, y, z);
}
TileLocation* BaseMap::getTileL(const Position &pos) {
return getTileL(pos.x, pos.y, pos.z);
}
const TileLocation* BaseMap::getTileL(const Position &pos) const {
return getTileL(pos.x, pos.y, pos.z);
}
TileLocation* BaseMap::createTileL(int x, int y, int z) {
ASSERT(z < rme::MapLayers);
QTreeNode* leaf = root.getLeafForce(x, y);
Floor* floor = leaf->createFloor(x, y, z);
uint32_t offsetX = x & 3;
uint32_t offsetY = y & 3;
return &floor->locs[offsetX * 4 + offsetY];
}
TileLocation* BaseMap::createTileL(const Position &pos) {
return createTileL(pos.x, pos.y, pos.z);
}
void BaseMap::setTile(TileLocation* location, Tile* new_tile, bool remove) {
ASSERT(location);
ASSERT(!new_tile || new_tile->getX() == location->getX());
ASSERT(!new_tile || new_tile->getY() == location->getY());
ASSERT(!new_tile || new_tile->getZ() == location->getZ());
Tile* old_tile = location->tile;
location->tile = new_tile;
if ((remove && old_tile) || new_tile) {
updateUniqueIds(remove ? old_tile : nullptr, new_tile);
}
if (new_tile && !old_tile) {
++tilecount;
} else if (old_tile && !new_tile) {
--tilecount;
}
if (remove) {
std::unique_ptr<Tile> { old_tile };
}
}
void BaseMap::setTile(int x, int y, int z, Tile* new_tile, bool remove) {
ASSERT(z >= 0 && z < rme::MapLayers);
ASSERT(!new_tile || new_tile->getX() == x);
ASSERT(!new_tile || new_tile->getY() == y);
ASSERT(!new_tile || new_tile->getZ() == z);
QTreeNode* leaf = root.getLeafForce(x, y);
Tile* old_tile = leaf->setTile(x, y, z, new_tile);
if ((remove && old_tile) || new_tile) {
updateUniqueIds(remove ? old_tile : nullptr, new_tile);
}
if (remove) {
std::unique_ptr<Tile> { old_tile };
}
}
void BaseMap::setTile(const Position &position, Tile* new_tile, bool remove) {
setTile(position.x, position.y, position.z, new_tile, remove);
}
void BaseMap::setTile(Tile* new_tile, bool remove) {
ASSERT(new_tile);
const Position &position = new_tile->getPosition();
setTile(position.x, position.y, position.z, new_tile, remove);
}
Tile* BaseMap::swapTile(int x, int y, int z, Tile* new_tile) {
ASSERT(z < rme::MapLayers);
ASSERT(!new_tile || new_tile->getX() == x);
ASSERT(!new_tile || new_tile->getY() == y);
ASSERT(!new_tile || new_tile->getZ() == z);
QTreeNode* leaf = root.getLeafForce(x, y);
Tile* old_tile = leaf->setTile(x, y, z, new_tile);
if (old_tile || new_tile) {
updateUniqueIds(old_tile, new_tile);
}
return old_tile;
}
Tile* BaseMap::swapTile(const Position &position, Tile* new_tile) {
return swapTile(position.x, position.y, position.z, new_tile);
}
// Iterators
MapIterator::MapIterator(BaseMap* _map) :
local_i(0),
local_z(0),
current_tile(nullptr),
map(_map) {
////
}
MapIterator::~MapIterator() {
////
}
MapIterator::MapIterator(const MapIterator &other) {
for (std::vector<MapIterator::NodeIndex>::const_iterator it = other.nodestack.begin(); it != other.nodestack.end(); it++) {
nodestack.push_back(MapIterator::NodeIndex(*it));
}
local_i = other.local_i;
local_z = other.local_z;
map = other.map;
current_tile = other.current_tile;
}
MapIterator BaseMap::begin() {
MapIterator it(this);
it.nodestack.push_back(MapIterator::NodeIndex(&root));
while (true) {
MapIterator::NodeIndex &current = it.nodestack.back();
QTreeNode* node = current.node;
int &index = current.index;
// printf("Contemplating %p of %p (stack size %d)\n", node, this, it.nodestack.size());
bool unwind = false;
for (; index < 16; ++index) {
// printf("\tChecking index %d of %p\n", index, node);
if (QTreeNode* child = node->child[index]) {
if (child->isLeaf) {
QTreeNode* leaf = child;
// printf("\t%p is leaf\n", child);
for (it.local_z = 0; it.local_z < rme::MapLayers; ++it.local_z) {
if (Floor* floor = leaf->array[it.local_z]) {
for (it.local_i = 0; it.local_i < 16; ++it.local_i) {
// printf("\tit(%d;%d;%d)\n", it.local_x, it.local_y, it.local_z);
TileLocation &t = floor->locs[it.local_i];
if (t.get()) {
// printf("return it\n");
it.current_tile = &t;
return it;
}
}
}
}
} else {
// printf("\tAdding %p\n", child);
++index;
it.nodestack.push_back(MapIterator::NodeIndex(child));
unwind = true;
break;
}
}
}
if (unwind) {
continue;
}
// printf("Discarding dead node %p\n", node);
it.nodestack.pop_back();
if (it.nodestack.empty()) {
break;
}
}
return end();
}
MapIterator BaseMap::end() {
MapIterator it(this);
it.local_i = -1;
it.local_z = -1;
return it;
}
TileLocation* MapIterator::operator*() {
return current_tile;
}
TileLocation* MapIterator::operator->() {
return current_tile;
}
MapIterator &MapIterator::operator++() {
// printf("MapIterator::operator++");
bool increased = false;
bool first = true;
while (true) {
MapIterator::NodeIndex &current = nodestack.back();
QTreeNode* node = current.node;
int &index = current.index;
// printf("Contemplating %p (stack size %d)\n", node, nodestack.size());
bool unwind = false;
for (; index < rme::MapLayers; ++index) {
// printf("\tChecking index %d of %p\n", index, node);
if (QTreeNode* child = node->child[index]) {
if (child->isLeaf) {
QTreeNode* leaf = child;
// printf("\t%p is leaf\n", child);
for (; local_z < rme::MapLayers; ++local_z) {
// printf("\t\tIterating over Z:%d of %p", local_z, child);
if (Floor* floor = leaf->array[local_z]) {
// printf("\n");
for (; local_i < rme::MapLayers; ++local_i) {
// printf("\t\tIterating over Y:%d of %p\n", local_y, child);
TileLocation &t = floor->locs[local_i];
if (t.get()) {
if (increased) {
// printf("Modified %p to %p\n", current_tile, t);
current_tile = &t;
return *this;
} else {
increased = true;
}
} else if (first) {
increased = true;
first = false;
}
}
if (local_i > rme::MapMaxLayer) {
// printf("\t\tReset local_x\n");
local_i = 0;
}
} else {
// printf(":dead floor\n");
}
}
if (local_z == rme::MapLayers) {
// printf("\t\tReset local_z\n");
local_z = 0;
}
} else {
// printf("\tAdding %p\n", child);
++index;
nodestack.push_back(MapIterator::NodeIndex(child));
unwind = true;
break;
}
}
}
if (unwind) {
continue;
}
// printf("Discarding dead node %p\n", node);
nodestack.pop_back();
if (nodestack.size() == 0) {
// Set all values to "end"
// printf("END\n");
local_z = -1;
local_i = -1;
return *this;
}
}
return *this;
}
MapIterator MapIterator::operator++(int) {
MapIterator i(*this);
++*this;
return i;
}