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 "action.h"
21#include "settings.h"
22#include "map.h"
23#include "editor.h"
24#include "gui.h"
25
26// Add necessary includes for exception handling and file operations
27#include <exception>
28#include <fstream>
29#include <wx/filename.h>
30#include <wx/datetime.h>
31#include <wx/stdpaths.h>
32
33Change::Change() :
34 type(CHANGE_NONE), data(nullptr) {
35 ////
36}
37
38Change::Change(Tile* t) :
39 type(CHANGE_TILE) {
40 ASSERT(t);
41 data = t;
42}
43
44Change* Change::Create(House* house, const Position& where) {
45 Change* c = newd Change();
46 c->type = CHANGE_MOVE_HOUSE_EXIT;
47 std::pair<uint32_t, Position>* p = newd std::pair<uint32_t, Position>;
48 p->first = house->getID();
49 p->second = where;
50 c->data = p;
51 return c;
52}
53
54Change* Change::Create(Waypoint* wp, const Position& where) {
55 Change* c = newd Change();
56 c->type = CHANGE_MOVE_WAYPOINT;
57 std::pair<std::string, Position>* p = newd std::pair<std::string, Position>;
58 p->first = wp->name;
59 p->second = where;
60 c->data = p;
61 return c;
62}
63
64Change::~Change() {
65 clear();
66}
67
68void Change::clear() {
69 switch (type) {
70 case CHANGE_TILE:
71 ASSERT(data);
72 delete reinterpret_cast<Tile*>(data);
73 break;
74 case CHANGE_MOVE_HOUSE_EXIT:
75 ASSERT(data);
76 delete reinterpret_cast<std::pair<uint32_t, Position>*>(data);
77 break;
78 case CHANGE_MOVE_WAYPOINT:
79 ASSERT(data);
80 delete reinterpret_cast<std::pair<std::string, Position>*>(data);
81 break;
82 case CHANGE_NONE:
83 break;
84 default:
85#ifdef __DEBUG_MODE__
86 if (data) {
87 printf("UNHANDLED CHANGE TYPE! Leak!");
88 }
89#endif
90 break;
91 }
92 type = CHANGE_NONE;
93 data = nullptr;
94}
95
96uint32_t Change::memsize() const {
97 uint32_t mem = sizeof(*this);
98 switch (type) {
99 case CHANGE_TILE:
100 ASSERT(data);
101 mem += reinterpret_cast<Tile*>(data)->memsize();
102 break;
103 default:
104 break;
105 }
106 return mem;
107}
108
109Action::Action(Editor& editor, ActionIdentifier ident) :
110 commited(false),
111 editor(editor),
112 type(ident) {
113}
114
115Action::~Action() {
116 ChangeList::const_reverse_iterator it = changes.rbegin();
117 while (it != changes.rend()) {
118 delete *it;
119 ++it;
120 }
121}
122
123size_t Action::approx_memsize() const {
124 uint32_t mem = sizeof(*this);
125 mem += changes.size() * (sizeof(Change) + sizeof(Tile) + sizeof(Item) + 6 /* approx overhead*/);
126 return mem;
127}
128
129size_t Action::memsize() const {
130 uint32_t mem = sizeof(*this);
131 mem += sizeof(Change*) * 3 * changes.size();
132 ChangeList::const_iterator it = changes.begin();
133 while (it != changes.end()) {
134 Change* c = *it;
135 switch (c->type) {
136 case CHANGE_TILE: {
137 ASSERT(c->data);
138 mem += reinterpret_cast<Tile*>(c->data)->memsize();
139 break;
140 }
141
142 default:
143 break;
144 }
145 ++it;
146 }
147 return mem;
148}
149
150void Action::commit(DirtyList* dirty_list) {
151 editor.selection.start(Selection::INTERNAL);
152 ChangeList::const_iterator it = changes.begin();
153 while (it != changes.end()) {
154 Change* c = *it;
155 switch (c->type) {
156 case CHANGE_TILE: {
157 void** data = &c->data;
158 Tile* newtile = reinterpret_cast<Tile*>(*data);
159 ASSERT(newtile);
160 Position pos = newtile->getPosition();
161
162 if (editor.IsLiveClient()) {
163 QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y);
164 if (!nd || !nd->isVisible(pos.z > GROUND_LAYER)) {
165 // Delete all changes that affect tiles outside our view
166 c->clear();
167 ++it;
168 continue;
169 }
170 }
171
172 Tile* oldtile = editor.map.swapTile(pos, newtile);
173 TileLocation* location = newtile->getLocation();
174
175 // Update other nodes in the network
176 if (editor.IsLiveServer() && dirty_list) {
177 dirty_list->AddPosition(pos.x, pos.y, pos.z);
178 }
179
180 newtile->update();
181
182 // std::cout << "\tSwitched tile at " << pos.x << ";" << pos.y << ";" << pos.z << " from " << (void*)oldtile << " to " << *data << std::endl;
183 if (newtile->isSelected()) {
184 editor.selection.addInternal(newtile);
185 }
186
187 if (oldtile) {
188 if (newtile->getHouseID() != oldtile->getHouseID()) {
189 // oooooomggzzz we need to add it to the appropriate house!
190 House* house = editor.map.houses.getHouse(oldtile->getHouseID());
191 if (house) {
192 house->removeTile(oldtile);
193 }
194
195 house = editor.map.houses.getHouse(newtile->getHouseID());
196 if (house) {
197 house->addTile(newtile);
198 }
199 }
200 if (oldtile->spawn) {
201 if (newtile->spawn) {
202 if (*oldtile->spawn != *newtile->spawn) {
203 editor.map.removeSpawn(oldtile);
204 editor.map.addSpawn(newtile);
205 }
206 } else {
207 // Spawn has been removed
208 editor.map.removeSpawn(oldtile);
209 }
210 } else if (newtile->spawn) {
211 editor.map.addSpawn(newtile);
212 }
213
214 // oldtile->update();
215 if (oldtile->isSelected()) {
216 editor.selection.removeInternal(oldtile);
217 }
218
219 *data = oldtile;
220 } else {
221 *data = editor.map.allocator(location);
222 if (newtile->getHouseID() != 0) {
223 // oooooomggzzz we need to add it to the appropriate house!
224 House* house = editor.map.houses.getHouse(newtile->getHouseID());
225 if (house) {
226 house->addTile(newtile);
227 }
228 }
229
230 if (newtile->spawn) {
231 editor.map.addSpawn(newtile);
232 }
233 }
234 // Mark the tile as modified
235 newtile->modify();
236
237 // Update client dirty list
238 if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) {
239 // Local action, assemble changes
240 dirty_list->AddChange(c);
241 }
242 break;
243 }
244
245 case CHANGE_MOVE_HOUSE_EXIT: {
246 std::pair<uint32_t, Position>* p = reinterpret_cast<std::pair<uint32_t, Position>*>(c->data);
247 ASSERT(p);
248 House* whathouse = editor.map.houses.getHouse(p->first);
249
250 if (whathouse) {
251 Position oldpos = whathouse->getExit();
252 whathouse->setExit(p->second);
253 p->second = oldpos;
254 }
255 break;
256 }
257
258 case CHANGE_MOVE_WAYPOINT: {
259 std::pair<std::string, Position>* p = reinterpret_cast<std::pair<std::string, Position>*>(c->data);
260 ASSERT(p);
261 Waypoint* wp = editor.map.waypoints.getWaypoint(p->first);
262
263 if (wp) {
264 // Change the tiles
265 TileLocation* oldtile = editor.map.getTileL(wp->pos);
266 TileLocation* newtile = editor.map.getTileL(p->second);
267
268 // Only need to remove from old if it actually exists
269 if (p->second != Position()) {
270 if (oldtile && oldtile->getWaypointCount() > 0) {
271 oldtile->decreaseWaypointCount();
272 }
273 }
274
275 newtile->increaseWaypointCount();
276
277 // Update shit
278 Position oldpos = wp->pos;
279 wp->pos = p->second;
280 p->second = oldpos;
281 }
282 break;
283 }
284
285 default:
286 break;
287 }
288 ++it;
289 }
290 editor.selection.finish(Selection::INTERNAL);
291 commited = true;
292}
293
294void Action::undo(DirtyList* dirty_list) {
295 if (changes.empty()) {
296 return;
297 }
298
299 editor.selection.start(Selection::INTERNAL);
300 ChangeList::reverse_iterator it = changes.rbegin();
301
302 while (it != changes.rend()) {
303 Change* c = *it;
304 switch (c->type) {
305 case CHANGE_TILE: {
306 void** data = &c->data;
307 Tile* oldtile = reinterpret_cast<Tile*>(*data);
308 ASSERT(oldtile);
309 Position pos = oldtile->getPosition();
310
311 if (editor.IsLiveClient()) {
312 QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y);
313 if (!nd || !nd->isVisible(pos.z > GROUND_LAYER)) {
314 // Delete all changes that affect tiles outside our view
315 c->clear();
316 ++it;
317 continue;
318 }
319 }
320
321 Tile* newtile = editor.map.swapTile(pos, oldtile);
322
323 // Update server side change list (for broadcast)
324 if (editor.IsLiveServer() && dirty_list) {
325 dirty_list->AddPosition(pos.x, pos.y, pos.z);
326 }
327
328 if (oldtile->isSelected()) {
329 editor.selection.addInternal(oldtile);
330 }
331 if (newtile->isSelected()) {
332 editor.selection.removeInternal(newtile);
333 }
334
335 if (newtile->getHouseID() != oldtile->getHouseID()) {
336 // oooooomggzzz we need to remove it from the appropriate house!
337 House* house = editor.map.houses.getHouse(newtile->getHouseID());
338 if (house) {
339 house->removeTile(newtile);
340 } else {
341 // Set tile house to 0, house has been removed
342 newtile->setHouse(nullptr);
343 }
344
345 house = editor.map.houses.getHouse(oldtile->getHouseID());
346 if (house) {
347 house->addTile(oldtile);
348 }
349 }
350
351 if (oldtile->spawn) {
352 if (newtile->spawn) {
353 if (*oldtile->spawn != *newtile->spawn) {
354 editor.map.removeSpawn(newtile);
355 editor.map.addSpawn(oldtile);
356 }
357 } else {
358 editor.map.addSpawn(oldtile);
359 }
360 } else if (newtile->spawn) {
361 editor.map.removeSpawn(newtile);
362 }
363 *data = newtile;
364
365 // Update client dirty list
366 if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) {
367 // Local action, assemble changes
368 dirty_list->AddChange(c);
369 }
370 break;
371 }
372
373 case CHANGE_MOVE_HOUSE_EXIT: {
374 std::pair<uint32_t, Position>* p = reinterpret_cast<std::pair<uint32_t, Position>*>(c->data);
375 ASSERT(p);
376 House* whathouse = editor.map.houses.getHouse(p->first);
377 if (whathouse) {
378 Position oldpos = whathouse->getExit();
379 whathouse->setExit(p->second);
380 p->second = oldpos;
381 }
382 break;
383 }
384
385 case CHANGE_MOVE_WAYPOINT: {
386 std::pair<std::string, Position>* p = reinterpret_cast<std::pair<std::string, Position>*>(c->data);
387 ASSERT(p);
388 Waypoint* wp = editor.map.waypoints.getWaypoint(p->first);
389
390 if (wp) {
391 // Change the tiles
392 TileLocation* oldtile = editor.map.getTileL(wp->pos);
393 TileLocation* newtile = editor.map.getTileL(p->second);
394
395 // Only need to remove from old if it actually exists
396 if (p->second != Position()) {
397 if (oldtile && oldtile->getWaypointCount() > 0) {
398 oldtile->decreaseWaypointCount();
399 }
400 }
401
402 if (newtile) {
403 newtile->increaseWaypointCount();
404 }
405
406 // Update shit
407 Position oldpos = wp->pos;
408 wp->pos = p->second;
409 p->second = oldpos;
410 }
411 break;
412 }
413
414 default:
415 break;
416 }
417 ++it;
418 }
419 editor.selection.finish(Selection::INTERNAL);
420 commited = false;
421}
422
423BatchAction::BatchAction(Editor& editor, ActionIdentifier ident) :
424 editor(editor),
425 timestamp(0),
426 memory_size(0),
427 type(ident) {
428 ////
429}
430
431BatchAction::~BatchAction() {
432 for (Action* action : batch) {
433 delete action;
434 }
435 batch.clear();
436}
437
438size_t BatchAction::memsize(bool recalc) const {
439 // Expensive operation, only evaluate once (won't change anyways)
440 if (!recalc && memory_size > 0) {
441 return memory_size;
442 }
443
444 uint32_t mem = sizeof(*this);
445 mem += sizeof(Action*) * 3 * batch.size();
446
447 for (Action* action : batch) {
448#ifdef __USE_EXACT_MEMSIZE__
449 mem += action->memsize();
450#else
451 // Less exact but MUCH faster
452 mem += action->approx_memsize();
453#endif
454 }
455
456 const_cast<BatchAction*>(this)->memory_size = mem;
457 return mem;
458}
459
460void BatchAction::addAction(Action* action) {
461 // If empty, do nothing.
462 if (action->size() == 0) {
463 delete action;
464 return;
465 }
466
467 ASSERT(action->getType() == type);
468
469 if (!editor.CanEdit()) {
470 delete action;
471 return;
472 }
473
474 // Add it!
475 batch.push_back(action);
476 timestamp = time(nullptr);
477}
478
479void BatchAction::addAndCommitAction(Action* action) {
480 // If empty, do nothing.
481 if (action->size() == 0) {
482 delete action;
483 return;
484 }
485
486 if (!editor.CanEdit()) {
487 delete action;
488 return;
489 }
490
491 // Add it!
492 action->commit(nullptr);
493 batch.push_back(action);
494 timestamp = time(nullptr);
495}
496
497void BatchAction::commit() {
498 for (Action* action : batch) {
499 if (!action->isCommited()) {
500 action->commit(nullptr);
501 }
502 }
503}
504
505void BatchAction::undo() {
506 for (Action* action : boost::adaptors::reverse(batch)) {
507 action->undo(nullptr);
508 }
509}
510
511void BatchAction::redo() {
512 for (Action* action : batch) {
513 action->redo(nullptr);
514 }
515}
516
517void BatchAction::merge(BatchAction* other) {
518 batch.insert(batch.end(), other->batch.begin(), other->batch.end());
519 other->batch.clear();
520}
521
522ActionQueue::ActionQueue(Editor& editor) :
523 current(0), memory_size(0), editor(editor) {
524 ////
525}
526
527ActionQueue::~ActionQueue() {
528 for (auto it = actions.begin(); it != actions.end(); /* no increment here */) {
529 delete *it;
530 it = actions.erase(it); // erase returns the next valid iterator
531 }
532}
533
534Action* ActionQueue::createAction(ActionIdentifier ident) {
535 return newd Action(editor, ident);
536}
537
538Action* ActionQueue::createAction(BatchAction* batch) {
539 return newd Action(editor, batch->getType());
540}
541
542BatchAction* ActionQueue::createBatch(ActionIdentifier ident) {
543 return newd BatchAction(editor, ident);
544}
545
546void ActionQueue::resetTimer() {
547 if (!actions.empty()) {
548 actions.back()->resetTimer();
549 }
550}
551
552void ActionQueue::addAction(Action* action, int stacking_delay) {
553 // Ensure we're on the main thread
554 if (!wxThread::IsMain()) {
555 wxTheApp->CallAfter([=]() {
556 this->addAction(action, stacking_delay);
557 });
558 return;
559 }
560
561 BatchAction* batch = createBatch(action->getType());
562 batch->addAndCommitAction(action);
563 if (batch->size() == 0) {
564 delete batch;
565 return;
566 }
567
568 addBatch(batch, stacking_delay);
569}
570
571void ActionQueue::addBatch(BatchAction* batch, int stacking_delay) {
572 // Ensure we're on the main thread
573 if (!wxThread::IsMain()) {
574 // Use CallAfter but with a 'copy' of needed parameters
575 // to prevent race conditions
576 ActionQueue* self = this;
577 int delay = stacking_delay;
578 wxTheApp->CallAfter([self, batch, delay]() {
579 self->addBatch(batch, delay);
580 });
581 return;
582 }
583
584 // Safety check - if batch is null, just return
585 if (!batch) {
586 return;
587 }
588
589 // Additional safety check to catch any potential crashes
590 try {
591 ASSERT(current <= actions.size());
592
593 if (batch->size() == 0) {
594 delete batch;
595 return;
596 }
597
598 // Commit any uncommited actions...
599 batch->commit();
600
601 // Update title
602 if (editor.map.doChange()) {
603 // Use a safer version that doesn't trigger UI updates
604 // during the first drawing operation
605 static bool isFirstOperation = true;
606 if (!isFirstOperation) {
607 g_gui.UpdateTitle();
608 } else {
609 isFirstOperation = false;
610 }
611 }
612
613 if (batch->type == ACTION_REMOTE) {
614 delete batch;
615 return;
616 }
617
618 // Protect against memory corruption
619 while (current < actions.size() && !actions.empty()) {
620 try {
621 memory_size -= actions.back()->memsize();
622 BatchAction* todelete = actions.back();
623 actions.pop_back();
624 delete todelete;
625 } catch (const std::exception& e) {
626 // Log error but continue processing
627 std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app);
628 if (logFile.is_open()) {
629 wxDateTime now = wxDateTime::Now();
630 logFile << now.FormatISOCombined() << ": Error clearing action: " << e.what() << "\n";
631 logFile.close();
632 }
633 break;
634 }
635 }
636
637 // Safely manage memory
638 try {
639 while (memory_size > size_t(1024 * 1024 * g_settings.getInteger(Config::UNDO_MEM_SIZE)) && !actions.empty()) {
640 memory_size -= actions.front()->memsize();
641 delete actions.front();
642 actions.pop_front();
643 if (current > 0) {
644 current--;
645 }
646 }
647
648 if (actions.size() > size_t(g_settings.getInteger(Config::UNDO_SIZE)) && !actions.empty()) {
649 memory_size -= actions.front()->memsize();
650 BatchAction* todelete = actions.front();
651 actions.pop_front();
652 delete todelete;
653 if (current > 0) {
654 current--;
655 }
656 }
657
658 // Process action with additional safety
659 do {
660 if (!actions.empty()) {
661 BatchAction* lastAction = actions.back();
662 if (lastAction->type == batch->type && g_settings.getInteger(Config::GROUP_ACTIONS) && time(nullptr) - stacking_delay < lastAction->timestamp) {
663 lastAction->merge(batch);
664 lastAction->timestamp = time(nullptr);
665 memory_size -= lastAction->memsize();
666 memory_size += lastAction->memsize(true);
667 delete batch;
668 break;
669 }
670 }
671 memory_size += batch->memsize();
672 actions.push_back(batch);
673 batch->timestamp = time(nullptr);
674 current++;
675 } while (false);
676 } catch (const std::exception& e) {
677 // Log error but don't crash
678 std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app);
679 if (logFile.is_open()) {
680 wxDateTime now = wxDateTime::Now();
681 logFile << now.FormatISOCombined() << ": Error processing batch: " << e.what() << "\n";
682 logFile.close();
683 }
684 }
685 } catch (const std::exception& e) {
686 // Last resort error handler for entire function
687 std::ofstream logFile((wxStandardPaths::Get().GetUserDataDir() + wxFileName::GetPathSeparator() + "action_error.log").ToStdString(), std::ios::app);
688 if (logFile.is_open()) {
689 wxDateTime now = wxDateTime::Now();
690 logFile << now.FormatISOCombined() << ": Critical error in addBatch: " << e.what() << "\n";
691 logFile.close();
692 }
693 }
694}
695
696void ActionQueue::undo() {
697 if (current > 0) {
698 current--;
699 BatchAction* batch = actions[current];
700 batch->undo();
701 }
702}
703
704void ActionQueue::redo() {
705 if (current < actions.size()) {
706 BatchAction* batch = actions[current];
707 batch->redo();
708 current++;
709 }
710}
711
712void ActionQueue::clear() {
713 for (ActionList::iterator it = actions.begin(); it != actions.end();) {
714 delete *it;
715 it = actions.erase(it);
716 }
717 current = 0;
718}
719
720DirtyList::DirtyList() :
721 owner(0) {
722 ;
723}
724
725DirtyList::~DirtyList() {
726 ;
727}
728
729void DirtyList::AddPosition(int x, int y, int z) {
730 uint32_t m = ((x >> 2) << 18) | ((y >> 2) << 4);
731 ValueType fi = { m, 0 };
732 SetType::iterator s = iset.find(fi);
733 if (s != iset.end()) {
734 ValueType v = *s;
735 iset.erase(s);
736 v.floors = (1 << z) | v.floors;
737 iset.insert(v);
738 } else {
739 ValueType v = { m, (uint32_t)(1 << z) };
740 iset.insert(v);
741 }
742}
743
744void DirtyList::AddChange(Change* c) {
745 ichanges.push_back(c);
746}
747
748DirtyList::SetType& DirtyList::GetPosList() {
749 return iset;
750}
751
752ChangeList& DirtyList::GetChanges() {
753 return ichanges;
754}
755