···55import util.EvictingStack;
66import util.Logger;
7788+/**
99+ * Manages undo and redo operations for editor commands in a thread-safe manner.
1010+ * <p>
1111+ * This class schedules command execution outside the critical section of an editor's main loop.
1212+ * It ensures that any undoable editor state remains immutable during the main loop by using
1313+ * a lock to synchronize access.
1414+ */
815public class ThreadSafeCommandManager
916{
1017 private EvictingStack<AbstractCommand> undoStack;
1118 private Stack<AbstractCommand> redoStack;
12191320 private final Object modifyLock;
2121+1422 private final Runnable modifyCallback;
15232424+ /**
2525+ * Creates a new {@code ThreadSafeCommandManager} with a specified undo limit.
2626+ *
2727+ * @param undoLimit the maximum number of commands to keep in the undo stack
2828+ * @param modifyLock the lock object to synchronize command execution
2929+ * @param modifyCallback the callback to notify when a command modifies the editor state
3030+ */
1631 public ThreadSafeCommandManager(int undoLimit, Object modifyLock, Runnable modifyCallback)
1732 {
1833 this.modifyLock = modifyLock;
···2237 redoStack = new Stack<>();
2338 }
24394040+ /**
4141+ * Sets the maximum number of commands that can be stored in the undo stack.
4242+ * When the limit is exceeded, the oldest commands are discarded.
4343+ *
4444+ * @param undoLimit the new maximum size of the undo stack
4545+ */
2546 public void setUndoLimit(int undoLimit)
2647 {
2748 undoStack.setCapacity(undoLimit);
2849 }
29505151+ /**
5252+ * Executes a command and adds it to the undo stack.
5353+ * <p>
5454+ * If the command modifies the editor state, the modify callback is triggered.
5555+ * The redo stack is cleared upon execution.
5656+ *
5757+ * @param cmd the command to execute
5858+ */
3059 public void executeCommand(AbstractCommand cmd)
3160 {
3261 if (!cmd.shouldExec())
···4372 }
4473 }
45744646- public void action_Undo()
7575+ /**
7676+ * Pushes a command onto the undo stack without executing it.
7777+ * <p>
7878+ * This is useful for commands that are undoable but do not require an initial execution.
7979+ * The redo stack is cleared when a new command is pushed.
8080+ *
8181+ * @param cmd the command to push onto the undo stack
8282+ */
8383+ public void pushCommand(AbstractCommand cmd)
8484+ {
8585+ synchronized (modifyLock) {
8686+ undoStack.push(cmd);
8787+ redoStack.clear();
8888+ }
8989+ }
9090+9191+ /**
9292+ * Undoes the last executed command.
9393+ * <p>
9494+ * The command will then be available to redo.
9595+ */
9696+ public void undo()
4797 {
4898 if (undoStack.size() > 0) {
4999 synchronized (modifyLock) {
···57107 }
58108 }
591096060- public void action_Redo()
110110+ /**
111111+ * Redoes the last undone command.
112112+ * <p>
113113+ * The command will then be available to undo.
114114+ */
115115+ public void redo()
61116 {
62117 if (redoStack.size() > 0) {
63118 synchronized (modifyLock) {
···71126 }
72127 }
73128129129+ /**
130130+ * Clears both the undo and redo stacks.
131131+ * <p>
132132+ * After calling this method, no commands will be available to undo or redo.
133133+ */
74134 public void flush()
75135 {
76136 undoStack.clear();
+9-3
src/main/java/game/map/editor/MapEditor.java
···104104import game.map.editor.commands.JoinHitObjects.JoinColliders;
105105import game.map.editor.commands.JoinHitObjects.JoinZones;
106106import game.map.editor.commands.JoinModels;
107107-import game.map.editor.commands.MapCommandManager;
108107import game.map.editor.commands.PaintVertices;
109108import game.map.editor.commands.SeparateVertices;
110109import game.map.editor.commands.SplitHitObject.SplitCollider;
···447446 * Major Components
448447 */
449448449449+ private static final int UNDO_LIMIT = 32;
450450+450451 public KeyboardInput keyboard;
451452 public MouseInput mouse;
452453 public SpriteLoader spriteLoader;
···558559 drawGeometryPreview = new PreviewGeometry();
559560560561 selectionManager = new SelectionManager(this);
561561- commandManager = new MapCommandManager(this, 32);
562562+ commandManager = new CommandManager(UNDO_LIMIT, this::onModified);
562563 drawTriManager = new DrawTrianglesManager(this, drawGeometryPreview);
563564564565 KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
···10831084 selectionPaintRadius = 16.0f;
1084108510851086 selectionManager = new SelectionManager(this);
10861086- commandManager = new MapCommandManager(this, 32);
10871087+ commandManager = new CommandManager(UNDO_LIMIT, this::onModified);
1087108810881089 if (changeMapState == ChangeMapState.NONE) {
10891090 showModels = true;
···14301431 profiler.record("the rest");
14311432 profiler.print();
14321433 }
14341434+ }
14351435+14361436+ private void onModified()
14371437+ {
14381438+ map.modified = true;
14331439 }
1434144014351441 public EditorMode getEditorMode()