diff --git a/src/main/java/com/sk89q/worldedit/DoubleArrayList.java b/src/main/java/com/sk89q/worldedit/DoubleArrayList.java
index baf685dfc..f63a16bc7 100644
--- a/src/main/java/com/sk89q/worldedit/DoubleArrayList.java
+++ b/src/main/java/com/sk89q/worldedit/DoubleArrayList.java
@@ -88,8 +88,8 @@ public class DoubleArrayList implements Iterable> {
*
* @return
*/
- public Iterator> iterator() {
- if (isReversed) {
+ public Iterator> iterator(boolean reversed) {
+ if (reversed) {
return new ReverseEntryIterator>(
listA.listIterator(listA.size()),
listB.listIterator(listB.size()));
@@ -100,6 +100,15 @@ public class DoubleArrayList implements Iterable> {
}
}
+ /**
+ * Get an entry set.
+ *
+ * @return
+ */
+ public Iterator> iterator() {
+ return iterator(isReversed);
+ }
+
/**
* Entry iterator.
*
diff --git a/src/main/java/com/sk89q/worldedit/EditSession.java b/src/main/java/com/sk89q/worldedit/EditSession.java
index 4db82aed9..af6e61334 100644
--- a/src/main/java/com/sk89q/worldedit/EditSession.java
+++ b/src/main/java/com/sk89q/worldedit/EditSession.java
@@ -36,6 +36,7 @@ import com.sk89q.worldedit.function.block.BlockReplace;
import com.sk89q.worldedit.function.block.Naturalizer;
import com.sk89q.worldedit.function.generator.GardenPatchGenerator;
import com.sk89q.worldedit.function.mask.*;
+import com.sk89q.worldedit.function.operation.ChangeSetExecutor;
import com.sk89q.worldedit.function.operation.ForwardExtentCopy;
import com.sk89q.worldedit.function.operation.OperationHelper;
import com.sk89q.worldedit.function.operation.OperationQueue;
@@ -43,6 +44,10 @@ import com.sk89q.worldedit.function.pattern.BlockPattern;
import com.sk89q.worldedit.function.pattern.Patterns;
import com.sk89q.worldedit.function.util.RegionOffset;
import com.sk89q.worldedit.function.visitor.*;
+import com.sk89q.worldedit.history.changeset.BlockOptimizedHistory;
+import com.sk89q.worldedit.history.changeset.ChangeSet;
+import com.sk89q.worldedit.history.UndoContext;
+import com.sk89q.worldedit.history.change.BlockChange;
import com.sk89q.worldedit.masks.Mask;
import com.sk89q.worldedit.math.interpolation.Interpolation;
import com.sk89q.worldedit.math.interpolation.KochanekBartelsInterpolation;
@@ -76,27 +81,10 @@ import static com.sk89q.worldedit.regions.Regions.*;
*/
public class EditSession implements Extent {
- /**
- * Random number generator.
- */
- private static Random prng = new Random();
+ private final static Random prng = new Random();
- /**
- * World.
- */
protected LocalWorld world;
-
- /**
- * Stores the original blocks before modification.
- */
- private DoubleArrayList original =
- new DoubleArrayList(true);
-
- /**
- * Stores the current blocks.
- */
- private DoubleArrayList current =
- new DoubleArrayList(false);
+ private final ChangeSet changeSet = new BlockOptimizedHistory();
/**
* Blocks that should be placed before last.
@@ -277,31 +265,35 @@ public class EditSession implements Extent {
}
}
- // if (!original.containsKey(blockPt)) {
- original.put(blockPt, getBlock(pt));
-
- if (maxBlocks != -1 && original.size() > maxBlocks) {
+ if (maxBlocks != -1 && changeSet.size() > maxBlocks) {
throw new MaxChangedBlocksException(maxBlocks);
}
- // }
- current.put(blockPt, block);
+ changeSet.add(new BlockChange(blockPt, getBlock(pt), block));
return smartSetBlock(pt, block);
}
+ /**
+ * Get the underlying {@link ChangeSet}.
+ *
+ * @return the change set
+ */
+ public ChangeSet getChangeSet() {
+ return changeSet;
+ }
+
/**
* Insert a contrived block change into the history.
*
- * @param pt
- * @param existing
- * @param block
+ * @param position the position
+ * @param existing the previous block at that position
+ * @param block the new block
+ * @deprecated Get the change set with {@link #getChangeSet()} and add the change with that
*/
- public void rememberChange(Vector pt, BaseBlock existing, BaseBlock block) {
- BlockVector blockPt = pt.toBlockVector();
-
- original.put(blockPt, existing);
- current.put(pt.toBlockVector(), block);
+ @Deprecated
+ public void rememberChange(Vector position, BaseBlock existing, BaseBlock block) {
+ changeSet.add(new BlockChange(position.toBlockVector(), existing, block));
}
/**
@@ -436,10 +428,9 @@ public class EditSession implements Extent {
* @param sess
*/
public void undo(EditSession sess) {
- for (Map.Entry entry : original) {
- BlockVector pt = entry.getKey();
- sess.smartSetBlock(pt, entry.getValue());
- }
+ UndoContext context = new UndoContext();
+ context.setExtent(sess);
+ OperationHelper.completeBlindly(ChangeSetExecutor.createUndo(changeSet, context));
sess.flushQueue();
}
@@ -449,10 +440,9 @@ public class EditSession implements Extent {
* @param sess
*/
public void redo(EditSession sess) {
- for (Map.Entry entry : current) {
- BlockVector pt = entry.getKey();
- sess.smartSetBlock(pt, entry.getValue());
- }
+ UndoContext context = new UndoContext();
+ context.setExtent(sess);
+ OperationHelper.completeBlindly(ChangeSetExecutor.createRedo(changeSet, context));
sess.flushQueue();
}
@@ -462,7 +452,7 @@ public class EditSession implements Extent {
* @return
*/
public int size() {
- return original.size();
+ return changeSet.size();
}
/**
@@ -648,7 +638,7 @@ public class EditSession implements Extent {
* @return
*/
public int getBlockChangeCount() {
- return original.size();
+ return changeSet.size();
}
/**
diff --git a/src/main/java/com/sk89q/worldedit/function/operation/ChangeSetExecutor.java b/src/main/java/com/sk89q/worldedit/function/operation/ChangeSetExecutor.java
new file mode 100644
index 000000000..bc993810c
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/function/operation/ChangeSetExecutor.java
@@ -0,0 +1,80 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.function.operation;
+
+import com.sk89q.worldedit.WorldEditException;
+import com.sk89q.worldedit.history.change.Change;
+import com.sk89q.worldedit.history.changeset.ChangeSet;
+import com.sk89q.worldedit.history.UndoContext;
+
+import java.util.Iterator;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class ChangeSetExecutor implements Operation {
+
+ public enum Type {UNDO, REDO}
+
+ private final Iterator iterator;
+ private final Type type;
+ private final UndoContext context;
+
+ private ChangeSetExecutor(ChangeSet changeSet, Type type, UndoContext context) {
+ checkNotNull(changeSet);
+ checkNotNull(type);
+ checkNotNull(context);
+
+ this.type = type;
+ this.context = context;
+
+ if (type == Type.UNDO) {
+ iterator = changeSet.backwardIterator();
+ } else {
+ iterator = changeSet.forwardIterator();
+ }
+ }
+
+ @Override
+ public Operation resume() throws WorldEditException {
+ while (iterator.hasNext()) {
+ Change change = iterator.next();
+ if (type == Type.UNDO) {
+ change.undo(context);
+ } else {
+ change.redo(context);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void cancel() {
+ }
+
+ public static ChangeSetExecutor createUndo(ChangeSet changeSet, UndoContext context) {
+ return new ChangeSetExecutor(changeSet, Type.UNDO, context);
+ }
+
+ public static ChangeSetExecutor createRedo(ChangeSet changeSet, UndoContext context) {
+ return new ChangeSetExecutor(changeSet, Type.REDO, context);
+ }
+
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/UndoContext.java b/src/main/java/com/sk89q/worldedit/history/UndoContext.java
new file mode 100644
index 000000000..fbcce2d37
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/UndoContext.java
@@ -0,0 +1,54 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history;
+
+import com.sk89q.worldedit.extent.Extent;
+import com.sk89q.worldedit.history.change.BlockChange;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides context for undo and redo operations.
+ *
+ * For example, {@link BlockChange}s take the {@link Extent} from the
+ * context rather than store a reference to one.
+ */
+public class UndoContext {
+
+ private Extent extent;
+
+ /**
+ * Get the extent set on this context.
+ *
+ * @return an extent or null
+ */
+ public @Nullable Extent getExtent() {
+ return extent;
+ }
+
+ /**
+ * Set the extent on this context.
+ *
+ * @param extent an extent or null
+ */
+ public void setExtent(@Nullable Extent extent) {
+ this.extent = extent;
+ }
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/change/BlockChange.java b/src/main/java/com/sk89q/worldedit/history/change/BlockChange.java
new file mode 100644
index 000000000..91d82acda
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/change/BlockChange.java
@@ -0,0 +1,96 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history.change;
+
+import com.sk89q.worldedit.BlockVector;
+import com.sk89q.worldedit.WorldEditException;
+import com.sk89q.worldedit.blocks.BaseBlock;
+import com.sk89q.worldedit.extent.Extent;
+import com.sk89q.worldedit.history.UndoContext;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Represents a block change that may be undone or replayed.
+ *
+ * This block change does not have an {@link Extent} assigned to it because
+ * one will be taken from the passed {@link UndoContext}. If the context
+ * does not have an extent (it is null), cryptic errors may occur.
+ */
+public class BlockChange implements Change {
+
+ private final BlockVector position;
+ private final BaseBlock previous;
+ private final BaseBlock current;
+
+ /**
+ * Create a new block change.
+ *
+ * @param position the position
+ * @param previous the previous block
+ * @param current the current block
+ */
+ public BlockChange(BlockVector position, BaseBlock previous, BaseBlock current) {
+ checkNotNull(position);
+ checkNotNull(previous);
+ checkNotNull(current);
+ this.position = position;
+ this.previous = previous;
+ this.current = current;
+ }
+
+ /**
+ * Get the position.
+ *
+ * @return the position
+ */
+ public BlockVector getPosition() {
+ return position;
+ }
+
+ /**
+ * Get the previous block.
+ *
+ * @return the previous block
+ */
+ public BaseBlock getPrevious() {
+ return previous;
+ }
+
+ /**
+ * Get the current block.
+ *
+ * @return the current block
+ */
+ public BaseBlock getCurrent() {
+ return current;
+ }
+
+ @Override
+ public void undo(UndoContext context) throws WorldEditException {
+ checkNotNull(context.getExtent()).setBlock(position, previous, true);
+ }
+
+ @Override
+ public void redo(UndoContext context) throws WorldEditException {
+ checkNotNull(context.getExtent()).setBlock(position, current, true);
+ }
+
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/change/Change.java b/src/main/java/com/sk89q/worldedit/history/change/Change.java
new file mode 100644
index 000000000..6f6e121dd
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/change/Change.java
@@ -0,0 +1,49 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history.change;
+
+import com.sk89q.worldedit.WorldEditException;
+import com.sk89q.worldedit.history.changeset.ChangeSet;
+import com.sk89q.worldedit.history.UndoContext;
+
+/**
+ * Describes a change that can be undone or re-applied.
+ */
+public interface Change {
+
+ /**
+ * Perform an undo. This method may not be available if the object
+ * was returned from {@link ChangeSet#forwardIterator()}.
+ *
+ * @param context a context for undo
+ * @throws WorldEditException on an error
+ */
+ void undo(UndoContext context) throws WorldEditException;
+
+ /**
+ * Perform an redo. This method may not be available if the object
+ * was returned from {@link ChangeSet#backwardIterator()} ()}.
+ *
+ * @param context a context for redo
+ * @throws WorldEditException on an error
+ */
+ void redo(UndoContext context) throws WorldEditException;
+
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/changeset/ArrayListHistory.java b/src/main/java/com/sk89q/worldedit/history/changeset/ArrayListHistory.java
new file mode 100644
index 000000000..8338bf946
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/changeset/ArrayListHistory.java
@@ -0,0 +1,59 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history.changeset;
+
+import com.google.common.collect.Lists;
+import com.sk89q.worldedit.history.change.Change;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Stores all {@link Change}s in an {@link ArrayList}.
+ */
+public class ArrayListHistory implements ChangeSet {
+
+ private final List changes = new ArrayList();
+
+ @Override
+ public void add(Change change) {
+ checkNotNull(change);
+ changes.add(change);
+ }
+
+ @Override
+ public Iterator backwardIterator() {
+ return Lists.reverse(changes).iterator();
+ }
+
+ @Override
+ public Iterator forwardIterator() {
+ return changes.iterator();
+ }
+
+ @Override
+ public int size() {
+ return changes.size();
+ }
+
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/changeset/BlockOptimizedHistory.java b/src/main/java/com/sk89q/worldedit/history/changeset/BlockOptimizedHistory.java
new file mode 100644
index 000000000..835818312
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/changeset/BlockOptimizedHistory.java
@@ -0,0 +1,97 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history.changeset;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterators;
+import com.sk89q.worldedit.BlockVector;
+import com.sk89q.worldedit.DoubleArrayList;
+import com.sk89q.worldedit.blocks.BaseBlock;
+import com.sk89q.worldedit.extent.Extent;
+import com.sk89q.worldedit.history.change.BlockChange;
+import com.sk89q.worldedit.history.change.Change;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Map.Entry;
+
+/**
+ * An extension of {@link ArrayListHistory} that stores {@link BlockChange}s
+ * separately in two {@link ArrayList}s.
+ *
+ * Whether this is a good idea or not is highly questionable, but this class
+ * exists because this is how history was implemented in WorldEdit for
+ * many years.
+ */
+public class BlockOptimizedHistory extends ArrayListHistory {
+
+ private final DoubleArrayList previous = new DoubleArrayList(true);
+ private final DoubleArrayList current = new DoubleArrayList(false);
+
+ @Override
+ public void add(Change change) {
+ checkNotNull(change);
+
+ if (change instanceof BlockChange) {
+ BlockChange blockChange = (BlockChange) change;
+ BlockVector position = blockChange.getPosition();
+ previous.put(position, blockChange.getPrevious());
+ current.put(position, blockChange.getCurrent());
+ } else {
+ super.add(change);
+ }
+ }
+
+ @Override
+ public Iterator forwardIterator() {
+ return Iterators.concat(
+ super.forwardIterator(),
+ Iterators.transform(current.iterator(), createTransform()));
+ }
+
+ @Override
+ public Iterator backwardIterator() {
+ return Iterators.concat(
+ super.backwardIterator(),
+ Iterators.transform(previous.iterator(true), createTransform()));
+ }
+
+ @Override
+ public int size() {
+ return super.size() + previous.size();
+ }
+
+ /**
+ * Create a function that transforms each entry from the double array lists' iterator
+ * into an {@link Change}.
+ *
+ * @return a function
+ */
+ private Function, Change> createTransform() {
+ return new Function, Change>() {
+ @Override
+ public Change apply(Entry entry) {
+ return new BlockChange(entry.getKey(), entry.getValue(), entry.getValue());
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java b/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java
new file mode 100644
index 000000000..7b05af4c5
--- /dev/null
+++ b/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java
@@ -0,0 +1,66 @@
+/*
+ * WorldEdit, a Minecraft world manipulation toolkit
+ * Copyright (C) sk89q
+ * Copyright (C) WorldEdit team and contributors
+ *
+ * This program 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.
+ *
+ * This program 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 .
+ */
+
+package com.sk89q.worldedit.history.changeset;
+
+import com.sk89q.worldedit.history.change.Change;
+
+import java.util.Iterator;
+
+/**
+ * Tracks a set of undoable operations and allows their undo and redo. The
+ * entirety of a change set should be undone and redone at once.
+ */
+public interface ChangeSet {
+
+ /**
+ * Add the given change to the history.
+ *
+ * @param change the change
+ */
+ void add(Change change);
+
+ /**
+ * Get a backward directed iterator that can be used for undo.
+ *
+ * The iterator may return the changes out of order, as long as the final
+ * result after all changes have been applied is correct.
+ *
+ * @return a undo directed iterator
+ */
+ Iterator backwardIterator();
+
+ /**
+ * Get a forward directed iterator that can be used for redo.
+ *
+ * The iterator may return the changes out of order, as long as the final
+ * result after all changes have been applied is correct.
+ *
+ * @return a forward directed iterator
+ */
+ Iterator forwardIterator();
+
+ /**
+ * Get the number of stored changes.
+ *
+ * @return the change count
+ */
+ int size();
+
+}