Loophole in Deck attribute Send Hotkey when empty prevents processing last card
Vassal appears to prevent using Deck "Send when empty" hotkey to send a command to the last card to be removed from the deck.

When the card is removed, the Hotkey triggers and, at that point, the removed card has no location and is not found by a Global Key Command (even using CurrentMap=="" or a GKC without any search criteria).

Demo module:
[Empty HK test2.zip](https://github.com/vassalengine/vassal/files/9723883/Empty.HK.test2.zip)

diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java b/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java
index 8884d15d8..d1f2f9b78 100644
--- a/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java
+++ b/vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java
@@ -772,6 +772,9 @@ public class PieceMover extends AbstractBuildable
    * @return Command encapsulating anything this method did, for replay in log file or on other clients
    */
   protected Command movedPiece(GamePiece p, Point loc) {
+    // Make sure we don't end up sending I'm-empty commands from decks that were emptied by other players (e.g. online or log-step)
+    Deck.clearEmptyDecksList();
+
     Command c = new NullCommand();
     c = c.append(setOldLocations(p));
     if (!loc.equals(p.getPosition())) {
@@ -1172,6 +1175,9 @@ public class PieceMover extends AbstractBuildable
       comm = comm.append(applyKeyAfterMove(allDraggedPieces, map.getMoveKey()));
     }
 
+    // If we emptied any decks, let them send their I-am-empty key commands
+    comm = Deck.checkEmptyDecks(comm);
+
     // Repaint any areas of the map window changed by our move
     tracker.repaint();
 
diff --git a/vassal-app/src/main/java/VASSAL/build/module/map/deck/DeckSendKeyCommand.java b/vassal-app/src/main/java/VASSAL/build/module/map/deck/DeckSendKeyCommand.java
index 8022f6836..f7b708089 100644
--- a/vassal-app/src/main/java/VASSAL/build/module/map/deck/DeckSendKeyCommand.java
+++ b/vassal-app/src/main/java/VASSAL/build/module/map/deck/DeckSendKeyCommand.java
@@ -27,27 +27,27 @@ import VASSAL.script.expression.FormattedStringExpression;
 import VASSAL.tools.FormattedString;
 import VASSAL.tools.NamedKeyStroke;
 import VASSAL.tools.NamedKeyStrokeListener;
+import org.apache.commons.lang3.ArrayUtils;
 
 import java.awt.event.ActionEvent;
 import java.util.Collections;
 import java.util.List;
 
-import org.apache.commons.lang3.ArrayUtils;
-
 public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
 
-  public static final String TARGET_DECK = "targetDeck";
-  public static final String VARIABLE_DECK = "variableDeck";
-  public static final String DECK_EXPRESSION = "deckExpression";
-  public static final String SEND_MATCHING = "sendMatching";
-  public static final String MATCH_EXPRESSION = "matchExpression";
-  public static final String LIMIT_TOTAL = "limitTotal";
-  public static final String LIMIT_EXPRESSION = "limitExpression";
-  public static final String STOP = "stop";
-  public static final String STOP_EXPRESSION = "stopExpression";
-  public static final String STOP_INCLUDE = "stopAlso";
-  public static final String ORDER = "order";
-  public static final String SENT_COUNT = "sentCount";
+  public static final String TARGET_DECK = "targetDeck"; //NON-NLS
+  public static final String VARIABLE_DECK = "variableDeck"; //NON-NLS
+  public static final String DECK_EXPRESSION = "deckExpression"; //NON-NLS
+  public static final String SEND_MATCHING = "sendMatching"; //NON-NLS
+  public static final String MATCH_EXPRESSION = "matchExpression"; //NON-NLS
+  public static final String LIMIT_TOTAL = "limitTotal"; //NON-NLS
+  public static final String LIMIT_EXPRESSION = "limitExpression"; //NON-NLS
+  public static final String STOP = "stop"; //NON-NLS
+  public static final String STOP_EXPRESSION = "stopExpression"; //NON-NLS
+  public static final String STOP_INCLUDE = "stopAlso"; //NON-NLS
+  public static final String ORDER = "order"; //NON-NLS
+  public static final String SENT_COUNT = "sentCount"; //NON-NLS
+  public static final String APPLY_ON_MOVE = "applyOnMove"; //NON-NLS
 
   private NamedKeyStrokeListener sendListener;
   private String targetDeck = "";
@@ -60,6 +60,7 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
   private boolean stop = false;
   private final FormattedString stopExpresson =  new FormattedString("{}");
   private boolean stopInclude;
+  private boolean applyOnMove = false;
 
   public static String getConfigureTypeName() {
     return Resources.getString("Editor.DeckSendKeyCommand.component_type"); //$NON-NLS-1$
@@ -109,6 +110,10 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
     return stopInclude;
   }
 
+  public boolean isApplyOnMove() {
+    return applyOnMove;
+  }
+
   @Override
   public String[] getAttributeNames() {
     return ArrayUtils.addAll(
@@ -122,7 +127,8 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
       LIMIT_EXPRESSION,
       STOP,
       STOP_EXPRESSION,
-      STOP_INCLUDE
+      STOP_INCLUDE,
+      APPLY_ON_MOVE
     );
   }
 
@@ -140,7 +146,8 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
       Resources.getString("Editor.DeckSendKeyCommand.stop"),
       Resources.getString("Editor.DeckSendKeyCommand.stop_expression"),
       Resources.getString("Editor.DeckSendKeyCommand.stop_include"),
-      Resources.getString("Editor.DeckSendKeyCommand.order")
+      Resources.getString("Editor.DeckSendKeyCommand.order"),
+      Resources.getString("Editor.DeckSendKeyCommand.apply_on_move")
     );
   }
 
