////////////////////////////////////////////////////////////////////// // 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 "action.h" #include "settings.h" #include "map.h" #include "editor.h" #include "gui.h" Change::Change() : type(CHANGE_NONE), data(nullptr) { //// } Change::Change(Tile* tile) : type(CHANGE_TILE) { ASSERT(tile); data = tile; } Change* Change::Create(House* house, const Position &position) { Change* change = new Change(); change->type = CHANGE_MOVE_HOUSE_EXIT; change->data = new HouseData { house->id, position }; return change; } Change* Change::Create(Waypoint* waypoint, const Position &position) { Change* change = new Change(); change->type = CHANGE_MOVE_WAYPOINT; change->data = new WaypointData { waypoint->name, position }; return change; } Change::~Change() { clear(); } void Change::clear() { switch (type) { case CHANGE_TILE: ASSERT(data); delete reinterpret_cast(data); break; case CHANGE_MOVE_HOUSE_EXIT: ASSERT(data); delete reinterpret_cast(data); break; case CHANGE_MOVE_WAYPOINT: ASSERT(data); delete reinterpret_cast(data); break; case CHANGE_NONE: break; default: #ifdef __DEBUG_MODE__ if (data) { printf("UNHANDLED CHANGE TYPE! Leak!"); } #endif break; } type = CHANGE_NONE; data = nullptr; } uint32_t Change::memsize() const { uint32_t mem = sizeof(*this); if (type == CHANGE_TILE) { mem += reinterpret_cast(data)->memsize(); } return mem; } Action::Action(Editor &editor, ActionIdentifier ident) : commited(false), editor(editor), type(ident) { } Action::~Action() { for (Change* change : changes) { delete change; } changes.clear(); } size_t Action::approx_memsize() const { uint32_t mem = sizeof(*this); mem += changes.size() * (sizeof(Change) + sizeof(Tile) + sizeof(Item) + 6 /* approx overhead*/); return mem; } size_t Action::memsize() const { uint32_t mem = sizeof(*this); mem += sizeof(Change*) * 3 * changes.size(); for (const Change* change : changes) { if (change && change->getType() == CHANGE_TILE) { mem += reinterpret_cast(change->getData())->memsize(); } } return mem; } void Action::commit(DirtyList* dirty_list) { Map &map = editor.getMap(); Selection &selection = editor.getSelection(); selection.start(Selection::INTERNAL); for (Change* change : changes) { switch (change->getType()) { case CHANGE_TILE: { void** data = &change->data; Tile* new_tile = reinterpret_cast(*data); ASSERT(new_tile); const Position &pos = new_tile->getPosition(); if (editor.IsLiveClient()) { QTreeNode* node = map.getLeaf(pos.x, pos.y); if (!node || !node->isVisible(pos.z > rme::MapGroundLayer)) { change->clear(); continue; } } Tile* old_tile = map.swapTile(pos, new_tile); TileLocation* location = new_tile->getLocation(); // Update other nodes in the network if (editor.IsLiveServer() && dirty_list) { dirty_list->AddPosition(pos.x, pos.y, pos.z); } new_tile->update(); // std::cout << "\tSwitched tile at " << pos.x << ";" << pos.y << ";" << pos.z << " from " << (void*)oldtile << " to " << *data << std::endl; if (new_tile->isSelected()) { selection.addInternal(new_tile); } if (old_tile) { if (new_tile->getHouseID() != old_tile->getHouseID()) { // oooooomggzzz we need to add it to the appropriate house! House* house = map.houses.getHouse(old_tile->getHouseID()); if (house) { house->removeTile(old_tile); } house = map.houses.getHouse(new_tile->getHouseID()); if (house) { house->addTile(new_tile); } } if (old_tile->spawnMonster) { if (new_tile->spawnMonster) { if (*old_tile->spawnMonster != *new_tile->spawnMonster) { map.removeSpawnMonster(old_tile); map.addSpawnMonster(new_tile); } } else { // Monster spawn has been removed editor.getMap().removeSpawnMonster(old_tile); } } else if (new_tile->spawnMonster) { editor.getMap().addSpawnMonster(new_tile); } if (old_tile->spawnNpc) { if (new_tile->spawnNpc) { if (*old_tile->spawnNpc != *new_tile->spawnNpc) { map.removeSpawnNpc(old_tile); map.addSpawnNpc(new_tile); } } else { // SpawnMonster has been removed map.removeSpawnNpc(old_tile); } } else if (new_tile->spawnNpc) { map.addSpawnNpc(new_tile); } // oldtile->update(); if (old_tile->isSelected()) { selection.removeInternal(old_tile); } *data = old_tile; } else { *data = map.allocator(location); if (new_tile->getHouseID() != 0) { // oooooomggzzz we need to add it to the appropriate house! House* house = map.houses.getHouse(new_tile->getHouseID()); if (house) { house->addTile(new_tile); } } if (new_tile->spawnMonster) { map.addSpawnMonster(new_tile); } if (new_tile->spawnNpc) { map.addSpawnNpc(new_tile); } } new_tile->modify(); // Update client dirty list if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { dirty_list->AddChange(change); } break; } case CHANGE_MOVE_HOUSE_EXIT: { HouseData* data = reinterpret_cast(change->data); ASSERT(data); House* house = map.houses.getHouse(data->id); if (house) { const Position &old_pos = house->getExit(); house->setExit(data->position); data->position = old_pos; } break; } case CHANGE_MOVE_WAYPOINT: { WaypointData* data = reinterpret_cast(change->data); ASSERT(data); Waypoint* waypoint = map.waypoints.getWaypoint(data->id); if (waypoint) { TileLocation* old_tile = map.getTileL(waypoint->pos); TileLocation* new_tile = map.getTileL(data->position); if (data->position.isValid() && old_tile && old_tile->getWaypointCount() > 0) { old_tile->decreaseWaypointCount(); } new_tile->increaseWaypointCount(); Position old_pos = waypoint->pos; waypoint->pos = data->position; data->position = old_pos; } break; } default: break; } } selection.finish(Selection::INTERNAL); commited = true; } void Action::undo(DirtyList* dirty_list) { if (changes.empty()) { return; } Map &map = editor.getMap(); Selection &selection = editor.getSelection(); selection.start(Selection::INTERNAL); for (Change* change : changes) { switch (change->getType()) { case CHANGE_TILE: { void** data = &change->data; Tile* old_tile = reinterpret_cast(*data); ASSERT(old_tile); const Position &pos = old_tile->getPosition(); if (editor.IsLiveClient()) { QTreeNode* node = map.getLeaf(pos.x, pos.y); if (!node || !node->isVisible(pos.z > rme::MapGroundLayer)) { // Delete all changes that affect tiles outside our view change->clear(); continue; } } Tile* new_tile = map.swapTile(pos, old_tile); // Update server side change list (for broadcast) if (editor.IsLiveServer() && dirty_list) { dirty_list->AddPosition(pos.x, pos.y, pos.z); } if (old_tile->isSelected()) { selection.addInternal(old_tile); } if (new_tile->isSelected()) { selection.removeInternal(new_tile); } if (new_tile->getHouseID() != old_tile->getHouseID()) { // oooooomggzzz we need to remove it from the appropriate house! House* house = map.houses.getHouse(new_tile->getHouseID()); if (house) { house->removeTile(new_tile); } else { new_tile->setHouse(nullptr); } house = map.houses.getHouse(old_tile->getHouseID()); if (house) { house->addTile(old_tile); } } if (old_tile->spawnMonster) { if (new_tile->spawnMonster) { if (*old_tile->spawnMonster != *new_tile->spawnMonster) { map.removeSpawnMonster(new_tile); map.addSpawnMonster(old_tile); } } else { map.addSpawnMonster(old_tile); } } else if (new_tile->spawnMonster) { map.removeSpawnMonster(new_tile); } if (old_tile->spawnNpc) { if (new_tile->spawnNpc) { if (*old_tile->spawnNpc != *new_tile->spawnNpc) { map.removeSpawnNpc(new_tile); map.addSpawnNpc(old_tile); } } else { map.addSpawnNpc(old_tile); } } else if (new_tile->spawnNpc) { map.removeSpawnNpc(new_tile); } *data = new_tile; // Update client dirty list if (editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { dirty_list->AddChange(change); } break; } case CHANGE_MOVE_HOUSE_EXIT: { HouseData* data = reinterpret_cast(change->data); ASSERT(data); House* house = map.houses.getHouse(data->id); if (house) { const Position &oldpos = house->getExit(); house->setExit(data->position); data->position = oldpos; } break; } case CHANGE_MOVE_WAYPOINT: { WaypointData* data = reinterpret_cast(change->data); ASSERT(data); Waypoint* waypoint = map.waypoints.getWaypoint(data->id); if (waypoint) { TileLocation* old_tile = map.getTileL(waypoint->pos); TileLocation* new_tile = map.getTileL(data->position); if (data->position.isValid() && old_tile && old_tile->getWaypointCount() > 0) { old_tile->decreaseWaypointCount(); } new_tile->increaseWaypointCount(); Position old_pos = waypoint->pos; waypoint->pos = data->position; data->position = old_pos; } break; } default: break; } } selection.finish(Selection::INTERNAL); commited = false; } BatchAction::BatchAction(Editor &editor, ActionIdentifier ident) : editor(editor), timestamp(0), memory_size(0), type(ident) { //// } BatchAction::~BatchAction() { for (Action* action : batch) { delete action; } batch.clear(); } size_t BatchAction::memsize(bool recalc) const { // Expensive operation, only evaluate once (won't change anyways) if (!recalc && memory_size > 0) { return memory_size; } uint32_t mem = sizeof(*this); mem += sizeof(Action*) * 3 * batch.size(); for (const Action* action : batch) { #ifdef __USE_EXACT_MEMSIZE__ mem += action->memsize(); #else // Less exact but MUCH faster mem += action->approx_memsize(); #endif } const_cast(this)->memory_size = mem; return mem; } bool BatchAction::isNoSelection() const noexcept { return type != ACTION_SELECT && type != ACTION_UNSELECT; } void BatchAction::addAction(Action* action) { if (!action) { return; } if (action->empty() || !editor.CanEdit()) { delete action; return; } ASSERT(action->getType() == type); batch.push_back(action); timestamp = time(nullptr); } void BatchAction::addAndCommitAction(Action* action) { if (!action) { return; } if (!editor.CanEdit() || action->empty()) { delete action; return; } action->commit(nullptr); batch.push_back(action); timestamp = time(nullptr); } void BatchAction::commit() { for (Action* action : batch) { if (action && !action->isCommited()) { action->commit(nullptr); } } } void BatchAction::undo() { for (Action* action : std::views::reverse(batch)) { action->undo(nullptr); } } void BatchAction::redo() { for (Action* action : batch) { action->redo(nullptr); } } void BatchAction::merge(BatchAction* other) { batch.insert(batch.end(), other->batch.begin(), other->batch.end()); other->batch.clear(); } ActionQueue::ActionQueue(Editor &editor) : current(0), memory_size(0), editor(editor) { //// } ActionQueue::~ActionQueue() { for (BatchAction* batch : actions) { delete batch; } actions.clear(); } Action* ActionQueue::createAction(ActionIdentifier identifier) const { return new Action(editor, identifier); } Action* ActionQueue::createAction(BatchAction* batch) const { return new Action(editor, batch->getType()); } BatchAction* ActionQueue::createBatch(ActionIdentifier identifier) const { return new BatchAction(editor, identifier); } void ActionQueue::resetTimer() { if (!actions.empty()) { actions.back()->resetTimer(); } } void ActionQueue::addBatch(BatchAction* batch, int stacking_delay) { ASSERT(batch); ASSERT(current <= actions.size()); if (batch->empty()) { delete batch; return; } // Commit any uncommited actions... batch->commit(); // Update title if (batch->isNoSelection() && editor.getMap().doChange()) { g_gui.UpdateTitle(); } if (batch->getType() == ACTION_REMOTE) { delete batch; return; } while (current != actions.size()) { memory_size -= actions.back()->memsize(); BatchAction* todelete = actions.back(); actions.pop_back(); delete todelete; } while (memory_size > size_t(1024 * 1024 * g_settings.getInteger(Config::UNDO_MEM_SIZE)) && !actions.empty()) { memory_size -= actions.front()->memsize(); delete actions.front(); actions.pop_front(); current--; } if (actions.size() > size_t(g_settings.getInteger(Config::UNDO_SIZE)) && !actions.empty()) { memory_size -= actions.front()->memsize(); BatchAction* todelete = actions.front(); actions.pop_front(); delete todelete; current--; } do { if (!actions.empty()) { BatchAction* lastAction = actions.back(); if (lastAction->type == batch->type && g_settings.getInteger(Config::GROUP_ACTIONS) && time(nullptr) - stacking_delay < lastAction->timestamp) { lastAction->merge(batch); lastAction->timestamp = time(nullptr); memory_size -= lastAction->memsize(); memory_size += lastAction->memsize(true); delete batch; break; } } memory_size += batch->memsize(); actions.push_back(batch); batch->timestamp = time(nullptr); current++; } while (false); } void ActionQueue::addAction(Action* action, int stacking_delay) { BatchAction* batch = createBatch(action->getType()); batch->addAndCommitAction(action); if (batch->empty()) { delete batch; return; } addBatch(batch, stacking_delay); } const BatchAction* ActionQueue::getAction(size_t index) const { if (index >= 0 && index < actions.size()) { return actions.at(index); } return nullptr; } void ActionQueue::generateLabels() { for (BatchAction* batch : actions) { if (batch && batch->label.IsEmpty()) { batch->label = createLabel(batch->getType()); } } } bool ActionQueue::undo() { if (current > 0) { current--; BatchAction* batch = actions.at(current); if (batch) { batch->undo(); } // Update title if (batch && batch->isNoSelection() && editor.getMap().doChange()) { g_gui.UpdateTitle(); } return true; } return false; } bool ActionQueue::redo() { if (current < actions.size()) { BatchAction* batch = actions.at(current); if (batch) { batch->redo(); } current++; // Update title if (batch && batch->isNoSelection() && editor.getMap().doChange()) { g_gui.UpdateTitle(); } return true; } return false; } bool ActionQueue::hasChanges() const { for (const BatchAction* batch : actions) { if (batch && !batch->empty() && batch->isNoSelection()) { return true; } } return false; } void ActionQueue::clear() { for (BatchAction* batch : actions) { delete batch; } actions.clear(); current = 0; } wxString ActionQueue::createLabel(ActionIdentifier type) { switch (type) { case ACTION_MOVE: return "Move"; case ACTION_SELECT: return "Select"; case ACTION_UNSELECT: return "Unselect"; case ACTION_DELETE_TILES: return "Delete"; case ACTION_CUT_TILES: return "Cut"; case ACTION_PASTE_TILES: return "Paste"; case ACTION_RANDOMIZE: return "Randomize"; case ACTION_BORDERIZE: return "Borderize"; case ACTION_DRAW: return "Draw"; case ACTION_ERASE: return "Erase"; case ACTION_SWITCHDOOR: return "Switch Door"; case ACTION_ROTATE_ITEM: return "Rotate Item"; case ACTION_REPLACE_ITEMS: return "Replace"; case ACTION_CHANGE_PROPERTIES: return "Change Properties"; case ACTION_LUA_SCRIPT: return "Lua Script"; default: return wxEmptyString; } } void DirtyList::AddPosition(int x, int y, int z) { uint32_t m = ((x >> 2) << 18) | ((y >> 2) << 4); ValueType fi = { m, 0 }; SetType::iterator s = iset.find(fi); if (s != iset.end()) { ValueType v = *s; iset.erase(s); v.floors = (1 << z) | v.floors; iset.insert(v); } else { ValueType v = { m, (uint32_t)(1 << z) }; iset.insert(v); } } void DirtyList::AddChange(Change* c) { ichanges.push_back(c); } DirtyList::SetType &DirtyList::GetPosList() { return iset; } ChangeList &DirtyList::GetChanges() { return ichanges; }