From 10e672a94a37f3faf8e3c607983466f2a9ffc8c4 Mon Sep 17 00:00:00 2001 From: sk89q Date: Sun, 30 Mar 2014 22:33:16 -0700 Subject: [PATCH] Added new history framework, visitors for history. --- .../com/sk89q/worldedit/DoubleArrayList.java | 13 ++- .../java/com/sk89q/worldedit/EditSession.java | 76 +++++++-------- .../function/operation/ChangeSetExecutor.java | 80 +++++++++++++++ .../sk89q/worldedit/history/UndoContext.java | 54 +++++++++++ .../worldedit/history/change/BlockChange.java | 96 ++++++++++++++++++ .../worldedit/history/change/Change.java | 49 ++++++++++ .../history/changeset/ArrayListHistory.java | 59 +++++++++++ .../changeset/BlockOptimizedHistory.java | 97 +++++++++++++++++++ .../history/changeset/ChangeSet.java | 66 +++++++++++++ 9 files changed, 545 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/sk89q/worldedit/function/operation/ChangeSetExecutor.java create mode 100644 src/main/java/com/sk89q/worldedit/history/UndoContext.java create mode 100644 src/main/java/com/sk89q/worldedit/history/change/BlockChange.java create mode 100644 src/main/java/com/sk89q/worldedit/history/change/Change.java create mode 100644 src/main/java/com/sk89q/worldedit/history/changeset/ArrayListHistory.java create mode 100644 src/main/java/com/sk89q/worldedit/history/changeset/BlockOptimizedHistory.java create mode 100644 src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java 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(); + +}