@@ -157,6 +164,7 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
       FormattedStringExpression.class,
       Boolean.class,
       FormattedStringExpression.class,
+      Boolean.class,
       Boolean.class
     );
   }
@@ -208,6 +216,12 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
       }
       stopInclude = (Boolean) value;
     }
+    else if (APPLY_ON_MOVE.equals(key)) {
+      if (value instanceof String) {
+        value = Boolean.valueOf((String) value);
+      }
+      applyOnMove = (Boolean) value;
+    }
     else {
       super.setAttribute(key, value);
     }
@@ -245,6 +259,9 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
     else if (STOP_INCLUDE.equals(key)) {
       return String.valueOf(stopInclude);
     }
+    else if (APPLY_ON_MOVE.equals(key)) {
+      return String.valueOf(applyOnMove);
+    }
     else {
       return super.getAttributeValueString(key);
     }
@@ -323,5 +340,4 @@ public class DeckSendKeyCommand extends AbstractDeckKeyCommand {
     }
     return Collections.emptyList();
   }
-
 }
diff --git a/vassal-app/src/main/java/VASSAL/counters/Deck.java b/vassal-app/src/main/java/VASSAL/counters/Deck.java
index a64a0025c..5392b6765 100644
--- a/vassal-app/src/main/java/VASSAL/counters/Deck.java
+++ b/vassal-app/src/main/java/VASSAL/counters/Deck.java
@@ -177,6 +177,39 @@ public class Deck extends Stack implements PlayerRoster.SideChangeListener {
   /** The matching DrawPile that generated this Deck */
   protected DrawPile myPile;
 
+  // List of decks who want to fire off their I-just-got-emptied key
+  protected static List<Deck> deckEmptiedKeyQueue = new ArrayList<>();
+
+  /**
+   * If any decks want to send their I-am-empty key, send it now.
+   */
+  public static Command checkEmptyDecks(Command c) {
+    final GameModule gm = GameModule.getGameModule();
+    gm.pauseLogging();
+    for (final Deck deck : deckEmptiedKeyQueue) {
+      deck.sendEmptyKey();
+    }
+
+    deckEmptiedKeyQueue.clear();
+
+    c = c.append(gm.resumeLogging());
+    return c;
+  }
+
+  /**
+   * Clears the list of I-am-empty decks
+   */
+  public static void clearEmptyDecksList() {
+    deckEmptiedKeyQueue.clear();
+  }
+
+  /**
+   * Sends the I-am-empty key for this deck (whether to send it was already determined when the last piece was removed)
+   */
+  protected void sendEmptyKey() {
+    gameModule.fireKeyStroke(emptyKey);
+  }
+
   /**
    * Special {@link CommandEncoder} to handle loading/saving Decks from files.
    */
@@ -469,7 +502,7 @@ public class Deck extends Stack implements PlayerRoster.SideChangeListener {
       fireNumCardsProperty();
       // Do NOT fire a Deck Empty key if it has been caused by an Undo Command
       if (hotkeyOnEmpty && emptyKey != null && startCount > 0 && pieceCount == 0 && ! GameModule.getGameModule().getBasicLogger().isUndoInProgress()) {
-        gameModule.fireKeyStroke(emptyKey);
+        deckEmptiedKeyQueue.add(this);
       }
     }
   }
