1//////////////////////////////////////////////////////////////////////
2// This file is part of Remere's Map Editor
3//////////////////////////////////////////////////////////////////////
4// Remere's Map Editor is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Remere's Map Editor is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//////////////////////////////////////////////////////////////////////
17
18#include "main.h"
19
20#include "gui.h" // loadbar
21
22#include "map.h"
23
24#include <sstream>
25#include "string_utils.h"
26
27Map::Map() :
28 BaseMap(),
29 width(512),
30 height(512),
31 houses(*this),
32 has_changed(false),
33 unnamed(false),
34 waypoints(*this) {
35 // Earliest version possible
36 // Caller is responsible for converting us to proper version
37 mapVersion.otbm = MAP_OTBM_1;
38 mapVersion.client = CLIENT_VERSION_NONE;
39}
40
41Map::~Map() {
42 ////
43}
44
45bool Map::open(const std::string file) {
46 if (file == filename) {
47 return true; // Do not reopen ourselves!
48 }
49
50 tilecount = 0;
51
52 IOMapOTBM maploader(getVersion());
53
54 bool success = maploader.loadMap(*this, wxstr(file));
55
56 mapVersion = maploader.version;
57
58 warnings = maploader.getWarnings();
59
60 if (!success) {
61 error = maploader.getError();
62 return false;
63 }
64
65 has_changed = false;
66
67 wxFileName fn = wxstr(file);
68 filename = fn.GetFullPath().mb_str(wxConvUTF8);
69 name = fn.GetFullName().mb_str(wxConvUTF8);
70
71 // convert(getReplacementMapClassic(), true);
72
73#if 0 // This will create a replacement map out of one of SO's template files
74 std::ofstream out("templateshit.cpp");
75 out << "\tConversionMap replacement_map;\n";
76 int count = 0;
77 out << "\tstd::vector<uint16_t> veckey, vecval;\n\n";
78
79 for(int x = 20; ; x += 2) {
80 int y = 22;
81 Tile* old = getTile(x, y, GROUND_LAYER);
82 if(old) {
83 y -= 2;
84 Tile* new_ = getTile(x, y, GROUND_LAYER);
85 if(new_) {
86 if(old->ground || old->items.size()) {
87 out << "\tvecval.clear();\n";
88 if(new_->ground)
89 out << "\tvecval.push_back(" << new_->ground->getID() << ");\n";
90 for(ItemVector::iterator iter = new_->items.begin(); iter != new_->items.end(); ++iter)
91 out << "\tvecval.push_back(" << (*iter)->getID() << ");\n";
92
93 if(old->ground && old->items.empty()) // Single item
94 out << "\treplacement_map.stm[" << old->ground->getID() << "] = vecval;\n\n";
95 else if(old->ground == nullptr && old->items.size() == 1) // Single item
96 out << "\treplacement_map.stm[" << old->items.front()->getID() << "] = vecval;\n\n";
97 else {
98 // Many items
99 out << "\tveckey.clear();\n";
100 if(old->ground)
101 out << "\tveckey.push_back(" << old->ground->getID() << ");\n";
102 for(ItemVector::iterator iter = old->items.begin(); iter != old->items.end(); ++iter)
103 out << "\tveckey.push_back(" << (*iter)->getID() << ");\n";
104 out << "\tstd::sort(veckey.begin(), veckey.end());\n";
105 out << "\treplacement_map.mtm[veckey] = vecval;\n\n";
106 }
107 }
108 }
109 } else {
110 break;
111 }
112 }
113 out.close();
114#endif
115 return true;
116}
117
118bool Map::convert(MapVersion to, bool showdialog) {
119 if (mapVersion.client == to.client) {
120 // Only OTBM version differs
121 // No changes necessary
122 mapVersion = to;
123 return true;
124 }
125
126 /* TODO
127
128 if(to.otbm == MAP_OTBM_4 && to.client < CLIENT_VERSION_850)
129 return false;
130
131 if(mapVersion.client >= CLIENT_VERSION_760 && to.client < CLIENT_VERSION_760)
132 convert(getReplacementMapFrom760To740(), showdialog);
133
134 if(mapVersion.client < CLIENT_VERSION_810 && to.client >= CLIENT_VERSION_810)
135 convert(getReplacementMapFrom800To810(), showdialog);
136
137 if(mapVersion.client == CLIENT_VERSION_854_BAD && to.client >= CLIENT_VERSION_854)
138 convert(getReplacementMapFrom854To854(), showdialog);
139 */
140 mapVersion = to;
141
142 return true;
143}
144
145bool Map::convert(const ConversionMap& rm, bool showdialog) {
146 if (showdialog) {
147 g_gui.CreateLoadBar("Converting map ...");
148 }
149
150 uint64_t tiles_done = 0;
151 std::vector<uint16_t> id_list;
152
153 // std::ofstream conversions("converted_items.txt");
154
155 for (MapIterator miter = begin(); miter != end(); ++miter) {
156 Tile* tile = (*miter)->get();
157 ASSERT(tile);
158
159 if (tile->size() == 0) {
160 continue;
161 }
162
163 // id_list try MTM conversion
164 id_list.clear();
165
166 if (tile->ground) {
167 id_list.push_back(tile->ground->getID());
168 }
169 for (ItemVector::const_iterator item_iter = tile->items.begin(); item_iter != tile->items.end(); ++item_iter) {
170 if ((*item_iter)->isBorder()) {
171 id_list.push_back((*item_iter)->getID());
172 }
173 }
174
175 std::sort(id_list.begin(), id_list.end());
176
177 ConversionMap::MTM::const_iterator cfmtm = rm.mtm.end();
178
179 while (id_list.size()) {
180 cfmtm = rm.mtm.find(id_list);
181 if (cfmtm != rm.mtm.end()) {
182 break;
183 }
184 id_list.pop_back();
185 }
186
187 // Keep track of how many items have been inserted at the bottom
188 size_t inserted_items = 0;
189
190 if (cfmtm != rm.mtm.end()) {
191 const std::vector<uint16_t>& v = cfmtm->first;
192
193 if (tile->ground && std::find(v.begin(), v.end(), tile->ground->getID()) != v.end()) {
194 delete tile->ground;
195 tile->ground = nullptr;
196 }
197
198 for (ItemVector::iterator item_iter = tile->items.begin(); item_iter != tile->items.end();) {
199 if (std::find(v.begin(), v.end(), (*item_iter)->getID()) != v.end()) {
200 delete *item_iter;
201 item_iter = tile->items.erase(item_iter);
202 } else {
203 ++item_iter;
204 }
205 }
206
207 const std::vector<uint16_t>& new_items = cfmtm->second;
208 for (std::vector<uint16_t>::const_iterator iit = new_items.begin(); iit != new_items.end(); ++iit) {
209 Item* item = Item::Create(*iit);
210 if (item->isGroundTile()) {
211 tile->ground = item;
212 } else {
213 tile->items.insert(tile->items.begin(), item);
214 ++inserted_items;
215 }
216 }
217 }
218
219 if (tile->ground) {
220 ConversionMap::STM::const_iterator cfstm = rm.stm.find(tile->ground->getID());
221 if (cfstm != rm.stm.end()) {
222 uint16_t aid = tile->ground->getActionID();
223 uint16_t uid = tile->ground->getUniqueID();
224 delete tile->ground;
225 tile->ground = nullptr;
226
227 const std::vector<uint16_t>& v = cfstm->second;
228 // conversions << "Converted " << tile->getX() << ":" << tile->getY() << ":" << tile->getZ() << " " << id << " -> ";
229 for (std::vector<uint16_t>::const_iterator iit = v.begin(); iit != v.end(); ++iit) {
230 Item* item = Item::Create(*iit);
231 // conversions << *iit << " ";
232 if (item->isGroundTile()) {
233 item->setActionID(aid);
234 item->setUniqueID(uid);
235 tile->addItem(item);
236 } else {
237 tile->items.insert(tile->items.begin(), item);
238 ++inserted_items;
239 }
240 }
241 // conversions << std::endl;
242 }
243 }
244
245 for (ItemVector::iterator replace_item_iter = tile->items.begin() + inserted_items; replace_item_iter != tile->items.end();) {
246 uint16_t id = (*replace_item_iter)->getID();
247 ConversionMap::STM::const_iterator cf = rm.stm.find(id);
248 if (cf != rm.stm.end()) {
249 // uint16_t aid = (*replace_item_iter)->getActionID();
250 // uint16_t uid = (*replace_item_iter)->getUniqueID();
251 delete *replace_item_iter;
252
253 replace_item_iter = tile->items.erase(replace_item_iter);
254 const std::vector<uint16_t>& v = cf->second;
255 for (std::vector<uint16_t>::const_iterator iit = v.begin(); iit != v.end(); ++iit) {
256 replace_item_iter = tile->items.insert(replace_item_iter, Item::Create(*iit));
257 // conversions << "Converted " << tile->getX() << ":" << tile->getY() << ":" << tile->getZ() << " " << id << " -> " << *iit << std::endl;
258 ++replace_item_iter;
259 }
260 } else {
261 ++replace_item_iter;
262 }
263 }
264
265 ++tiles_done;
266 if (showdialog && tiles_done % 0x10000 == 0) {
267 g_gui.SetLoadDone(int(tiles_done / double(getTileCount()) * 100.0));
268 }
269 }
270
271 if (showdialog) {
272 g_gui.DestroyLoadBar();
273 }
274
275 return true;
276}
277
278void Map::cleanInvalidTiles(bool showdialog) {
279 uint64_t tiles_done = 0;
280 uint64_t removed_count = 0;
281 bool has_invalid_tiles = false;
282
283 // First check if there are any invalid tiles to remove
284 for (MapIterator miter = begin(); miter != end(); ++miter) {
285 Tile* tile = (*miter)->get();
286 if (!tile || tile->size() == 0) {
287 continue;
288 }
289
290 for (ItemVector::iterator item_iter = tile->items.begin(); item_iter != tile->items.end(); ++item_iter) {
291 if (!g_items.typeExists((*item_iter)->getID())) {
292 has_invalid_tiles = true;
293 break;
294 }
295 }
296
297 if (has_invalid_tiles) {
298 break;
299 }
300 }
301
302 if (!has_invalid_tiles) {
303 if (showdialog) {
304 g_gui.SetLoadDone(100);
305 g_gui.PopupDialog("Cleanup Complete", "No invalid tiles found.", wxOK);
306 }
307 return;
308 }
309
310 // Note: We don't create a loading bar here anymore, it should be created by the caller
311
312 for (MapIterator miter = begin(); miter != end(); ++miter) {
313 Tile* tile = (*miter)->get();
314 ASSERT(tile);
315
316 if (tile->size() == 0) {
317 continue;
318 }
319
320 for (ItemVector::iterator item_iter = tile->items.begin(); item_iter != tile->items.end();) {
321 if (g_items.typeExists((*item_iter)->getID())) {
322 ++item_iter;
323 } else {
324 delete *item_iter;
325 item_iter = tile->items.erase(item_iter);
326 ++removed_count;
327 }
328 }
329
330 ++tiles_done;
331 if (showdialog && tiles_done % 0x10000 == 0) {
332 g_gui.SetLoadDone(int(tiles_done / double(getTileCount()) * 100.0));
333 }
334 }
335
336 if (showdialog) {
337 g_gui.SetLoadDone(100);
338 // Note: We don't destroy the loading bar here anymore, it should be destroyed by the caller
339 g_gui.PopupDialog("Cleanup Complete", "Removed " + i2ws(removed_count) + " invalid tiles.", wxOK);
340 }
341}
342
343void Map::convertHouseTiles(uint32_t fromId, uint32_t toId) {
344 g_gui.CreateLoadBar("Converting house tiles...");
345 uint64_t tiles_done = 0;
346
347 for (MapIterator miter = begin(); miter != end(); ++miter) {
348 Tile* tile = (*miter)->get();
349 ASSERT(tile);
350
351 uint32_t houseId = tile->getHouseID();
352 if (houseId == 0 || houseId != fromId) {
353 continue;
354 }
355
356 tile->setHouseID(toId);
357 ++tiles_done;
358 if (tiles_done % 0x10000 == 0) {
359 g_gui.SetLoadDone(int(tiles_done / double(getTileCount()) * 100.0));
360 }
361 }
362
363 g_gui.DestroyLoadBar();
364}
365
366MapVersion Map::getVersion() const {
367 return mapVersion;
368}
369
370bool Map::hasChanged() const {
371 return has_changed;
372}
373
374bool Map::doChange() {
375 bool doupdate = !has_changed;
376 has_changed = true;
377 return doupdate;
378}
379
380bool Map::clearChanges() {
381 bool doupdate = has_changed;
382 has_changed = false;
383 return doupdate;
384}
385
386bool Map::hasFile() const {
387 return filename != "";
388}
389
390void Map::setWidth(int new_width) {
391 if (new_width > 65000) {
392 width = 65000;
393 } else if (new_width < 64) {
394 width = 64;
395 } else {
396 width = new_width;
397 }
398}
399
400void Map::setHeight(int new_height) {
401 if (new_height > 65000) {
402 height = 65000;
403 } else if (new_height < 64) {
404 height = 64;
405 } else {
406 height = new_height;
407 }
408}
409void Map::setMapDescription(const std::string& new_description) {
410 description = new_description;
411}
412
413void Map::setHouseFilename(const std::string& new_housefile) {
414 housefile = new_housefile;
415 unnamed = false;
416}
417
418void Map::setSpawnFilename(const std::string& new_spawnfile) {
419 spawnfile = new_spawnfile;
420 unnamed = false;
421}
422
423bool Map::addSpawn(Tile* tile) {
424 Spawn* spawn = tile->spawn;
425 if (spawn) {
426 int z = tile->getZ();
427 int start_x = tile->getX() - spawn->getSize();
428 int start_y = tile->getY() - spawn->getSize();
429 int end_x = tile->getX() + spawn->getSize();
430 int end_y = tile->getY() + spawn->getSize();
431
432 for (int y = start_y; y <= end_y; ++y) {
433 for (int x = start_x; x <= end_x; ++x) {
434 TileLocation* ctile_loc = createTileL(x, y, z);
435 ctile_loc->increaseSpawnCount();
436 }
437 }
438 spawns.addSpawn(tile);
439 return true;
440 }
441 return false;
442}
443
444void Map::removeSpawnInternal(Tile* tile) {
445 Spawn* spawn = tile->spawn;
446 ASSERT(spawn);
447
448 int z = tile->getZ();
449 int start_x = tile->getX() - spawn->getSize();
450 int start_y = tile->getY() - spawn->getSize();
451 int end_x = tile->getX() + spawn->getSize();
452 int end_y = tile->getY() + spawn->getSize();
453
454 for (int y = start_y; y <= end_y; ++y) {
455 for (int x = start_x; x <= end_x; ++x) {
456 TileLocation* ctile_loc = getTileL(x, y, z);
457 if (ctile_loc != nullptr && ctile_loc->getSpawnCount() > 0) {
458 ctile_loc->decreaseSpawnCount();
459 }
460 }
461 }
462}
463
464void Map::removeSpawn(Tile* tile) {
465 if (tile->spawn) {
466 removeSpawnInternal(tile);
467 spawns.removeSpawn(tile);
468 }
469}
470
471SpawnList Map::getSpawnList(Tile* where) {
472 SpawnList list;
473 TileLocation* tile_loc = where->getLocation();
474 if (tile_loc) {
475 if (tile_loc->getSpawnCount() > 0) {
476 uint32_t found = 0;
477 if (where->spawn) {
478 ++found;
479 list.push_back(where->spawn);
480 }
481
482 // Scans the border tiles in an expanding square around the original spawn
483 int z = where->getZ();
484 int start_x = where->getX() - 1, end_x = where->getX() + 1;
485 int start_y = where->getY() - 1, end_y = where->getY() + 1;
486 while (found != tile_loc->getSpawnCount()) {
487 for (int x = start_x; x <= end_x; ++x) {
488 Tile* tile = getTile(x, start_y, z);
489 if (tile && tile->spawn) {
490 list.push_back(tile->spawn);
491 ++found;
492 }
493 tile = getTile(x, end_y, z);
494 if (tile && tile->spawn) {
495 list.push_back(tile->spawn);
496 ++found;
497 }
498 }
499
500 for (int y = start_y + 1; y < end_y; ++y) {
501 Tile* tile = getTile(start_x, y, z);
502 if (tile && tile->spawn) {
503 list.push_back(tile->spawn);
504 ++found;
505 }
506 tile = getTile(end_x, y, z);
507 if (tile && tile->spawn) {
508 list.push_back(tile->spawn);
509 ++found;
510 }
511 }
512 --start_x, --start_y;
513 ++end_x, ++end_y;
514 }
515 }
516 }
517 return list;
518}
519
520bool Map::exportMinimap(FileName filename, int floor, bool displaydialog) {
521 uint8_t* pic = nullptr;
522
523 try {
524 const int MAX_AREA_SIZE = 2500;
525 std::vector<std::pair<Position, Position>> initial_areas;
526 std::map<uint32_t, bool> processed_tiles;
527
528 // Helper function to get tile key
529 auto getTileKey = [](int x, int y) -> uint32_t {
530 return (uint32_t(x) << 16) | uint32_t(y);
531 };
532
533 // Helper function to check if two areas can be merged within size limits
534 auto canMergeAreas = [MAX_AREA_SIZE](const Position& min1, const Position& max1,
535 const Position& min2, const Position& max2) -> bool {
536 // Calculate combined bounds
537 Position combined_min(
538 std::min(min1.x, min2.x),
539 std::min(min1.y, min2.y),
540 min1.z
541 );
542 Position combined_max(
543 std::max(max1.x, max2.x),
544 std::max(max1.y, max2.y),
545 max1.z
546 );
547
548 // Check if combined area is within limits
549 return (combined_max.x - combined_min.x + 1 <= MAX_AREA_SIZE) &&
550 (combined_max.y - combined_min.y + 1 <= MAX_AREA_SIZE);
551 };
552
553 // Helper function to check if areas are adjacent or close enough to merge
554 auto areAreasNearby = [](const Position& max1, const Position& min2,
555 const Position& min1, const Position& max2) -> bool {
556 const int MERGE_DISTANCE = 1000; // Maximum distance to consider areas for merging
557
558 // Check if areas are within merge distance of each other
559 bool x_overlap = (min1.x <= max2.x + MERGE_DISTANCE) && (max1.x + MERGE_DISTANCE >= min2.x);
560 bool y_overlap = (min1.y <= max2.y + MERGE_DISTANCE) && (max1.y + MERGE_DISTANCE >= min2.y);
561
562 return x_overlap && y_overlap;
563 };
564
565 // First pass - collect all initial areas using the existing flood fill
566 std::vector<Position> tiles_on_floor;
567 for (MapIterator mit = begin(); mit != end(); ++mit) {
568 if ((*mit)->get() == nullptr || (*mit)->empty() || (*mit)->getZ() != floor) {
569 continue;
570 }
571 tiles_on_floor.push_back((*mit)->getPosition());
572 }
573
574 // Sort tiles for efficient processing
575 std::sort(tiles_on_floor.begin(), tiles_on_floor.end(),
576 [](const Position& a, const Position& b) {
577 return a.x < b.x || (a.x == b.x && a.y < b.y);
578 });
579
580 // Collect initial areas using flood fill
581 for (const Position& pos : tiles_on_floor) {
582 uint32_t tile_key = getTileKey(pos.x, pos.y);
583 if (processed_tiles[tile_key]) {
584 continue;
585 }
586
587 Position area_start = pos;
588 Position area_end = pos;
589 std::queue<Position> to_process;
590 to_process.push(pos);
591 processed_tiles[tile_key] = true;
592
593 while (!to_process.empty()) {
594 Position current = to_process.front();
595 to_process.pop();
596
597 if (area_end.x - area_start.x >= MAX_AREA_SIZE ||
598 area_end.y - area_start.y >= MAX_AREA_SIZE) {
599 continue;
600 }
601
602 area_start.x = std::min(area_start.x, current.x);
603 area_start.y = std::min(area_start.y, current.y);
604 area_end.x = std::max(area_end.x, current.x);
605 area_end.y = std::max(area_end.y, current.y);
606
607 for (int dx = -1; dx <= 1; dx++) {
608 for (int dy = -1; dy <= 1; dy++) {
609 if (dx == 0 && dy == 0) continue;
610
611 Position next(current.x + dx, current.y + dy, floor);
612 uint32_t next_key = getTileKey(next.x, next.y);
613
614 if (!processed_tiles[next_key]) {
615 Tile* next_tile = getTile(next);
616 if (next_tile && !next_tile->empty()) {
617 to_process.push(next);
618 processed_tiles[next_key] = true;
619 }
620 }
621 }
622 }
623 }
624
625 // Add the area if it contains content
626 bool has_content = false;
627 for (int y = area_start.y; y <= area_end.y && !has_content; y++) {
628 for (int x = area_start.x; x <= area_end.x && !has_content; x++) {
629 Tile* tile = getTile(x, y, floor);
630 if (tile && !tile->empty()) {
631 has_content = true;
632 }
633 }
634 }
635 if (has_content) {
636 initial_areas.push_back(std::make_pair(area_start, area_end));
637 }
638 }
639
640 // Second pass - merge nearby areas into larger areas while respecting size limits
641 std::vector<std::pair<Position, Position>> merged_areas;
642 std::vector<bool> area_processed(initial_areas.size(), false);
643
644 for (size_t i = 0; i < initial_areas.size(); i++) {
645 if (area_processed[i]) continue;
646
647 Position current_min = initial_areas[i].first;
648 Position current_max = initial_areas[i].second;
649 area_processed[i] = true;
650
651 bool merged;
652 do {
653 merged = false;
654 for (size_t j = 0; j < initial_areas.size(); j++) {
655 if (area_processed[j]) continue;
656
657 const auto& area2 = initial_areas[j];
658 if (canMergeAreas(current_min, current_max, area2.first, area2.second) &&
659 areAreasNearby(current_max, area2.first, current_min, area2.second)) {
660
661 // Merge the areas
662 current_min.x = std::min(current_min.x, area2.first.x);
663 current_min.y = std::min(current_min.y, area2.first.y);
664 current_max.x = std::max(current_max.x, area2.second.x);
665 current_max.y = std::max(current_max.y, area2.second.y);
666
667 area_processed[j] = true;
668 merged = true;
669 }
670 }
671 } while (merged);
672
673 merged_areas.push_back(std::make_pair(current_min, current_max));
674 }
675
676 // No areas found
677 if (merged_areas.empty()) {
678 return true;
679 }
680
681 // Export each merged area
682 int area_count = 0;
683 for (const auto& area : merged_areas) {
684 Position min_pos = area.first;
685 Position max_pos = area.second;
686
687 // Add small padding but respect MAX_AREA_SIZE
688 min_pos.x = std::max(0, min_pos.x - 5);
689 min_pos.y = std::max(0, min_pos.y - 5);
690 max_pos.x = std::min(65535, max_pos.x + 5);
691 max_pos.y = std::min(65535, max_pos.y + 5);
692
693 // Ensure we don't exceed MAX_AREA_SIZE
694 if (max_pos.x - min_pos.x + 1 > MAX_AREA_SIZE) {
695 max_pos.x = min_pos.x + MAX_AREA_SIZE - 1;
696 }
697 if (max_pos.y - min_pos.y + 1 > MAX_AREA_SIZE) {
698 max_pos.y = min_pos.y + MAX_AREA_SIZE - 1;
699 }
700
701 // Calculate dimensions
702 int minimap_width = max_pos.x - min_pos.x + 1;
703 int minimap_height = max_pos.y - min_pos.y + 1;
704
705 // Allocate memory for the bitmap
706 pic = newd uint8_t[minimap_width * minimap_height];
707 memset(pic, 0, minimap_width * minimap_height);
708
709 // Fill the bitmap
710 for (int y = min_pos.y; y <= max_pos.y; y++) {
711 for (int x = min_pos.x; x <= max_pos.x; x++) {
712 Tile* tile = getTile(x, y, floor);
713 if (!tile || tile->empty()) {
714 continue;
715 }
716
717 uint32_t pixelpos = (y - min_pos.y) * minimap_width + (x - min_pos.x);
718 uint8_t& pixel = pic[pixelpos];
719
720 // Get color from items
721 for (ItemVector::const_reverse_iterator item_iter = tile->items.rbegin();
722 item_iter != tile->items.rend(); ++item_iter) {
723 if ((*item_iter)->getMiniMapColor()) {
724 pixel = (*item_iter)->getMiniMapColor();
725 break;
726 }
727 }
728
729 // If no item color, check ground
730 if (pixel == 0 && tile->hasGround()) {
731 pixel = tile->ground->getMiniMapColor();
732 }
733 }
734 }
735
736 // Generate filename for this area
737 wxString base_name = filename.GetFullPath();
738 base_name = base_name.BeforeLast('.');
739 wxString area_filename = wxString::Format("%s_area%d.bmp", base_name, area_count++);
740
741 // Write to file
742 FileWriteHandle fh(nstr(area_filename));
743 if (!fh.isOpen()) {
744 delete[] pic;
745 return false;
746 }
747
748 // Store the magic number
749 fh.addRAW("BM");
750
751 // Store the file size
752 uint32_t file_size = 14 // header
753 + 40 // image data header
754 + 256 * 4 // color palette
755 + ((minimap_width + 3) / 4 * 4) * minimap_height; // pixels
756 fh.addU32(file_size);
757
758 // Two values reserved, must always be 0.
759 fh.addU16(0);
760 fh.addU16(0);
761
762 // Bitmapdata offset
763 fh.addU32(14 + 40 + 256 * 4);
764
765 // Header size
766 fh.addU32(40);
767
768 // Header width/height
769 fh.addU32(minimap_width);
770 fh.addU32(minimap_height);
771
772 // Color planes
773 fh.addU16(1);
774
775 // bits per pixel, OT map format is 8
776 fh.addU16(8);
777
778 // compression type, 0 is no compression
779 fh.addU32(0);
780
781 // image size, 0 is valid if we use no compression
782 fh.addU32(0);
783
784 // horizontal/vertical resolution in pixels / meter
785 fh.addU32(4000);
786 fh.addU32(4000);
787
788 // Number of colors
789 fh.addU32(256);
790 // Important colors, 0 is all
791 fh.addU32(0);
792
793 // Write the color palette
794 for (int i = 0; i < 256; ++i) {
795 fh.addU32(uint32_t(minimap_color[i]));
796 }
797
798 // Bitmap width must be divisible by four, calculate how much padding we need
799 int padding = ((minimap_width & 3) != 0 ? 4 - (minimap_width & 3) : 0);
800 // Bitmap rows are saved in reverse order
801 for (int y = minimap_height - 1; y >= 0; --y) {
802 fh.addRAW(pic + y * minimap_width, minimap_width);
803 for (int i = 0; i < padding; ++i) {
804 fh.addU8(0);
805 }
806 if (displaydialog && y % 100 == 0) {
807 g_gui.SetLoadDone(90 + int((minimap_height - y) / double(minimap_height) * 10.0));
808 }
809 }
810
811 delete[] pic;
812 pic = nullptr;
813 }
814
815 return true;
816 } catch (...) {
817 delete[] pic;
818 throw;
819 }
820}
821
822uint32_t Map::cleanDuplicateItems(const std::vector<std::pair<uint16_t, uint16_t>>& ranges, const PropertyFlags& flags) {
823 uint32_t duplicates_removed = 0;
824 uint32_t tiles_affected = 0;
825
826 // Helper function to check if item is in ranges
827 auto isInRanges = [&ranges](uint16_t id) -> bool {
828 if (ranges.empty()) return true; // If no ranges, check all items
829 for (const auto& range : ranges) {
830 if (id >= range.first && id <= range.second) return true;
831 }
832 return false;
833 };
834
835 // Helper function to compare items considering property flags
836 auto compareItems = [&flags](Item* item1, Item* item2) -> bool {
837 if (item1->getID() != item2->getID()) return false;
838
839 const ItemType& type1 = g_items[item1->getID()];
840 const ItemType& type2 = g_items[item2->getID()];
841
842 // If a flag is true (checked), we treat items with that property as different
843 // even if they have the same value for that property
844 if (flags.ignore_unpassable && (type1.unpassable || type2.unpassable)) return false;
845 if (flags.ignore_unmovable && (type1.moveable || type2.moveable)) return false;
846 if (flags.ignore_block_missiles && (type1.blockMissiles || type2.blockMissiles)) return false;
847 if (flags.ignore_block_pathfinder && (type1.blockPathfinder || type2.blockPathfinder)) return false;
848 if (flags.ignore_readable && (type1.canReadText || type2.canReadText)) return false;
849 if (flags.ignore_writeable && (type1.canWriteText || type2.canWriteText)) return false;
850 if (flags.ignore_pickupable && (type1.pickupable || type2.pickupable)) return false;
851 if (flags.ignore_stackable && (type1.stackable || type2.stackable)) return false;
852 if (flags.ignore_rotatable && (type1.rotable || type2.rotable)) return false;
853 if (flags.ignore_hangable && (type1.isHangable || type2.isHangable)) return false;
854 if (flags.ignore_hook_east && (type1.hookEast || type2.hookEast)) return false;
855 if (flags.ignore_hook_south && (type1.hookSouth || type2.hookSouth)) return false;
856 if (flags.ignore_elevation && (type1.hasElevation || type2.hasElevation)) return false;
857
858 return true;
859 };
860
861 for (MapIterator mit = begin(); mit != end(); ++mit) {
862 Tile* tile = (*mit)->get();
863 if (!tile) continue;
864
865 bool tile_modified = false;
866 std::set<Item*> kept_items; // Track first instances
867
868 // First pass: identify items to keep
869 for (Item* item : tile->items) {
870 if (!isInRanges(item->getID())) continue;
871
872 bool is_duplicate = false;
873 for (Item* kept : kept_items) {
874 if (compareItems(item, kept)) {
875 is_duplicate = true;
876 break;
877 }
878 }
879
880 if (!is_duplicate) {
881 kept_items.insert(item);
882 }
883 }
884
885 // Second pass: remove duplicates
886 auto iit = tile->items.begin();
887 while (iit != tile->items.end()) {
888 Item* item = *iit;
889 if (!isInRanges(item->getID())) {
890 ++iit;
891 continue;
892 }
893
894 bool should_remove = true;
895 for (Item* kept : kept_items) {
896 if (item == kept) {
897 should_remove = false;
898 break;
899 }
900 }
901
902 if (should_remove) {
903 delete item;
904 iit = tile->items.erase(iit);
905 duplicates_removed++;
906 tile_modified = true;
907 } else {
908 ++iit;
909 }
910 }
911
912 if (tile_modified) {
913 tiles_affected++;
914 }
915 }
916
917 return duplicates_removed;
918}
919