////////////////////////////////////////////////////////////////////// // 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 . ////////////////////////////////////////////////////////////////////// #include "main.h" #include #include #include #include #include #include "settings.h" #include "gui.h" // Loadbar #include "creatures.h" #include "creature.h" #include "map.h" #include "tile.h" #include "item.h" #include "complexitem.h" #include "town.h" #include "wall_brush.h" #include "iomap_otbm.h" typedef uint8_t attribute_t; typedef uint32_t flags_t; // H4X void reform(Map* map, Tile* tile, Item* item) { /* int aid = item->getActionID(); int id = item->getID(); int uid = item->getUniqueID(); if(item->isDoor()) { item->eraseAttribute("aid"); item->setAttribute("keyid", aid); } if((item->isDoor()) && tile && tile->getHouseID()) { Door* self = static_cast(item); House* house = map->houses.getHouse(tile->getHouseID()); self->setDoorID(house->getEmptyDoorID()); } */ } // ============================================================================ // Item Item* Item::Create_OTBM(const IOMap& maphandle, BinaryNode* stream) { uint16_t _id; if (!stream->getU16(_id)) { return nullptr; } uint8_t _count = 0; const ItemType& iType = g_items[_id]; if (maphandle.version.otbm == MAP_OTBM_1) { if (iType.stackable || iType.isSplash() || iType.isFluidContainer()) { stream->getU8(_count); } } return Item::Create(_id, _count); } bool Item::readItemAttribute_OTBM(const IOMap& maphandle, OTBM_ItemAttribute attr, BinaryNode* stream) { switch (attr) { case OTBM_ATTR_COUNT: { uint8_t subtype; if (!stream->getU8(subtype)) { return false; } setSubtype(subtype); break; } case OTBM_ATTR_ACTION_ID: { uint16_t aid; if (!stream->getU16(aid)) { return false; } setActionID(aid); break; } case OTBM_ATTR_UNIQUE_ID: { uint16_t uid; if (!stream->getU16(uid)) { return false; } setUniqueID(uid); break; } case OTBM_ATTR_CHARGES: { uint16_t charges; if (!stream->getU16(charges)) { return false; } setSubtype(charges); break; } case OTBM_ATTR_TEXT: { std::string text; if (!stream->getString(text)) { return false; } setText(text); break; } case OTBM_ATTR_DESC: { std::string text; if (!stream->getString(text)) { return false; } setDescription(text); break; } case OTBM_ATTR_RUNE_CHARGES: { uint8_t subtype; if (!stream->getU8(subtype)) { return false; } setSubtype(subtype); break; } case OTBM_ATTR_TIER: { uint8_t tier; if (!stream->getU8(tier)) { return false; } setTier(static_cast(tier)); break; } // The following *should* be handled in the derived classes // However, we still need to handle them here since otherwise things // will break horribly case OTBM_ATTR_DEPOT_ID: return stream->skip(2); case OTBM_ATTR_HOUSEDOORID: return stream->skip(1); case OTBM_ATTR_TELE_DEST: return stream->skip(5); case OTBM_ATTR_PODIUMOUTFIT: return stream->skip(15); default: return false; } return true; } bool Item::unserializeAttributes_OTBM(const IOMap& maphandle, BinaryNode* stream) { uint8_t attribute; while (stream->getU8(attribute)) { if (attribute == OTBM_ATTR_ATTRIBUTE_MAP) { if (!ItemAttributes::unserializeAttributeMap(maphandle, stream)) { return false; } } else if (!readItemAttribute_OTBM(maphandle, static_cast(attribute), stream)) { return false; } } return true; } bool Item::unserializeItemNode_OTBM(const IOMap& maphandle, BinaryNode* node) { return unserializeAttributes_OTBM(maphandle, node); } void Item::serializeItemAttributes_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { if (maphandle.version.otbm >= MAP_OTBM_2) { const ItemType& iType = g_items[id]; if (iType.stackable || iType.isSplash() || iType.isFluidContainer()) { stream.addU8(OTBM_ATTR_COUNT); stream.addU8(getSubtype()); } } if (maphandle.version.otbm >= MAP_OTBM_4) { if (attributes && !attributes->empty()) { stream.addU8(OTBM_ATTR_ATTRIBUTE_MAP); serializeAttributeMap(maphandle, stream); } } else { if (g_items.MinorVersion >= CLIENT_VERSION_820 && isCharged()) { stream.addU8(OTBM_ATTR_CHARGES); stream.addU16(getSubtype()); } uint16_t actionId = getActionID(); if (actionId > 0) { stream.addU8(OTBM_ATTR_ACTION_ID); stream.addU16(actionId); } uint16_t uniqueId = getUniqueID(); if (uniqueId > 0) { stream.addU8(OTBM_ATTR_UNIQUE_ID); stream.addU16(uniqueId); } const std::string& text = getText(); if (!text.empty()) { stream.addU8(OTBM_ATTR_TEXT); stream.addString(text); } const std::string& description = getDescription(); if (!description.empty()) { stream.addU8(OTBM_ATTR_DESC); stream.addString(description); } uint16_t tier = getTier(); if (tier > 0) { stream.addU8(OTBM_ATTR_TIER); stream.addU8(static_cast(tier)); } } } void Item::serializeItemCompact_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { stream.addU16(id); /* This is impossible const ItemType& iType = g_items[id]; if(iType.stackable || iType.isSplash() || iType.isFluidContainer()){ stream.addU8(getSubtype()); } */ } bool Item::serializeItemNode_OTBM(const IOMap& maphandle, NodeFileWriteHandle& file) const { file.addNode(OTBM_ITEM); file.addU16(id); if (maphandle.version.otbm == MAP_OTBM_1) { const ItemType& iType = g_items[id]; if (iType.stackable || iType.isSplash() || iType.isFluidContainer()) { file.addU8(getSubtype()); } } serializeItemAttributes_OTBM(maphandle, file); file.endNode(); return true; } // ============================================================================ // Teleport bool Teleport::readItemAttribute_OTBM(const IOMap& maphandle, OTBM_ItemAttribute attribute, BinaryNode* stream) { if (OTBM_ATTR_TELE_DEST == attribute) { uint16_t x, y; uint8_t z; if (!stream->getU16(x) || !stream->getU16(y) || !stream->getU8(z)) { return false; } destination = Position(x, y, z); return true; } else { return Item::readItemAttribute_OTBM(maphandle, attribute, stream); } } void Teleport::serializeItemAttributes_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { Item::serializeItemAttributes_OTBM(maphandle, stream); stream.addByte(OTBM_ATTR_TELE_DEST); stream.addU16(destination.x); stream.addU16(destination.y); stream.addU8(destination.z); } // ============================================================================ // Door bool Door::readItemAttribute_OTBM(const IOMap& maphandle, OTBM_ItemAttribute attribute, BinaryNode* stream) { if (OTBM_ATTR_HOUSEDOORID == attribute) { uint8_t id = 0; if (!stream->getU8(id)) { return false; } doorId = id; return true; } else { return Item::readItemAttribute_OTBM(maphandle, attribute, stream); } } void Door::serializeItemAttributes_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { Item::serializeItemAttributes_OTBM(maphandle, stream); if (doorId) { stream.addByte(OTBM_ATTR_HOUSEDOORID); stream.addU8(doorId); } } DoorType Door::getDoorType() const { WallBrush* wb = getWallBrush(); if (!wb) { return WALL_UNDEFINED; } return wb->getDoorTypeFromID(id); } bool Door::isRealDoor() const { const DoorType& dt = getDoorType(); // doors with no wallbrush will appear as WALL_UNDEFINED // this is for compatibility return dt == WALL_UNDEFINED || dt == WALL_DOOR_NORMAL || dt == WALL_DOOR_LOCKED || dt == WALL_DOOR_QUEST || dt == WALL_DOOR_MAGIC || dt == WALL_DOOR_NORMAL_ALT; } uint8_t Door::getDoorID() const { return isRealDoor() ? doorId : 0; } void Door::setDoorID(uint8_t id) { doorId = isRealDoor() ? id : 0; } // ============================================================================ // Depots bool Depot::readItemAttribute_OTBM(const IOMap& maphandle, OTBM_ItemAttribute attribute, BinaryNode* stream) { if (OTBM_ATTR_DEPOT_ID == attribute) { uint16_t id = 0; if (!stream->getU16(id)) { return false; } depotId = id; return true; } else { return Item::readItemAttribute_OTBM(maphandle, attribute, stream); } } void Depot::serializeItemAttributes_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { Item::serializeItemAttributes_OTBM(maphandle, stream); if (depotId) { stream.addByte(OTBM_ATTR_DEPOT_ID); stream.addU16(depotId); } } // ============================================================================ // Container bool Container::unserializeItemNode_OTBM(const IOMap& maphandle, BinaryNode* node) { if (!Item::unserializeAttributes_OTBM(maphandle, node)) { return false; } BinaryNode* child = node->getChild(); if (child) { do { uint8_t type; if (!child->getByte(type)) { return false; } if (type != OTBM_ITEM) { return false; } Item* item = Item::Create_OTBM(maphandle, child); if (!item) { return false; } if (!item->unserializeItemNode_OTBM(maphandle, child)) { delete item; return false; } contents.push_back(item); } while (child->advance()); } return true; } bool Container::serializeItemNode_OTBM(const IOMap& maphandle, NodeFileWriteHandle& file) const { file.addNode(OTBM_ITEM); file.addU16(id); if (maphandle.version.otbm == MAP_OTBM_1) { // In the ludicrous event that an item is a container AND stackable, we have to do this. :p const ItemType& iType = g_items[id]; if (iType.stackable || iType.isSplash() || iType.isFluidContainer()) { file.addU8(getSubtype()); } } serializeItemAttributes_OTBM(maphandle, file); for (Item* item : contents) { item->serializeItemNode_OTBM(maphandle, file); } file.endNode(); return true; } // ============================================================================ // Podium bool Podium::readItemAttribute_OTBM(const IOMap& maphandle, OTBM_ItemAttribute attribute, BinaryNode* stream) { if (OTBM_ATTR_PODIUMOUTFIT == attribute) { uint8_t flags; uint8_t direction; uint16_t lookType; uint8_t lookHead; uint8_t lookBody; uint8_t lookLegs; uint8_t lookFeet; uint8_t lookAddon; uint16_t lookMount; uint8_t lookMountHead; uint8_t lookMountBody; uint8_t lookMountLegs; uint8_t lookMountFeet; if ( // podium settings stream->getU8(flags) && stream->getU8(direction) && // outfit stream->getU16(lookType) && stream->getU8(lookHead) && stream->getU8(lookBody) && stream->getU8(lookLegs) && stream->getU8(lookFeet) && stream->getU8(lookAddon) && // mount stream->getU16(lookMount) && stream->getU8(lookMountHead) && stream->getU8(lookMountBody) && stream->getU8(lookMountLegs) && stream->getU8(lookMountFeet) ) { //"if" condition ends here setShowOutfit((flags & PODIUM_SHOW_OUTFIT) != 0); setShowMount((flags & PODIUM_SHOW_MOUNT) != 0); setShowPlatform((flags & PODIUM_SHOW_PLATFORM) != 0); setDirection(static_cast(direction)); struct Outfit newOutfit = Outfit(); newOutfit.lookType = static_cast(lookType); newOutfit.lookHead = static_cast(lookHead); newOutfit.lookBody = static_cast(lookBody); newOutfit.lookLegs = static_cast(lookLegs); newOutfit.lookFeet = static_cast(lookFeet); newOutfit.lookAddon = static_cast(lookAddon); newOutfit.lookMount = static_cast(lookMount); newOutfit.lookMountHead = static_cast(lookMountHead); newOutfit.lookMountBody = static_cast(lookMountBody); newOutfit.lookMountLegs = static_cast(lookMountLegs); newOutfit.lookMountFeet = static_cast(lookMountFeet); setOutfit(newOutfit); return true; } return false; } else { return Item::readItemAttribute_OTBM(maphandle, attribute, stream); } } void Podium::serializeItemAttributes_OTBM(const IOMap& maphandle, NodeFileWriteHandle& stream) const { Item::serializeItemAttributes_OTBM(maphandle, stream); uint8_t flags = PODIUM_SHOW_OUTFIT * static_cast(showOutfit) + PODIUM_SHOW_MOUNT * static_cast(showMount) + PODIUM_SHOW_PLATFORM * static_cast(showPlatform); stream.addByte(OTBM_ATTR_PODIUMOUTFIT); stream.addU8(flags); stream.addU8(direction); stream.addU16(outfit.lookType); stream.addU8(outfit.lookHead); stream.addU8(outfit.lookBody); stream.addU8(outfit.lookLegs); stream.addU8(outfit.lookFeet); stream.addU8(outfit.lookAddon); stream.addU16(outfit.lookMount); stream.addU8(outfit.lookMountHead); stream.addU8(outfit.lookMountBody); stream.addU8(outfit.lookMountLegs); stream.addU8(outfit.lookMountFeet); } /* OTBM_ROOTV1 | |--- OTBM_MAP_DATA | | | |--- OTBM_TILE_AREA | | |--- OTBM_TILE | | |--- OTBM_TILE_SQUARE (not implemented) | | |--- OTBM_TILE_REF (not implemented) | | |--- OTBM_HOUSETILE | | | |--- OTBM_SPAWNS (not implemented) | | |--- OTBM_SPAWN_AREA (not implemented) | | |--- OTBM_MONSTER (not implemented) | | | |--- OTBM_TOWNS | |--- OTBM_TOWN | |--- OTBM_ITEM_DEF (not implemented) */ bool IOMapOTBM::getVersionInfo(const FileName& filename, MapVersion& out_ver) { #ifdef OTGZ_SUPPORT if (filename.GetExt() == "otgz") { // Open the archive std::shared_ptr a(archive_read_new(), archive_read_free); archive_read_support_filter_all(a.get()); archive_read_support_format_all(a.get()); if (archive_read_open_filename(a.get(), nstr(filename.GetFullPath()).c_str(), 10240) != ARCHIVE_OK) { return false; } // Loop over the archive entries until we find the otbm file struct archive_entry* entry; while (archive_read_next_header(a.get(), &entry) == ARCHIVE_OK) { std::string entryName = archive_entry_pathname(entry); if (entryName == "world/map.otbm") { // Read the OTBM header into temporary memory uint8_t buffer[8096]; memset(buffer, 0, 8096); // Read from the archive int read_bytes = archive_read_data(a.get(), buffer, 8096); // Check so it at least contains the 4-byte file id if (read_bytes < 4) { return false; } // Create a read handle on it std::shared_ptr f(new MemoryNodeFileReadHandle(buffer + 4, read_bytes - 4)); // Read the version info return getVersionInfo(f.get(), out_ver); } } // Didn't find OTBM file, lame return false; } #endif // Just open a disk-based read handle DiskNodeFileReadHandle f(nstr(filename.GetFullPath()), StringVector(1, "OTBM")); if (!f.isOk()) { return false; } return getVersionInfo(&f, out_ver); } bool IOMapOTBM::getVersionInfo(NodeFileReadHandle* f, MapVersion& out_ver) { BinaryNode* root = f->getRootNode(); if (!root) { return false; } root->skip(1); // Skip the type byte uint16_t u16; uint32_t u32; if (!root->getU32(u32)) { // Version return false; } out_ver.otbm = (MapVersionID)u32; root->getU16(u16); // map size X root->getU16(u16); // map size Y root->getU32(u32); // OTB major version if (!root->getU32(u32)) { // OTB minor version return false; } out_ver.client = ClientVersionID(u32); return true; } bool IOMapOTBM::loadMap(Map& map, const FileName& filename) { #ifdef OTGZ_SUPPORT if (filename.GetExt() == "otgz") { // Open the archive std::shared_ptr a(archive_read_new(), archive_read_free); archive_read_support_filter_all(a.get()); archive_read_support_format_all(a.get()); if (archive_read_open_filename(a.get(), nstr(filename.GetFullPath()).c_str(), 10240) != ARCHIVE_OK) { return false; } // Memory buffers for the houses & spawns std::shared_ptr house_buffer; std::shared_ptr spawn_buffer; size_t house_buffer_size = 0; size_t spawn_buffer_size = 0; // See if the otbm file has been loaded bool otbm_loaded = false; // Loop over the archive entries until we find the otbm file g_gui.SetLoadDone(0, "Decompressing archive..."); struct archive_entry* entry; while (archive_read_next_header(a.get(), &entry) == ARCHIVE_OK) { std::string entryName = archive_entry_pathname(entry); if (entryName == "world/map.otbm") { // Read the entire OTBM file into a memory region size_t otbm_size = archive_entry_size(entry); std::shared_ptr otbm_buffer(new uint8_t[otbm_size], [](uint8_t* p) { delete[] p; }); // Read from the archive size_t read_bytes = archive_read_data(a.get(), otbm_buffer.get(), otbm_size); // Check so it at least contains the 4-byte file id if (read_bytes < 4) { return false; } if (read_bytes < otbm_size) { error("Could not read file."); return false; } g_gui.SetLoadDone(0, "Loading OTBM map..."); // Create a read handle on it std::shared_ptr f( new MemoryNodeFileReadHandle(otbm_buffer.get() + 4, otbm_size - 4) ); // Read the version info if (!loadMap(map, *f.get())) { error("Could not load OTBM file inside archive"); return false; } otbm_loaded = true; } else if (entryName == "world/houses.xml") { house_buffer_size = archive_entry_size(entry); house_buffer.reset(new uint8_t[house_buffer_size]); // Read from the archive size_t read_bytes = archive_read_data(a.get(), house_buffer.get(), house_buffer_size); // Check so it at least contains the 4-byte file id if (read_bytes < house_buffer_size) { house_buffer.reset(); house_buffer_size = 0; warning("Failed to decompress houses."); } } else if (entryName == "world/spawns.xml") { spawn_buffer_size = archive_entry_size(entry); spawn_buffer.reset(new uint8_t[spawn_buffer_size]); // Read from the archive size_t read_bytes = archive_read_data(a.get(), spawn_buffer.get(), spawn_buffer_size); // Check so it at least contains the 4-byte file id if (read_bytes < spawn_buffer_size) { spawn_buffer.reset(); spawn_buffer_size = 0; warning("Failed to decompress spawns."); } } } if (!otbm_loaded) { error("OTBM file not found inside archive."); return false; } // Load the houses from the stored buffer if (house_buffer.get() && house_buffer_size > 0) { pugi::xml_document doc; pugi::xml_parse_result result = doc.load_buffer(house_buffer.get(), house_buffer_size); if (result) { if (!loadHouses(map, doc)) { warning("Failed to load houses."); } } else { warning("Failed to load houses due to XML parse error."); } } // Load the spawns from the stored buffer if (spawn_buffer.get() && spawn_buffer_size > 0) { pugi::xml_document doc; pugi::xml_parse_result result = doc.load_buffer(spawn_buffer.get(), spawn_buffer_size); if (result) { if (!loadSpawns(map, doc)) { warning("Failed to load spawns."); } } else { warning("Failed to load spawns due to XML parse error."); } } return true; } #endif DiskNodeFileReadHandle f(nstr(filename.GetFullPath()), StringVector(1, "OTBM")); if (!f.isOk()) { error(("Couldn't open file for reading\nThe error reported was: " + wxstr(f.getErrorMessage())).wc_str()); return false; } if (!loadMap(map, f)) { return false; } // Read auxilliary files if (!loadHouses(map, filename)) { warning("Failed to load houses."); map.housefile = nstr(filename.GetName()) + "-house.xml"; } if (!loadSpawns(map, filename)) { warning("Failed to load spawns."); map.spawnfile = nstr(filename.GetName()) + "-spawn.xml"; } if (!loadWaypoints(map, filename)) { // just assume the map did not have this file before // warning("Failed to load waypoints."); map.waypointfile = nstr(filename.GetName()) + "-waypoint.xml"; } return true; } bool IOMapOTBM::loadMap(Map& map, NodeFileReadHandle& f) { BinaryNode* root = f.getRootNode(); if (!root) { error("Could not read root node."); return false; } root->skip(1); // Skip the type byte uint8_t u8; uint16_t u16; uint32_t u32; if (!root->getU32(u32)) { return false; } version.otbm = (MapVersionID)u32; if (version.otbm > MAP_OTBM_4) { // Failed to read version if (g_gui.PopupDialog("Map error", "The loaded map appears to be a OTBM format that is not supported by the editor." "Do you still want to attempt to load the map?", wxYES | wxNO) == wxID_YES) { warning("Unsupported or damaged map version"); } else { error("Unsupported OTBM version, could not load map"); return false; } } if (!root->getU16(u16)) { return false; } map.width = u16; if (!root->getU16(u16)) { return false; } map.height = u16; if (!root->getU32(u32) || u32 > (unsigned long)g_items.MajorVersion) { // OTB major version if (g_gui.PopupDialog("Map error", "The loaded map appears to be a items.otb format that deviates from the " "items.otb loaded by the editor. Do you still want to attempt to load the map?", wxYES | wxNO) == wxID_YES) { warning("Unsupported or damaged map version"); } else { error("Outdated items.otb, could not load map"); return false; } } if (!root->getU32(u32) || u32 > (unsigned long)g_items.MinorVersion) { // OTB minor version warning("This editor needs an updated items.otb version"); } version.client = (ClientVersionID)u32; BinaryNode* mapHeaderNode = root->getChild(); if (mapHeaderNode == nullptr || !mapHeaderNode->getByte(u8) || u8 != OTBM_MAP_DATA) { error("Could not get root child node. Cannot recover from fatal error!"); return false; } uint8_t attribute; while (mapHeaderNode->getU8(attribute)) { switch (attribute) { case OTBM_ATTR_DESCRIPTION: { if (!mapHeaderNode->getString(map.description)) { warning("Invalid map description tag"); } // std::cout << "Map description: " << mapDescription << std::endl; break; } case OTBM_ATTR_EXT_SPAWN_FILE: { if (!mapHeaderNode->getString(map.spawnfile)) { warning("Invalid map spawnfile tag"); } break; } case OTBM_ATTR_EXT_HOUSE_FILE: { if (!mapHeaderNode->getString(map.housefile)) { warning("Invalid map housefile tag"); } break; } case OTBM_ATTR_EXT_SPAWN_NPC_FILE: { // compatibility: skip Canary RME NPC spawn file tag std::string stringToSkip; if (!mapHeaderNode->getString(stringToSkip)) { warning("Invalid map housefile tag"); } break; } default: { warning("Unknown header node."); break; } } } int nodes_loaded = 0; for (BinaryNode* mapNode = mapHeaderNode->getChild(); mapNode != nullptr; mapNode = mapNode->advance()) { ++nodes_loaded; if (nodes_loaded % 15 == 0) { g_gui.SetLoadDone(static_cast(100.0 * f.tell() / f.size())); } uint8_t node_type; if (!mapNode->getByte(node_type)) { warning("Invalid map node"); continue; } if (node_type == OTBM_TILE_AREA) { uint16_t base_x, base_y; uint8_t base_z; if (!mapNode->getU16(base_x) || !mapNode->getU16(base_y) || !mapNode->getU8(base_z)) { warning("Invalid map node, no base coordinate"); continue; } for (BinaryNode* tileNode = mapNode->getChild(); tileNode != nullptr; tileNode = tileNode->advance()) { Tile* tile = nullptr; uint8_t tile_type; if (!tileNode->getByte(tile_type)) { warning("Invalid tile type"); continue; } if (tile_type == OTBM_TILE || tile_type == OTBM_HOUSETILE) { // printf("Start\n"); uint8_t x_offset, y_offset; if (!tileNode->getU8(x_offset) || !tileNode->getU8(y_offset)) { warning("Could not read position of tile"); continue; } const Position pos(base_x + x_offset, base_y + y_offset, base_z); if (map.getTile(pos)) { warning("Duplicate tile at %d:%d:%d, discarding duplicate", pos.x, pos.y, pos.z); continue; } tile = map.allocator(map.createTileL(pos)); House* house = nullptr; if (tile_type == OTBM_HOUSETILE) { uint32_t house_id; if (!tileNode->getU32(house_id)) { warning("House tile without house data, discarding tile"); continue; } if (house_id) { house = map.houses.getHouse(house_id); if (!house) { house = newd House(map); house->setID(house_id); map.houses.addHouse(house); } } else { warning("Invalid house id from tile %d:%d:%d", pos.x, pos.y, pos.z); } } // printf("So far so good\n"); uint8_t attribute; while (tileNode->getU8(attribute)) { switch (attribute) { case OTBM_ATTR_TILE_FLAGS: { uint32_t flags = 0; if (!tileNode->getU32(flags)) { warning("Invalid tile flags of tile on %d:%d:%d", pos.x, pos.y, pos.z); } tile->setMapFlags(flags); if (flags & TILESTATE_ZONE_BRUSH) { uint16_t zoneId = 0; do { if (!tileNode->getU16(zoneId)) { warning("Invalid zone id of tile on %d:%d:%d", pos.x, pos.y, pos.z); } if (zoneId != 0) { tile->addZoneId(zoneId); } } while (zoneId != 0); } break; } case OTBM_ATTR_ITEM: { Item* item = Item::Create_OTBM(*this, tileNode); if (item == nullptr) { warning("Invalid item at tile %d:%d:%d", pos.x, pos.y, pos.z); } tile->addItem(item); break; } default: { warning("Unknown tile attribute at %d:%d:%d", pos.x, pos.y, pos.z); break; } } } // printf("Didn't die in loop\n"); for (BinaryNode* itemNode = tileNode->getChild(); itemNode != nullptr; itemNode = itemNode->advance()) { Item* item = nullptr; uint8_t item_type; if (!itemNode->getByte(item_type)) { warning("Unknown item type %d:%d:%d", pos.x, pos.y, pos.z); continue; } if (item_type == OTBM_ITEM) { item = Item::Create_OTBM(*this, itemNode); if (item) { if (!item->unserializeItemNode_OTBM(*this, itemNode)) { warning("Couldn't unserialize item attributes at %d:%d:%d", pos.x, pos.y, pos.z); } // reform(&map, tile, item); tile->addItem(item); } } else { warning("Unknown type of tile child node"); } } tile->update(); if (house) { house->addTile(tile); } map.setTile(pos.x, pos.y, pos.z, tile); } else { warning("Unknown type of tile node"); } } } else if (node_type == OTBM_TOWNS) { for (BinaryNode* townNode = mapNode->getChild(); townNode != nullptr; townNode = townNode->advance()) { Town* town = nullptr; uint8_t town_type; if (!townNode->getByte(town_type)) { warning("Invalid town type (1)"); continue; } if (town_type != OTBM_TOWN) { warning("Invalid town type (2)"); continue; } uint32_t town_id; if (!townNode->getU32(town_id)) { warning("Invalid town id"); continue; } town = map.towns.getTown(town_id); if (town) { warning("Duplicate town id %d, discarding duplicate", town_id); continue; } else { town = newd Town(town_id); if (!map.towns.addTown(town)) { delete town; continue; } } std::string town_name; if (!townNode->getString(town_name)) { warning("Invalid town name"); continue; } town->setName(town_name); Position pos; uint16_t x; uint16_t y; uint8_t z; if (!townNode->getU16(x) || !townNode->getU16(y) || !townNode->getU8(z)) { warning("Invalid town temple position"); continue; } pos.x = x; pos.y = y; pos.z = z; town->setTemplePosition(pos); map.getOrCreateTile(pos)->getLocation()->increaseTownCount(); } } else if (node_type == OTBM_WAYPOINTS) { for (BinaryNode* waypointNode = mapNode->getChild(); waypointNode != nullptr; waypointNode = waypointNode->advance()) { uint8_t waypoint_type; if (!waypointNode->getByte(waypoint_type)) { warning("Invalid waypoint type (1)"); continue; } if (waypoint_type != OTBM_WAYPOINT) { warning("Invalid waypoint type (2)"); continue; } Waypoint wp; if (!waypointNode->getString(wp.name)) { warning("Invalid waypoint name"); continue; } uint16_t x; uint16_t y; uint8_t z; if (!waypointNode->getU16(x) || !waypointNode->getU16(y) || !waypointNode->getU8(z)) { warning("Invalid waypoint position"); continue; } wp.pos.x = x; wp.pos.y = y; wp.pos.z = z; map.waypoints.addWaypoint(newd Waypoint(wp)); } } } if (!f.isOk()) { warning(wxstr(f.getErrorMessage()).wc_str()); } return true; } bool IOMapOTBM::loadSpawns(Map& map, const FileName& dir) { std::string fn = (const char*)(dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME).mb_str(wxConvUTF8)); fn += map.spawnfile; FileName filename(wxstr(fn)); if (!filename.FileExists()) { warnings.push_back("IOMapOTBM::loadSpawns: File not found."); return false; } // has to be declared again as encoding-specific characters break loading there std::string encoded_path = (const char*)(dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME).mb_str(wxConvWhateverWorks)); encoded_path += map.spawnfile; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(encoded_path.c_str()); if (!result) { warnings.push_back("IOMapOTBM::loadSpawns: File loading error."); return false; } return loadSpawns(map, doc); } bool IOMapOTBM::loadSpawns(Map& map, pugi::xml_document& doc) { pugi::xml_node node = doc.child("spawns"); if (!node) { warnings.push_back("IOMapOTBM::loadSpawns: Invalid rootheader."); return false; } for (pugi::xml_node spawnNode = node.first_child(); spawnNode; spawnNode = spawnNode.next_sibling()) { if (as_lower_str(spawnNode.name()) != "spawn") { continue; } Position spawnPosition; spawnPosition.x = spawnNode.attribute("centerx").as_int(); spawnPosition.y = spawnNode.attribute("centery").as_int(); spawnPosition.z = spawnNode.attribute("centerz").as_int(); if (spawnPosition.x == 0 || spawnPosition.y == 0) { warning("Bad position data on one spawn, discarding..."); continue; } int32_t radius = spawnNode.attribute("radius").as_int(); if (radius < 1) { warning("Couldn't read radius of spawn.. discarding spawn..."); continue; } Tile* tile = map.getTile(spawnPosition); if (tile && tile->spawn) { warning("Duplicate spawn on position %d:%d:%d\n", tile->getX(), tile->getY(), tile->getZ()); continue; } Spawn* spawn = newd Spawn(radius); if (!tile) { tile = map.allocator(map.createTileL(spawnPosition)); map.setTile(spawnPosition, tile); } tile->spawn = spawn; map.addSpawn(tile); for (pugi::xml_node creatureNode = spawnNode.first_child(); creatureNode; creatureNode = creatureNode.next_sibling()) { const std::string& creatureNodeName = as_lower_str(creatureNode.name()); if (creatureNodeName != "monster" && creatureNodeName != "npc") { continue; } bool isNpc = creatureNodeName == "npc"; const std::string& name = creatureNode.attribute("name").as_string(); if (name.empty()) { wxString err; err << "Bad creature position data, discarding creature at spawn " << spawnPosition.x << ":" << spawnPosition.y << ":" << spawnPosition.z << " due missing name."; warnings.Add(err); break; } int32_t spawntime = creatureNode.attribute("spawntime").as_int(); if (spawntime == 0) { spawntime = g_settings.getInteger(Config::DEFAULT_SPAWNTIME); } Direction direction = NORTH; int dir = creatureNode.attribute("direction").as_int(-1); if (dir >= DIRECTION_FIRST && dir <= DIRECTION_LAST) { direction = (Direction)dir; } Position creaturePosition(spawnPosition); pugi::xml_attribute xAttribute = creatureNode.attribute("x"); pugi::xml_attribute yAttribute = creatureNode.attribute("y"); if (!xAttribute || !yAttribute) { wxString err; err << "Bad creature position data, discarding creature \"" << name << "\" at spawn " << creaturePosition.x << ":" << creaturePosition.y << ":" << creaturePosition.z << " due to invalid position."; warnings.Add(err); break; } creaturePosition.x += xAttribute.as_int(); creaturePosition.y += yAttribute.as_int(); radius = std::max(radius, std::abs(creaturePosition.x - spawnPosition.x)); radius = std::max(radius, std::abs(creaturePosition.y - spawnPosition.y)); radius = std::min(radius, g_settings.getInteger(Config::MAX_SPAWN_RADIUS)); Tile* creatureTile; if (creaturePosition == spawnPosition) { creatureTile = tile; } else { creatureTile = map.getTile(creaturePosition); } if (!creatureTile) { wxString err; err << "Discarding creature \"" << name << "\" at " << creaturePosition.x << ":" << creaturePosition.y << ":" << creaturePosition.z << " due to invalid position."; warnings.Add(err); break; } if (creatureTile->creature) { wxString err; err << "Duplicate creature \"" << name << "\" at " << creaturePosition.x << ":" << creaturePosition.y << ":" << creaturePosition.z << " was discarded."; warnings.Add(err); break; } CreatureType* type = g_creatures[name]; if (!type) { type = g_creatures.addMissingCreatureType(name, isNpc); } Creature* creature = newd Creature(type); creature->setDirection(direction); creature->setSpawnTime(spawntime); creatureTile->creature = creature; if (creatureTile->getLocation()->getSpawnCount() == 0) { // No spawn, create a newd one ASSERT(creatureTile->spawn == nullptr); Spawn* spawn = newd Spawn(5); creatureTile->spawn = spawn; map.addSpawn(creatureTile); } } } return true; } bool IOMapOTBM::loadHouses(Map& map, const FileName& dir) { std::string fn = (const char*)(dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME).mb_str(wxConvUTF8)); fn += map.housefile; FileName filename(wxstr(fn)); if (!filename.FileExists()) { warnings.push_back("IOMapOTBM::loadHouses: File not found."); return false; } // has to be declared again as encoding-specific characters break loading there std::string encoded_path = (const char*)(dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME).mb_str(wxConvWhateverWorks)); encoded_path += map.housefile; pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(encoded_path.c_str()); if (!result) { warnings.push_back("IOMapOTBM::loadHouses: File loading error."); return false; } return loadHouses(map, doc); } bool IOMapOTBM::loadHouses(Map& map, pugi::xml_document& doc) { pugi::xml_node node = doc.child("houses"); if (!node) { warnings.push_back("IOMapOTBM::loadHouses: Invalid rootheader."); return false; } pugi::xml_attribute attribute; for (pugi::xml_node houseNode = node.first_child(); houseNode; houseNode = houseNode.next_sibling()) { if (as_lower_str(houseNode.name()) != "house") { continue; } House* house = nullptr; if ((attribute = houseNode.attribute("houseid"))) { house = map.houses.getHouse(attribute.as_uint()); if (!house) { break; } } if ((attribute = houseNode.attribute("name"))) { house->name = attribute.as_string(); } else { house->name = "House #" + std::to_string(house->getID()); } Position exitPosition( houseNode.attribute("entryx").as_int(), houseNode.attribute("entryy").as_int(), houseNode.attribute("entryz").as_int() ); if (exitPosition.x != 0 && exitPosition.y != 0 && exitPosition.z != 0) { house->setExit(exitPosition); } if ((attribute = houseNode.attribute("rent"))) { house->rent = attribute.as_int(); } if ((attribute = houseNode.attribute("guildhall"))) { house->guildhall = attribute.as_bool(); } if ((attribute = houseNode.attribute("townid"))) { house->townid = attribute.as_uint(); } else { warning("House %d has no town! House was removed.", house->getID()); map.houses.removeHouse(house); } } return true; } bool IOMapOTBM::loadWaypoints(Map& map, const FileName& dir) { std::string fn = (const char*)(dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME).mb_str(wxConvUTF8)); fn += map.waypointfile; FileName filename(wxstr(fn)); if (!filename.FileExists()) { return false; } pugi::xml_document doc; pugi::xml_parse_result result = doc.load_file(fn.c_str()); if (!result) { return false; } return loadWaypoints(map, doc); }; bool IOMapOTBM::loadWaypoints(Map& map, pugi::xml_document& doc) { return true; }; bool IOMapOTBM::saveMap(Map& map, const FileName& identifier) { #ifdef OTGZ_SUPPORT if (identifier.GetExt() == "otgz") { // Create the archive struct archive* a = archive_write_new(); struct archive_entry* entry = nullptr; std::ostringstream streamData; archive_write_set_compression_gzip(a); archive_write_set_format_pax_restricted(a); archive_write_open_filename(a, nstr(identifier.GetFullPath()).c_str()); g_gui.SetLoadDone(0, "Saving spawns..."); pugi::xml_document spawnDoc; if (saveSpawns(map, spawnDoc)) { // Write the data spawnDoc.save(streamData, "", pugi::format_raw, pugi::encoding_utf8); std::string xmlData = streamData.str(); // Write to the arhive entry = archive_entry_new(); archive_entry_set_pathname(entry, "world/spawns.xml"); archive_entry_set_size(entry, xmlData.size()); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); // Write to the archive archive_write_header(a, entry); archive_write_data(a, xmlData.data(), xmlData.size()); // Free the entry archive_entry_free(entry); streamData.str(""); } g_gui.SetLoadDone(0, "Saving houses..."); pugi::xml_document houseDoc; if (saveHouses(map, houseDoc)) { // Write the data houseDoc.save(streamData, "", pugi::format_raw, pugi::encoding_utf8); std::string xmlData = streamData.str(); // Write to the arhive entry = archive_entry_new(); archive_entry_set_pathname(entry, "world/houses.xml"); archive_entry_set_size(entry, xmlData.size()); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); // Write to the archive archive_write_header(a, entry); archive_write_data(a, xmlData.data(), xmlData.size()); // Free the entry archive_entry_free(entry); streamData.str(""); } // to do /* g_gui.SetLoadDone(0, "Saving waypoints..."); pugi::xml_document wpDoc; if (saveWaypoints(map, wpDoc)) { // Write the data wpDoc.save(streamData, "", pugi::format_raw, pugi::encoding_utf8); std::string xmlData = streamData.str(); // Write to the arhive entry = archive_entry_new(); archive_entry_set_pathname(entry, "world/waypoints.xml"); archive_entry_set_size(entry, xmlData.size()); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); // Write to the archive archive_write_header(a, entry); archive_write_data(a, xmlData.data(), xmlData.size()); // Free the entry archive_entry_free(entry); streamData.str(""); } */ g_gui.SetLoadDone(0, "Saving OTBM map..."); MemoryNodeFileWriteHandle otbmWriter; saveMap(map, otbmWriter); g_gui.SetLoadDone(75, "Compressing..."); // Create an archive entry for the otbm file entry = archive_entry_new(); archive_entry_set_pathname(entry, "world/map.otbm"); archive_entry_set_size(entry, otbmWriter.getSize() + 4); // 4 bytes extra for header archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); archive_write_header(a, entry); // Write the version header char otbm_identifier[] = "OTBM"; archive_write_data(a, otbm_identifier, 4); // Write the OTBM data archive_write_data(a, otbmWriter.getMemory(), otbmWriter.getSize()); archive_entry_free(entry); // Free / close the archive archive_write_close(a); archive_write_free(a); g_gui.DestroyLoadBar(); return true; } #endif DiskNodeFileWriteHandle f( nstr(identifier.GetFullPath()), (g_settings.getInteger(Config::SAVE_WITH_OTB_MAGIC_NUMBER) ? "OTBM" : std::string(4, '\0')) ); if (!f.isOk()) { error("Can not open file %s for writing", (const char*)identifier.GetFullPath().mb_str(wxConvUTF8)); return false; } if (!saveMap(map, f)) { return false; } g_gui.SetLoadDone(99, "Saving spawns..."); saveSpawns(map, identifier); g_gui.SetLoadDone(99, "Saving houses..."); saveHouses(map, identifier); // to do // g_gui.SetLoadDone(99, "Saving waypoints..."); // saveWaypoints(map, identifier); return true; } bool IOMapOTBM::saveMap(Map& map, NodeFileWriteHandle& f) { /* STOP! * Before you even think about modifying this, please reconsider. * while adding stuff to the binary format may be "cool", you'll * inevitably make it incompatible with any future releases of * the map editor, meaning you cannot reuse your map. Before you * try to modify this, PLEASE consider using an external file * like spawns.xml or houses.xml, as that will be MUCH easier * to port to newer versions of the editor than a custom binary * format. */ bool waypointsWarning = false; const IOMapOTBM& self = *this; FileName tmpName; MapVersion mapVersion = map.getVersion(); f.addNode(0); { f.addU32(mapVersion.otbm); // Version f.addU16(map.width); f.addU16(map.height); f.addU32(g_items.MajorVersion); f.addU32(g_items.MinorVersion); f.addNode(OTBM_MAP_DATA); { f.addByte(OTBM_ATTR_DESCRIPTION); // Neither SimOne's nor OpenTibia cares for additional description tags f.addString("Saved with " + __RME_APPLICATION_NAME__ + " " + __RME_VERSION__); f.addU8(OTBM_ATTR_DESCRIPTION); f.addString(map.description); tmpName.Assign(wxstr(map.spawnfile)); f.addU8(OTBM_ATTR_EXT_SPAWN_FILE); f.addString(nstr(tmpName.GetFullName())); tmpName.Assign(wxstr(map.housefile)); f.addU8(OTBM_ATTR_EXT_HOUSE_FILE); f.addString(nstr(tmpName.GetFullName())); // Start writing tiles uint32_t tiles_saved = 0; bool first = true; int local_x = -1, local_y = -1, local_z = -1; MapIterator map_iterator = map.begin(); while (map_iterator != map.end()) { // Update progressbar ++tiles_saved; if (tiles_saved % 8192 == 0) { g_gui.SetLoadDone(int(tiles_saved / double(map.getTileCount()) * 100.0)); } // Get tile Tile* save_tile = (*map_iterator)->get(); // Is it an empty tile that we can skip? (Leftovers...) if (!save_tile || save_tile->size() == 0) { ++map_iterator; continue; } const Position& pos = save_tile->getPosition(); // Decide if newd node should be created if (pos.x < local_x || pos.x >= local_x + 256 || pos.y < local_y || pos.y >= local_y + 256 || pos.z != local_z) { // End last node if (!first) { f.endNode(); } first = false; // Start newd node f.addNode(OTBM_TILE_AREA); f.addU16(local_x = pos.x & 0xFF00); f.addU16(local_y = pos.y & 0xFF00); f.addU8(local_z = pos.z); } f.addNode(save_tile->isHouseTile() ? OTBM_HOUSETILE : OTBM_TILE); f.addU8(save_tile->getX() & 0xFF); f.addU8(save_tile->getY() & 0xFF); if (save_tile->isHouseTile()) { f.addU32(save_tile->getHouseID()); } if (save_tile->getMapFlags()) { f.addByte(OTBM_ATTR_TILE_FLAGS); f.addU32(save_tile->getMapFlags()); if (save_tile->getMapFlags() & TILESTATE_ZONE_BRUSH) { for (const auto& zoneId : save_tile->getZoneIds()) { f.addU16(zoneId); } f.addU16(0); } } if (save_tile->ground) { Item* ground = save_tile->ground; if (ground->isMetaItem()) { // Do nothing, we don't save metaitems... } else if (ground->hasBorderEquivalent()) { bool found = false; for (Item* item : save_tile->items) { if (item->getGroundEquivalent() == ground->getID()) { // Do nothing // Found equivalent found = true; break; } } if (!found) { ground->serializeItemNode_OTBM(self, f); } } else if (ground->isComplex()) { ground->serializeItemNode_OTBM(self, f); } else { f.addByte(OTBM_ATTR_ITEM); ground->serializeItemCompact_OTBM(self, f); } } for (Item* item : save_tile->items) { if (!item->isMetaItem()) { item->serializeItemNode_OTBM(self, f); } } f.endNode(); ++map_iterator; } // Only close the last node if one has actually been created if (!first) { f.endNode(); } f.addNode(OTBM_TOWNS); for (const auto& townEntry : map.towns) { Town* town = townEntry.second; const Position& townPosition = town->getTemplePosition(); f.addNode(OTBM_TOWN); f.addU32(town->getID()); f.addString(town->getName()); f.addU16(townPosition.x); f.addU16(townPosition.y); f.addU8(townPosition.z); f.endNode(); } f.endNode(); bool supportWaypoints = version.otbm >= MAP_OTBM_3; if (supportWaypoints || map.waypoints.waypoints.size() > 0) { if (!supportWaypoints) { waypointsWarning = true; } f.addNode(OTBM_WAYPOINTS); for (const auto& waypointEntry : map.waypoints) { Waypoint* waypoint = waypointEntry.second; f.addNode(OTBM_WAYPOINT); f.addString(waypoint->name); f.addU16(waypoint->pos.x); f.addU16(waypoint->pos.y); f.addU8(waypoint->pos.z); f.endNode(); } f.endNode(); } } f.endNode(); } f.endNode(); if (waypointsWarning) { g_gui.PopupDialog(g_gui.root, "Warning", "Waypoints were saved, but they are not supported in OTBM 2!\nIf your map fails to load, consider removing all waypoints and saving again.\n\nThis warning can be disabled in file->preferences.", wxOK); } return true; } bool IOMapOTBM::saveSpawns(Map& map, const FileName& dir) { wxString filepath = dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME); filepath += wxString(map.spawnfile.c_str(), wxConvUTF8); // Create the XML file pugi::xml_document doc; if (saveSpawns(map, doc)) { return doc.save_file(filepath.wc_str(), "\t", pugi::format_default, pugi::encoding_utf8); } return false; } bool IOMapOTBM::saveSpawns(Map& map, pugi::xml_document& doc) { pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); if (!decl) { return false; } decl.append_attribute("version") = "1.0"; CreatureList creatureList; pugi::xml_node spawnNodes = doc.append_child("spawns"); for (const auto& spawnPosition : map.spawns) { Tile* tile = map.getTile(spawnPosition); if (tile == nullptr) { continue; } Spawn* spawn = tile->spawn; ASSERT(spawn); pugi::xml_node spawnNode = spawnNodes.append_child("spawn"); spawnNode.append_attribute("centerx") = spawnPosition.x; spawnNode.append_attribute("centery") = spawnPosition.y; spawnNode.append_attribute("centerz") = spawnPosition.z; int32_t radius = spawn->getSize(); spawnNode.append_attribute("radius") = radius; for (int32_t y = -radius; y <= radius; ++y) { for (int32_t x = -radius; x <= radius; ++x) { Tile* creature_tile = map.getTile(spawnPosition + Position(x, y, 0)); if (creature_tile) { Creature* creature = creature_tile->creature; if (creature && !creature->isSaved()) { pugi::xml_node creatureNode = spawnNode.append_child(creature->isNpc() ? "npc" : "monster"); creatureNode.append_attribute("name") = creature->getName().c_str(); creatureNode.append_attribute("x") = x; creatureNode.append_attribute("y") = y; creatureNode.append_attribute("z") = spawnPosition.z; creatureNode.append_attribute("spawntime") = creature->getSpawnTime(); if (creature->getDirection() != NORTH) { creatureNode.append_attribute("direction") = creature->getDirection(); } // Mark as saved creature->save(); creatureList.push_back(creature); } } } } } for (Creature* creature : creatureList) { creature->reset(); } return true; } bool IOMapOTBM::saveHouses(Map& map, const FileName& dir) { wxString filepath = dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME); filepath += wxString(map.housefile.c_str(), wxConvUTF8); // Create the XML file pugi::xml_document doc; if (saveHouses(map, doc)) { return doc.save_file(filepath.wc_str(), "\t", pugi::format_default, pugi::encoding_utf8); } return false; } bool IOMapOTBM::saveHouses(Map& map, pugi::xml_document& doc) { pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); if (!decl) { return false; } decl.append_attribute("version") = "1.0"; pugi::xml_node houseNodes = doc.append_child("houses"); for (const auto& houseEntry : map.houses) { const House* house = houseEntry.second; pugi::xml_node houseNode = houseNodes.append_child("house"); houseNode.append_attribute("name") = house->name.c_str(); houseNode.append_attribute("houseid") = house->getID(); const Position& exitPosition = house->getExit(); houseNode.append_attribute("entryx") = exitPosition.x; houseNode.append_attribute("entryy") = exitPosition.y; houseNode.append_attribute("entryz") = exitPosition.z; houseNode.append_attribute("rent") = house->rent; if (house->guildhall) { houseNode.append_attribute("guildhall") = true; } houseNode.append_attribute("townid") = house->townid; houseNode.append_attribute("size") = static_cast(house->size()); } return true; } bool IOMapOTBM::saveWaypoints(Map& map, const FileName& dir) { wxString filepath = dir.GetPath(wxPATH_GET_SEPARATOR | wxPATH_GET_VOLUME); filepath += wxString(map.waypointfile.c_str(), wxConvUTF8); // Create the XML file pugi::xml_document doc; if (saveWaypoints(map, doc)) { return doc.save_file(filepath.wc_str(), "\t", pugi::format_default, pugi::encoding_utf8); } return false; } bool IOMapOTBM::saveWaypoints(Map& map, pugi::xml_document& doc) { pugi::xml_node decl = doc.prepend_child(pugi::node_declaration); if (!decl) { return false; } decl.append_attribute("version") = "1.0"; pugi::xml_node houseNodes = doc.append_child("waypoints"); for (const auto& houseEntry : map.houses) { const House* house = houseEntry.second; pugi::xml_node houseNode = houseNodes.append_child("waypoint"); houseNode.append_attribute("name") = house->name.c_str(); houseNode.append_attribute("id") = house->getID(); houseNode.append_attribute("icon") = house->getID(); const Position& exitPosition = house->getExit(); houseNode.append_attribute("x") = exitPosition.x; houseNode.append_attribute("y") = exitPosition.y; houseNode.append_attribute("z") = exitPosition.z; houseNode.append_attribute("townid") = house->townid; } return true; }