@@ -1814,7 +1847,12 @@ public class Deck extends Stack implements PlayerRoster.SideChangeListener {
       // move cards to deck
       final int cnt = getPieceCount() - 1;
       for (int i = cnt; i >= 0; i--) {
-        c = c.append(target.addToContents(getPieceAt(i)));
+        final GamePiece p = getPieceAt(i);
+
+        // Prepare the piece for move, writing "old location" properties and unlinking from any deck
+        c = p.prepareMove(c, false);
+        c = c.append(target.addToContents(p));
+        c = p.finishMove(c, false, false);
       }
     }
     return c;
@@ -1905,7 +1943,18 @@ public class Deck extends Stack implements PlayerRoster.SideChangeListener {
 
     // Send them
     for (final GamePiece piece : sending) {
+
+      // Prepare the piece for move, writing "old location" properties and unlinking from any deck
+      c = piece.prepareMove(c, false);
+
+      // Move it to the new deck
       c = c.append(target.addToContents(piece));
+
+      // Finish up after piece's move. Apply afterburner key if that option is selected
+      c = piece.finishMove(c, dkc.isApplyOnMove() && (map != null), false);
+      if (dkc.isApplyOnMove() && (map != null)) {
+        map.repaint();
+      }
     }
 
     // And add a report
diff --git a/vassal-app/src/main/java/VASSAL/counters/EditablePiece.java b/vassal-app/src/main/java/VASSAL/counters/EditablePiece.java
index 290f3b28d..5b5b022fc 100644
--- a/vassal-app/src/main/java/VASSAL/counters/EditablePiece.java
+++ b/vassal-app/src/main/java/VASSAL/counters/EditablePiece.java
@@ -17,7 +17,12 @@
  */
 package VASSAL.counters;
 
+import VASSAL.build.module.Map;
 import VASSAL.build.module.documentation.HelpFile;
+import VASSAL.command.ChangeTracker;
+import VASSAL.command.Command;
+
+import static VASSAL.counters.Decorator.putOldProperties;
 
 /**
  * If class implementing GamePiece also implements the EditablePiece interface, then
@@ -57,4 +62,67 @@ public interface EditablePiece extends GamePiece {
     }
     return desc;
   }
+
+  /**
+   * Centralized method for preparing a piece to move. Writes the "Old Map" properties based on its current location,
+   * optionally marks the piece as moved, and tells any deck its in that it is leaving.
+   * @param c Command to which will be appended a command for recreating anything this method does
+   * @param mark_moved If true the piece will be mark moved
+   * @return A command for recreating anything this method does, appended to the command passed
+   */
+  @Override
+  default Command prepareMove(Command c, boolean mark_moved) {
+    // Make sure we don't send deck-is-empty hotkeys when deck is emptied by another player in an online game or by a log-step
+    Deck.clearEmptyDecksList();
+
+    final GamePiece outer = Decorator.getOutermost(this);
+
+    // If selected, mark piece as moved.
+    if (mark_moved) {
+      final ChangeTracker tracker = new ChangeTracker(outer);
+      outer.setProperty(Properties.MOVED, Boolean.TRUE);
+      c = c.append(tracker.getChangeCommand());
+    }
+
+    // Write the "Old Location" properties of the piece using the current location
+    c = c.append(putOldProperties(this));
+
+    // Tell our stack (in case it is a deck) that we've removed a piece
+    final Stack parent = outer.getParent();
+    if (parent != null) {
+      c = c.append(parent.pieceRemoved(outer));
+    }
+    return c;
+  }
+
+  /**
+   * Centralized method for finishing up after a piece moves. Optionally finds a new mat if needed,
+   * and optionally applies any afterburner apply-on-move key for the piece's map. If we emptied any
+   * decks, allows them to send their I-am-empty key commands
+   * @param c Command to which will be appended a command for recreating anything this method does
+   * @param afterburner if true, apply the afterburner apply-on-move key for the piece's map
+   * @param findmat if true, find a new mat for this piece if needed (if this piece is cargo)
+   * @return A command for recreating anything this method does, appended to the command passed
+   */
+  @Override
+  default Command finishMove(Command c, boolean afterburner, boolean findmat) {
+    final GamePiece outer = Decorator.getOutermost(this);
+    if (findmat) {
+      // If a cargo piece has been moved, find it a new Mat if needed.
+      c = MatCargo.findNewMat(c, outer);
+    }
+
+    // Apply "afterburner" apply-on-move key command
+    if (afterburner) {
+      final Map map = outer.getMap();
+      if (map.getMoveKey() != null) {
+        c = c.append(outer.keyEvent(map.getMoveKey()));
+      }
+    }
+
+    // If we emptied any decks, let them send their I-am-empty key commands
+    c = Deck.checkEmptyDecks(c);
+
+    return c;
+  }
 }
