////////////////////////////////////////////////////////////////////// // 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* t) : type(CHANGE_TILE) { ASSERT(t); data = t; } Change* Change::Create(House* house, const Position& where) { Change* c = newd Change(); c->type = CHANGE_MOVE_HOUSE_EXIT; std::pair* p = newd std::pair; p->first = house->id; p->second = where; c->data = p; return c; } Change* Change::Create(Waypoint* wp, const Position& where) { Change* c = newd Change(); c->type = CHANGE_MOVE_WAYPOINT; std::pair* p = newd std::pair; p->first = wp->name; p->second = where; c->data = p; return c; } 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); switch(type) { case CHANGE_TILE: ASSERT(data); mem += reinterpret_cast(data)->memsize(); break; default: break; } return mem; } Action::Action(Editor& editor, ActionIdentifier ident) : commited(false), editor(editor), type(ident) { } Action::~Action() { ChangeList::const_reverse_iterator it = changes.rbegin(); while(it != changes.rend()) { delete *it; ++it; } } 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(); ChangeList::const_iterator it = changes.begin(); while(it != changes.end()) { Change* c = *it; switch(c->type) { case CHANGE_TILE: { ASSERT(c->data); mem += reinterpret_cast(c->data)->memsize(); break; } default: break; } ++it; } return mem; } void Action::commit(DirtyList* dirty_list) { editor.selection.start(Selection::INTERNAL); ChangeList::const_iterator it = changes.begin(); while(it != changes.end()) { Change* c = *it; switch(c->type) { case CHANGE_TILE: { void** data = &c->data; Tile* newtile = reinterpret_cast(*data); ASSERT(newtile); Position pos = newtile->getPosition(); if(editor.IsLiveClient()) { QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y); if(!nd || !nd->isVisible(pos.z > GROUND_LAYER)) { // Delete all changes that affect tiles outside our view c->clear(); ++it; continue; } } Tile* oldtile = editor.map.swapTile(pos, newtile); TileLocation* location = newtile->getLocation(); // Update other nodes in the network if(editor.IsLiveServer() && dirty_list) dirty_list->AddPosition(pos.x, pos.y, pos.z); newtile->update(); //std::cout << "\tSwitched tile at " << pos.x << ";" << pos.y << ";" << pos.z << " from " << (void*)oldtile << " to " << *data << std::endl; if(newtile->isSelected()) editor.selection.addInternal(newtile); if(oldtile) { if(newtile->getHouseID() != oldtile->getHouseID()) { // oooooomggzzz we need to add it to the appropriate house! House* house = editor.map.houses.getHouse(oldtile->getHouseID()); if(house) house->removeTile(oldtile); house = editor.map.houses.getHouse(newtile->getHouseID()); if(house) house->addTile(newtile); } if(oldtile->spawnMonster) { if(newtile->spawnMonster) { if(*oldtile->spawnMonster != *newtile->spawnMonster) { editor.map.removeSpawnMonster(oldtile); editor.map.addSpawnMonster(newtile); } } else { // Monster spawn has been removed editor.map.removeSpawnMonster(oldtile); } } else if(newtile->spawnMonster) { editor.map.addSpawnMonster(newtile); } if(oldtile->spawnNpc) { if(newtile->spawnNpc) { if(*oldtile->spawnNpc != *newtile->spawnNpc) { editor.map.removeSpawnNpc(oldtile); editor.map.addSpawnNpc(newtile); } } else { // SpawnMonster has been removed editor.map.removeSpawnNpc(oldtile); } } else if(newtile->spawnNpc) { editor.map.addSpawnNpc(newtile); } //oldtile->update(); if(oldtile->isSelected()) editor.selection.removeInternal(oldtile); *data = oldtile; } else { *data = editor.map.allocator(location); if(newtile->getHouseID() != 0) { // oooooomggzzz we need to add it to the appropriate house! House* house = editor.map.houses.getHouse(newtile->getHouseID()); if(house) { house->addTile(newtile); } } if(newtile->spawnMonster) editor.map.addSpawnMonster(newtile); if(newtile->spawnNpc) editor.map.addSpawnNpc(newtile); } // Mark the tile as modified newtile->modify(); // Update client dirty list if(editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { // Local action, assemble changes dirty_list->AddChange(c); } break; } case CHANGE_MOVE_HOUSE_EXIT: { std::pair* p = reinterpret_cast* >(c->data); ASSERT(p); House* whathouse = editor.map.houses.getHouse(p->first); if(whathouse) { Position oldpos = whathouse->getExit(); whathouse->setExit(p->second); p->second = oldpos; } break; } case CHANGE_MOVE_WAYPOINT: { std::pair* p = reinterpret_cast* >(c->data); ASSERT(p); Waypoint* wp = editor.map.waypoints.getWaypoint(p->first); if(wp) { // Change the tiles TileLocation* oldtile = editor.map.getTileL(wp->pos); TileLocation* newtile = editor.map.getTileL(p->second); // Only need to remove from old if it actually exists if(p->second != Position()) if(oldtile && oldtile->getWaypointCount() > 0) oldtile->decreaseWaypointCount(); newtile->increaseWaypointCount(); // Update shit Position oldpos = wp->pos; wp->pos = p->second; p->second = oldpos; } break; } default: break; } ++it; } editor.selection.finish(Selection::INTERNAL); commited = true; } void Action::undo(DirtyList* dirty_list) { if(changes.empty()) return; editor.selection.start(Selection::INTERNAL); ChangeList::reverse_iterator it = changes.rbegin(); while(it != changes.rend()) { Change* c = *it; switch(c->type) { case CHANGE_TILE: { void** data = &c->data; Tile* oldtile = reinterpret_cast(*data); ASSERT(oldtile); Position pos = oldtile->getPosition(); if(editor.IsLiveClient()) { QTreeNode* nd = editor.map.getLeaf(pos.x, pos.y); if(!nd || !nd->isVisible(pos.z > GROUND_LAYER)) { // Delete all changes that affect tiles outside our view c->clear(); ++it; continue; } } Tile* newtile = editor.map.swapTile(pos, oldtile); // Update server side change list (for broadcast) if(editor.IsLiveServer() && dirty_list) dirty_list->AddPosition(pos.x, pos.y, pos.z); if(oldtile->isSelected()) editor.selection.addInternal(oldtile); if(newtile->isSelected()) editor.selection.removeInternal(newtile); if(newtile->getHouseID() != oldtile->getHouseID()) { // oooooomggzzz we need to remove it from the appropriate house! House* house = editor.map.houses.getHouse(newtile->getHouseID()); if(house) { house->removeTile(newtile); } else { // Set tile house to 0, house has been removed newtile->setHouse(nullptr); } house = editor.map.houses.getHouse(oldtile->getHouseID()); if(house) { house->addTile(oldtile); } } if(oldtile->spawnMonster) { if(newtile->spawnMonster) { if(*oldtile->spawnMonster != *newtile->spawnMonster) { editor.map.removeSpawnMonster(newtile); editor.map.addSpawnMonster(oldtile); } } else { editor.map.addSpawnMonster(oldtile); } } else if(newtile->spawnMonster) { editor.map.removeSpawnMonster(newtile); } if(oldtile->spawnNpc) { if(newtile->spawnNpc) { if(*oldtile->spawnNpc != *newtile->spawnNpc) { editor.map.removeSpawnNpc(newtile); editor.map.addSpawnNpc(oldtile); } } else { editor.map.addSpawnNpc(oldtile); } } else if(newtile->spawnNpc) { editor.map.removeSpawnNpc(newtile); } *data = newtile; // Update client dirty list if(editor.IsLiveClient() && dirty_list && type != ACTION_REMOTE) { // Local action, assemble changes dirty_list->AddChange(c); } break; } case CHANGE_MOVE_HOUSE_EXIT: { std::pair* p = reinterpret_cast* >(c->data); ASSERT(p); House* whathouse = editor.map.houses.getHouse(p->first); if(whathouse) { Position oldpos = whathouse->getExit(); whathouse->setExit(p->second); p->second = oldpos; } break; } case CHANGE_MOVE_WAYPOINT: { std::pair* p = reinterpret_cast* >(c->data); ASSERT(p); Waypoint* wp = editor.map.waypoints.getWaypoint(p->first); if(wp) { // Change the tiles TileLocation* oldtile = editor.map.getTileL(wp->pos); TileLocation* newtile = editor.map.getTileL(p->second); // Only need to remove from old if it actually exists if(p->second != Position()) if(oldtile && oldtile->getWaypointCount() > 0) oldtile->decreaseWaypointCount(); newtile->increaseWaypointCount(); // Update shit Position oldpos = wp->pos; wp->pos = p->second; p->second = oldpos; } break; } default: break; } ++it; } editor.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(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; } void BatchAction::addAction(Action* action) { // If empty, do nothing. if(action->size() == 0) { delete action; return; } ASSERT(action->getType() == type); if(!editor.CanEdit()) { delete action; return; } // Add it! batch.push_back(action); timestamp = time(nullptr); } void BatchAction::addAndCommitAction(Action* action) { // If empty, do nothing. if(action->size() == 0) { delete action; return; } if(!editor.CanEdit()) { delete action; return; } // Add it! action->commit(nullptr); batch.push_back(action); timestamp = time(nullptr); } void BatchAction::commit() { for(Action* action : batch) { if(!action->isCommited()) { action->commit(nullptr); } } } void BatchAction::undo() { for(Action* action : boost::adaptors::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(auto it = actions.begin(); it != actions.end(); it = actions.erase(it)) { delete *it; } } Action* ActionQueue::createAction(ActionIdentifier ident) { return newd Action(editor, ident); } Action* ActionQueue::createAction(BatchAction* batch) { return newd Action(editor, batch->getType()); } BatchAction* ActionQueue::createBatch(ActionIdentifier ident) { return newd BatchAction(editor, ident); } 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->size() == 0) { delete batch; return; } // Commit any uncommited actions... batch->commit(); // Update title if(editor.map.doChange()) g_gui.UpdateTitle(); if(batch->type == 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->size() == 0) { delete batch; return; } addBatch(batch, stacking_delay); } void ActionQueue::undo() { if(current > 0) { current--; BatchAction* batch = actions[current]; batch->undo(); } } void ActionQueue::redo() { if(current < actions.size()) { BatchAction* batch = actions[current]; batch->redo(); current++; } } void ActionQueue::clear() { for(ActionList::iterator it = actions.begin(); it != actions.end();) { delete *it; it = actions.erase(it); } current = 0; } DirtyList::DirtyList() : owner(0) { ; } DirtyList::~DirtyList() { ; } 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; }