//////////////////////////////////////////////////////////////////////
// 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;
}