diff --git a/vassal-app/src/main/java/VASSAL/counters/FreeRotator.java b/vassal-app/src/main/java/VASSAL/counters/FreeRotator.java
index cd1c1ec5a..4feba754c 100644
--- a/vassal-app/src/main/java/VASSAL/counters/FreeRotator.java
+++ b/vassal-app/src/main/java/VASSAL/counters/FreeRotator.java
@@ -17,10 +17,36 @@
  */
 package VASSAL.counters;
 
+import VASSAL.build.GameModule;
+import VASSAL.build.module.Map;
+import VASSAL.build.module.documentation.HelpFile;
+import VASSAL.build.module.map.Drawable;
+import VASSAL.command.ChangeTracker;
+import VASSAL.command.Command;
+import VASSAL.command.NullCommand;
+import VASSAL.configure.BooleanConfigurer;
 import VASSAL.configure.FormattedExpressionConfigurer;
+import VASSAL.configure.IntConfigurer;
+import VASSAL.configure.NamedHotKeyConfigurer;
+import VASSAL.configure.StringConfigurer;
+import VASSAL.i18n.PieceI18nData;
+import VASSAL.i18n.Resources;
+import VASSAL.i18n.TranslatablePiece;
 import VASSAL.script.expression.AuditTrail;
 import VASSAL.script.expression.AuditableException;
 import VASSAL.tools.FormattedString;
+import VASSAL.tools.NamedKeyStroke;
+import VASSAL.tools.SequenceEncoder;
+import VASSAL.tools.imageop.GamePieceOp;
+import VASSAL.tools.imageop.Op;
+import VASSAL.tools.imageop.RotateScaleOp;
+import VASSAL.tools.swing.SwingUtils;
+import net.miginfocom.swing.MigLayout;
+
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.KeyStroke;
 import java.awt.AlphaComposite;
 import java.awt.Component;
 import java.awt.Cursor;
@@ -46,32 +72,6 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Random;
 
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.KeyStroke;
-
-import VASSAL.build.GameModule;
-import VASSAL.build.module.Map;
-import VASSAL.build.module.documentation.HelpFile;
-import VASSAL.build.module.map.Drawable;
-import VASSAL.command.ChangeTracker;
-import VASSAL.command.Command;
-import VASSAL.configure.BooleanConfigurer;
-import VASSAL.configure.IntConfigurer;
-import VASSAL.configure.NamedHotKeyConfigurer;
-import VASSAL.configure.StringConfigurer;
-import VASSAL.i18n.PieceI18nData;
-import VASSAL.i18n.Resources;
-import VASSAL.i18n.TranslatablePiece;
-import VASSAL.tools.NamedKeyStroke;
-import VASSAL.tools.SequenceEncoder;
-import VASSAL.tools.imageop.GamePieceOp;
-import VASSAL.tools.imageop.Op;
-import VASSAL.tools.imageop.RotateScaleOp;
-import VASSAL.tools.swing.SwingUtils;
-import net.miginfocom.swing.MigLayout;
-
 /**
  * d/b/a "Can Rotate"
  *
@@ -502,33 +502,14 @@ public class FreeRotator extends Decorator
       return null;
     }
 
-    // Set the Old... properties
-    Command c = putOldProperties(this);
-
-    // Mark the piece moved
-    final GamePiece outer = Decorator.getOutermost(gp);
-    final ChangeTracker comm = new ChangeTracker(outer);
-    outer.setProperty(Properties.MOVED, Boolean.TRUE);
-    c = c.append(comm.getChangeCommand());
+    // Prepare the piece for move, writing "old location" properties, marking moved, and unlinking from any deck
+    Command c = prepareMove(new NullCommand(), true);
 
     // Move the piece
-    c = c.append(map.placeOrMerge(outer, dest));
-
-    // If a cargo piece has been "sent" (but not moving in tandem with its mat), find it a new Mat if needed.
-    if (!cargoFollowup) {
-      c = MatCargo.findNewMat(c, outer);
-    }
+    c = c.append(map.placeOrMerge(Decorator.getOutermost(gp), dest));
 
-    // Apply after Move Key
-    if (map.getMoveKey() != null) {
-      c = c.append(outer.keyEvent(map.getMoveKey()));
-    }
-
-    // Unlink from Parent Stack (in case it is a Deck).
-    final Stack parent = outer.getParent();
-    if (parent != null) {
-      c = c.append(parent.pieceRemoved(outer));
-    }
+    // Post move actions -- find a new mat if needed, and apply any afterburner apply-on-move key
+    c = finishMove(c, true, !cargoFollowup);
 
     return c;
   }
diff --git a/vassal-app/src/main/java/VASSAL/counters/GamePiece.java b/vassal-app/src/main/java/VASSAL/counters/GamePiece.java
index abd77429c..b76e5df6b 100644
--- a/vassal-app/src/main/java/VASSAL/counters/GamePiece.java
+++ b/vassal-app/src/main/java/VASSAL/counters/GamePiece.java
@@ -17,14 +17,7 @@
  */
 package VASSAL.counters;
 
-import java.awt.Component;
-import java.awt.Graphics;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.Shape;
-
-import javax.swing.KeyStroke;
-
+import VASSAL.build.GameModule;
 import VASSAL.build.module.BasicCommandEncoder;
 import VASSAL.build.module.GameState;
 import VASSAL.build.module.Map;
@@ -32,6 +25,13 @@ import VASSAL.build.module.properties.PropertySource;
 import VASSAL.command.ChangePiece;
 import VASSAL.command.Command;
 
+import javax.swing.KeyStroke;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Shape;
+
 /**
  * Interface to deal with the behaviors of a physical component of the game, <b><i>or a Trait of one</i></b>. Although from
  * the name you might think that anything implementing this would be "a whole piece" (and perhaps in Elder Times that was true),
@@ -217,4 +217,14 @@ public interface GamePiece extends PropertySource {
    */
   @Override
   Object getProperty(Object key);
+
+  default Command prepareMove(Command c, boolean mark_moved) {
+    GameModule.getGameModule().warn("GamePiece#prepareMove called by Deck or Stack"); //NON-NLS
+    return c;
+  }
+
+  default Command finishMove(Command c, boolean afterburner, boolean findmat) {
+    GameModule.getGameModule().warn("GamePiece#finishMove called by Deck or Stack"); //NON-NLS
+    return c;
+  }
 }
diff --git a/vassal-app/src/main/java/VASSAL/counters/ReturnToDeck.java b/vassal-app/src/main/java/VASSAL/counters/ReturnToDeck.java
index 86da1c15d..282984965 100644
--- a/vassal-app/src/main/java/VASSAL/counters/ReturnToDeck.java
+++ b/vassal-app/src/main/java/VASSAL/counters/ReturnToDeck.java
@@ -22,6 +22,7 @@ import VASSAL.build.module.Map;
 import VASSAL.build.module.documentation.HelpFile;
 import VASSAL.build.module.map.DrawPile;
 import VASSAL.command.Command;
+import VASSAL.command.NullCommand;
 import VASSAL.configure.BooleanConfigurer;
 import VASSAL.configure.DeckSelectionConfigurer;
 import VASSAL.configure.NamedHotKeyConfigurer;
@@ -166,20 +167,24 @@ public class ReturnToDeck extends Decorator implements TranslatablePiece {
 
       final Map preMap = getMap();
       final Point prePos = getPosition();
-      comm = putOldProperties(this);
+
+      // Prepare the piece for move, writing "old location" properties and unlinking from any deck
+      comm = prepareMove(new NullCommand(), false);
+
+      // Move the piece to its new deck
       comm = comm.append(pile.addToContents(Decorator.getOutermost(this)));
 
-      // If this piece is also loaded cargo, remove it from it's mat
+      // If this piece is also loaded cargo, remove it from its mat
       final MatCargo cargo = (MatCargo) Decorator.getDecorator(Decorator.getOutermost(this), MatCargo.class);
       if (cargo != null && cargo.getMat() != null) {
         comm = comm.append(cargo.makeClearMatCommand());
       }
 
-      // Apply Auto-move key if the piece has moved
       final Map m = pile.getMap();
-      if (m != null && m.getMoveKey() != null && (m != preMap || !getPosition().equals(prePos))) {
-        comm.append(Decorator.getOutermost(this).keyEvent(m.getMoveKey()));
-      }
+
+      // Post move actions -- apply any afterburner apply-on-move key (but only if the piece was actually moved)
+      comm = finishMove(comm, (m != null && m.getMoveKey() != null && (m != preMap || !getPosition().equals(prePos))), false);
+
       if (m != null) {
         m.repaint();
       }
diff --git a/vassal-app/src/main/java/VASSAL/counters/SendToLocation.java b/vassal-app/src/main/java/VASSAL/counters/SendToLocation.java
index 0f15a5b8a..408263fec 100644
--- a/vassal-app/src/main/java/VASSAL/counters/SendToLocation.java
+++ b/vassal-app/src/main/java/VASSAL/counters/SendToLocation.java
@@ -502,7 +502,6 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
     Command c = null;
     Point dest = null;
     final Map oldMap = outer.getMap();
-    Map backMap = null;
 
     // If we're about to move a Mat, establish the initial relative positions of all its "contents"
     if (GameModule.getGameModule().isMatSupport()) {
@@ -518,7 +517,6 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
     }
 
     if (send) {
-      final Stack parent = outer.getParent();
       dest = getSendLocation();
       if (map != null && dest != null) {
         if (map == getMap() && dest.equals(getPosition())) {
@@ -528,14 +526,11 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
         final ChangeTracker tracker = new ChangeTracker(this);
         setProperty(BACK_MAP, getMap());
         setProperty(BACK_POINT, getPosition());
+        c = tracker.getChangeCommand();
 
-        // Mark moved and generate movement trail if global options setting is on
-        if (GlobalOptions.getInstance().isSendToLocationMoveTrails()) {
-          outer.setProperty(Properties.MOVED, Boolean.TRUE);
-        }
+        // Prepare piece for move, setting "old location" properties. Mark moved and generate movement trail if global options setting is on
+        c = prepareMove(c, GlobalOptions.getInstance().isSendToLocationMoveTrails());
 
-        c = tracker.getChangeCommand();
-        c = c.append(putOldProperties(this));
         if (!Boolean.TRUE.equals(outer.getProperty(Properties.IGNORE_GRID))) {
           dest = map.snapTo(dest);
         }
@@ -547,45 +542,27 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
         }
         map.repaint();
 
-        // If a cargo piece has been "sent", find it a new Mat if needed.
-        c = MatCargo.findNewMat(c, outer);
-
-        // Apply Auto-move key
-        if (map.getMoveKey() != null) {
-          c = c.append(outer.keyEvent(map.getMoveKey()));
-        }
-
-        if (parent != null) {
-          c = c.append(parent.pieceRemoved(outer));
-        }
+        // Complete the move, finding new mat if needed and applying map afterburner key
+        c = finishMove(c, true, true);
       }
     }
     else {
-      backMap = (Map) getProperty(BACK_MAP);
+      final Map backMap = (Map) getProperty(BACK_MAP);
       final Point backPoint = (Point) getProperty(BACK_POINT);
       final ChangeTracker tracker = new ChangeTracker(this);
       setProperty(BACK_MAP, null);
       setProperty(BACK_POINT, null);
-
-      // Mark moved and generate movement trail if global options setting
-      if (GlobalOptions.getInstance().isSendToLocationMoveTrails()) {
-        outer.setProperty(Properties.MOVED, Boolean.TRUE);
-      }
-
       c = tracker.getChangeCommand();
 
       if (backMap != null && backPoint != null) {
-        c = c.append(putOldProperties(this));
+        // Prepare piece for move, setting "old location" properties. Mark moved and generate movement trail if global options setting is on
+        c = prepareMove(c, GlobalOptions.getInstance().isSendToLocationMoveTrails());
+
         c = c.append(backMap.placeOrMerge(outer, backPoint));
         dest = backPoint;
 
-        // If a cargo piece has been "sent", find it a new Mat if needed.
-        c = MatCargo.findNewMat(c, outer);
-
-        // Apply Auto-move key
-        if (backMap.getMoveKey() != null) {
-          c = c.append(outer.keyEvent(backMap.getMoveKey()));
-        }
+        // Complete the move, finding new mat if needed and applying map afterburner key
+        c = finishMove(c, true, true);
 
         if (oldMap != null && oldMap != backMap) {
           oldMap.repaint();
@@ -600,7 +577,6 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
       if ((offsets != null) && dest != null) {
         for (int i = 0; i < contents.size(); i++) {
           final GamePiece piece = contents.get(i);
-          final Stack pieceParent = piece.getParent();
           final MatCargo cargo = (MatCargo) Decorator.getDecorator(piece, MatCargo.class);
           if (cargo != null) {
 
@@ -609,16 +585,10 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
             pt.x += offsets.get(i).x;
             pt.y += offsets.get(i).y;
 
-            // From here down we're just duplicating a send-to-location command for the Cargo piece
+            // From here down we're basically just duplicating a send-to-location command for the Cargo piece
 
-            final ChangeTracker tracker = new ChangeTracker(piece);
-            // Mark moved and generate movement trail if Global Options setting
-            if (GlobalOptions.getInstance().isSendToLocationMoveTrails()) {
-              piece.setProperty(Properties.MOVED, Boolean.TRUE);
-            }
-
-            c = c.append(tracker.getChangeCommand());
-            c = c.append(putOldProperties(piece));
+            // Prepare piece for move, setting "old location" properties. Mark moved and generate movement trail if global options setting is on
+            c = piece.prepareMove(c, GlobalOptions.getInstance().isSendToLocationMoveTrails());
 
             //BR// I sort of think cargo shouldn't snap when moving in lockstep with its mat.
             //BR// This may lead to the eventual conclusion that cargo pieces shouldn't snap
@@ -628,13 +598,9 @@ public class SendToLocation extends Decorator implements TranslatablePiece {
             //}
 
             c = c.append(map.placeOrMerge(piece, pt));
-            // Apply Auto-move key
-            if (map.getMoveKey() != null) {
-              c = c.append(piece.keyEvent(map.getMoveKey()));
-            }
-            if (pieceParent != null) {
-              c = c.append(pieceParent.pieceRemoved(piece));
-            }
+
+            // Complete the move, applying map afterburner key
+            c = piece.finishMove(c, true, false);
           }
         }
       }
diff --git a/vassal-app/src/main/java/VASSAL/counters/Translate.java b/vassal-app/src/main/java/VASSAL/counters/Translate.java
index 3b690e3eb..64647938b 100644
--- a/vassal-app/src/main/java/VASSAL/counters/Translate.java
+++ b/vassal-app/src/main/java/VASSAL/counters/Translate.java
@@ -23,7 +23,6 @@ import VASSAL.build.module.Map;
 import VASSAL.build.module.documentation.HelpFile;
 import VASSAL.build.module.map.MovementReporter;
 import VASSAL.build.module.map.boardPicker.Board;
-import VASSAL.command.ChangeTracker;
 import VASSAL.command.Command;
 import VASSAL.command.NullCommand;
 import VASSAL.configure.BooleanConfigurer;
@@ -36,7 +35,12 @@ import VASSAL.i18n.TranslatablePiece;
 import VASSAL.tools.FormattedString;
 import VASSAL.tools.NamedKeyStroke;
 import VASSAL.tools.SequenceEncoder;
+import net.miginfocom.swing.MigLayout;
 
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.KeyStroke;
+import javax.swing.SwingUtilities;
 import java.awt.Component;
 import java.awt.Graphics;
 import java.awt.Point;
@@ -50,13 +54,6 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.KeyStroke;
-import javax.swing.SwingUtilities;
-
-import net.miginfocom.swing.MigLayout;
-
 /**
  * d/b/a "Move Fixed Distance"
  *
@@ -322,31 +319,14 @@ public class Translate extends Decorator implements TranslatablePiece {
       return null;
     }
 
-    // Set the Old... properties
-    Command c = putOldProperties(this);
-
-    // Mark the piece moved
-    final GamePiece outer = Decorator.getOutermost(gp);
-    final ChangeTracker comm = new ChangeTracker(outer);
-    outer.setProperty(Properties.MOVED, Boolean.TRUE);
-    c = c.append(comm.getChangeCommand());
+    // Prepare the piece for move, writing "old location" properties, marking moved, and unlinking from any deck
+    Command c = prepareMove(new NullCommand(), true);
 
     // Move the piece
-    c = c.append(map.placeOrMerge(outer, dest));
-
-    // If a cargo piece has been "sent", find it a new Mat if needed.
-    c = MatCargo.findNewMat(c, outer);
-    
-    // Apply after Move Key
-    if (map.getMoveKey() != null) {
-      c = c.append(outer.keyEvent(map.getMoveKey()));
-    }
+    c = c.append(map.placeOrMerge(Decorator.getOutermost(this), dest));
 
-    // Unlink from Parent Stack (in case it is a Deck).
-    final Stack parent = outer.getParent();
-    if (parent != null) {
-      c = c.append(parent.pieceRemoved(outer));
-    }
+    // Post move actions -- find a new mat if needed, and apply any afterburner apply-on-move key
+    c = finishMove(c, true, true);
 
     return c;
   }
diff --git a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties
index 845b5292f..4f6278526 100644
--- a/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties
+++ b/vassal-app/src/main/resources/VASSAL/i18n/Editor.properties
@@ -472,6 +472,7 @@ Editor.DeckSendKeyCommand.stop_include=Also send card that matches stop conditio
 Editor.DeckSendKeyCommand.order=Order to send cards
 Editor.DeckSendKeyCommand.error_no_target=Target Deck not found
 Editor.DeckSendKeyCommand.error_invalid_send_limit=Invalid send card limit, Sending all cards
+Editor.DeckSendKeyCommand.apply_on_move=Apply any apply-on-move key commands for destination map
 
 # Delete
 # Description of the Delete trait seen by users
diff --git a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/DeckSendKeyCommand.adoc b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/DeckSendKeyCommand.adoc
index 47cec2f61..87ffb8144 100644
--- a/vassal-doc/src/main/readme-referencemanual/ReferenceManual/DeckSendKeyCommand.adoc
+++ b/vassal-doc/src/main/readme-referencemanual/ReferenceManual/DeckSendKeyCommand.adoc
@@ -54,7 +54,9 @@ NOTE: If you add new Decks to the module, you will need to save and reload the m
 
 *Stop sending expression:*:: A <<PropertyMatchExpression.adoc#top,Property Match Expression>> that when it matches a card, no more cards will be sent to the target Deck as part of this Deck Send Command.
 
-Also send card that matches stop condition:*:: Controls whether the card that triggers the _Stop sending expression_ is sent to the target Deck or not.
+*Also send card that matches stop condition:*:: Controls whether the card that triggers the _Stop sending expression_ is sent to the target Deck or not.
+
+*Apply any apply-on-move key commands for destination map:*:: If selected, then if the destination deck's map has an apply-on-move key (aka _Key Command to apply to all units ending movement on this map_), then it will be applied to each card after it is moved to the destination. If unchecked, then no apply-on-move commands will activate.
 
 a|
 image:images/DeckSendKeyCommand.png[]
['vassal-app/src/main/java/VASSAL/counters/GamePiece.java', 'vassal-app/src/main/resources/VASSAL/i18n/Editor.properties', 'vassal-app/src/main/java/VASSAL/build/module/map/deck/DeckSendKeyCommand.java', 'vassal-app/src/main/java/VASSAL/counters/EditablePiece.java', 'vassal-doc/src/main/readme-referencemanual/ReferenceManual/DeckSendKeyCommand.adoc', 'vassal-app/src/main/java/VASSAL/counters/SendToLocation.java', 'vassal-app/src/main/java/VASSAL/counters/ReturnToDeck.java', 'vassal-app/src/main/java/VASSAL/build/module/map/PieceMover.java', 'vassal-app/src/main/java/VASSAL/counters/Deck.java', 'vassal-app/src/main/java/VASSAL/counters/Translate.java', 'vassal-app/src/main/java/VASSAL/counters/FreeRotator.java']