Skip to content

Implement RFE #7032 / Core Rules - Walk on deployment - #8812

Draft
TenkawaBC wants to merge 99 commits into
MegaMek:mainfrom
TenkawaBC:walk-on-deployment
Draft

Implement RFE #7032 / Core Rules - Walk on deployment#8812
TenkawaBC wants to merge 99 commits into
MegaMek:mainfrom
TenkawaBC:walk-on-deployment

Conversation

@TenkawaBC

@TenkawaBC TenkawaBC commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

This is the beginning of the implementation of walk-on deployment. Gated behind core rules.

What this changes

If an entity is not yet deployed, and their deployment is not Any or Center, and the deployment round is not <0 (pre-deployment), then skip the unit for the deployment phase and instead have them deploy as part of movement.

The first click in the movement phase will be on the edge to deploy the unit, the second click should make the movement path. Pressing Escape should remove the entire path and clear the deployment, so it can be deployed elsewhere.

When clicking the Move button, it sends the movepath to the server and tells the server to also deploy the entity.

As part of core rules, it is also skipping any units that are immobile for movement/deployment and initiative.

This is a pre-alpha and has several issues currently (See below)

Known issues:

  • FIXED it is skipping the deployment for Any() / Center()
  • FIXED The second click is showing illegal movement, but press backspace until it is just the deployment, then clicking will have the correct movement path
  • FIXED Sometimes clicking move does not update the sprite on the board
  • FIXED The initial DEPLOY_STEP currently does not have an mp cost. This needs to be set to the cost of entering the underlying terrain (ignoring elevation), and reset when Escape is pressed.
  • DONE Still need to add a pre-deployment option to the deployment round options in the lobby (set to a round of -1) and gated behind walkOnDeployment() check
    -Deployment zones in lobby still show the width of 3, even though I am overriding this to 1 in the code. This should instead set to 1 when walkOnDeployment() is true by default.
  • DONE Need to add an override method in TWRulesGame to read a new boolean for walkOn and return that value. Also need to add an option for walk-on deployment in game options for TW only. In intializeRules, read this game option and call the new TWRulesGame method "setWalkOnDeployment(boolean walkOn) {this.walkOn = walkOn;} to set the private boolean.
  • DONE Currently does not check for loaded units and process them with the deployment

@TenkawaBC
TenkawaBC requested a review from a team as a code owner August 24, 2026 17:35
@TenkawaBC TenkawaBC added the New Core Rules Summer 2026 Core Rules label Aug 24, 2026
@TenkawaBC
TenkawaBC marked this pull request as draft August 24, 2026 17:36
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 16.38258% with 883 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.58%. Comparing base (e9e49f5) to head (bcf2255).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...lient/ui/panels/phaseDisplay/DeploymentHelper.java 16.04% 148 Missing and 9 partials ⚠️
...ek/server/totalWarfare/DeploymentServerHelper.java 20.62% 112 Missing and 15 partials ⚠️
...k/client/ui/dialogs/customMek/CustomMekDialog.java 0.00% 111 Missing ⚠️
megamek/src/megamek/common/game/Game.java 9.01% 105 Missing and 6 partials ⚠️
...ient/ui/panels/phaseDisplay/DeploymentDisplay.java 8.77% 103 Missing and 1 partial ⚠️
megamek/src/megamek/common/moves/MovePath.java 15.65% 81 Missing and 16 partials ⚠️
megamek/src/megamek/common/board/Board.java 21.87% 45 Missing and 5 partials ⚠️
...anels/phaseDisplay/lobby/PlayerSettingsDialog.java 0.00% 36 Missing ⚠️
megamek/src/megamek/common/Player.java 36.36% 13 Missing and 1 partial ⚠️
megamek/src/megamek/common/rules/RulesGame.java 43.47% 7 Missing and 6 partials ⚠️
... and 13 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8812      +/-   ##
============================================
+ Coverage     36.41%   36.58%   +0.16%     
- Complexity    32692    33021     +329     
============================================
  Files          3586     3589       +3     
  Lines        354030   354601     +571     
  Branches      62030    62135     +105     
============================================
+ Hits         128934   129740     +806     
+ Misses       211598   211109     -489     
- Partials      13498    13752     +254     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add setter function for TW
Add initialization to the TWRulesManager for walkOnDeployment.
@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

This implements an old RFE. #7032

@TenkawaBC TenkawaBC changed the title Walk on deployment Implement RFE #7032 / Core Rules - Walk on deployment Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Begins implementing “walk-on deployment” for Core Rules by introducing a new DEPLOY move step and adjusting eligibility/turn-handling so undeployed units can enter the map during the Movement phase instead of the Deployment phase (when the rule is enabled).

Changes:

  • Add MoveStepType.DEPLOY plus a DeployStep phase-pass to support a deployment step in movement path compilation.
  • Update client movement UI/commands to allow selecting a legal deployment hex and to clear deployment state when cancelling movement.
  • Add/propagate a walkOnDeployment() rule toggle via RulesGame (Core defaults on; TW defaults off with a placeholder hook).

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
megamek/src/megamek/server/totalWarfare/TWGameManager.java Attempts to skip certain entities from turn counting based on walk-on deployment/immobility.
megamek/src/megamek/server/totalWarfare/MovePathHandler.java Applies server-side “deploy” effects when a move path contains DEPLOY.
megamek/src/megamek/common/units/Entity.java Adjusts phase eligibility to allow undeployed entities to participate in Movement under walk-on deployment.
megamek/src/megamek/common/rules/totalwarfare/TWRulesGame.java Adds TW-side backing boolean and overrides for walk-on deployment (default off).
megamek/src/megamek/common/rules/RulesGame.java Introduces walkOnDeployment() and a TW hook setWalkOnDeployment(boolean).
megamek/src/megamek/common/rules/core/CoreRulesGame.java Enables walk-on deployment in Core rules (default on).
megamek/src/megamek/common/moves/PhasePassSelector.java Registers a DeployStep phase-pass for MoveStepType.DEPLOY.
megamek/src/megamek/common/moves/MoveStep.java Adds per-step isDeploying state for the new deploy step.
megamek/src/megamek/common/moves/DeployStep.java New phase-pass that marks a step as deploying and short-circuits compilation.
megamek/src/megamek/common/game/Game.java Adds a placeholder hook to enable walk-on deployment for TW via game options (not yet wired).
megamek/src/megamek/common/enums/MoveStepType.java Adds DEPLOY as a new move step type.
megamek/src/megamek/common/board/Board.java Narrows edge deployment-zone width to 1 when walk-on deployment is enabled.
megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java Adds UI handling to place an undeployed unit on a legal edge hex during Movement.
megamek/src/megamek/client/commands/MoveCommand.java Clears deployment state when cancelling planned movement containing DEPLOY.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread megamek/src/megamek/server/totalWarfare/TWGameManager.java Outdated
Comment thread megamek/src/megamek/common/units/Entity.java Outdated
Comment thread megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java Outdated
Walk on deployment no longer has issue with invalid hexes
Walk on deployment is now consistent with deployment
New issue found. switching to Jump generates an NPE around StepSprite.
@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

StepSprite line 93 is causing issues when switching movement type to Jump.
13:30:13,194 ERROR [megamek.MegaMek] {AWT-EventQueue-0}
megamek.MegaMek.lambda$main$1(MegaMek.java:118) - Uncaught java.lang.NullPointerException detected. Please open up an issue containing all logs, the game save file, and customs at https://github.com/MegaMek/megamek/issues . If Sentry is enabled, the error has also been logged.
java.lang.NullPointerException: Cannot read field "x" because "p" is null
at java.desktop/java.awt.Rectangle.(Rectangle.java:241)
at megamek.client.ui.clientGUI.boardview.sprite.StepSprite.(StepSprite.java:93)
at megamek.client.ui.clientGUI.boardview.BoardView.drawMovementData(BoardView.java:4190)
at megamek.client.ui.panels.phaseDisplay.MovementDisplay.updateMove(MovementDisplay.java:1136)
at megamek.client.ui.panels.phaseDisplay.MovementDisplay.updateMove(MovementDisplay.java:1130)
at megamek.client.ui.panels.phaseDisplay.MovementDisplay.addStepToMovePath(MovementDisplay.java:1066)
at megamek.client.ui.panels.phaseDisplay.MovementDisplay.initializeJumpMovePath(MovementDisplay.java:1972)
at megamek.client.ui.panels.phaseDisplay.MovementDisplay.actionPerformed(MovementDisplay.java:6319)
at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1972)
at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2314)
at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:407)
at java.desktop/javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:262)
at java.desktop/javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:279)
at java.desktop/java.awt.AWTEventMulticaster.mouseReleased(AWTEventMulticaster.java:299)
at java.desktop/java.awt.Component.processMouseEvent(Component.java:6621)

@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

Code to set a turn in the hex of deployment to be no cost is not working properly.

Add exclusion to prevent bots from using walk-on deployment (until they support it)
@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

-Need overall review for how to position initial deployment to not cost turns if immediately after deployment and not exiting the hex yet.
-Default position when clicking a deployment hex on an edge should be to face towards the middle of the board (back should be facing off-board)
-Need check on the NPE when switching from run to jump as detailed in the comments here
-When clicking the hex to deploy, update the movepath overlay to not say +0, but instead the actual MP cost at that time

@HammerGS

Copy link
Copy Markdown
Member

Here is the DropShip fix as a patch, ready to apply. It is the change from my last comment, nothing else. I checked it applies cleanly to 6e92141d5c with git apply --check.

Save as dropship-clear-secondaries.patch and run git apply dropship-clear-secondaries.patch:

diff --git a/megamek/src/megamek/common/units/Dropship.java b/megamek/src/megamek/common/units/Dropship.java
index de837e9ce3..0fa46e4b7e 100644
--- a/megamek/src/megamek/common/units/Dropship.java
+++ b/megamek/src/megamek/common/units/Dropship.java
@@ -565,6 +565,13 @@ public class Dropship extends SmallCraft {
         }
 
         super.setPosition(position, false);
+        if (position == null) {
+            // Clearing a walk-on deployment sets the position to null. Without dropping the secondary hexes too,
+            // getOccupiedCoords() keeps returning the seven hexes this DropShip used to fill, the position lookup
+            // never releases them, and anything that asks what is in one of those hexes gets back a DropShip
+            // whose getPosition() is null.
+            secondaryPositions.clear();
+        }
         if ((getAltitude() == 0) && (null != game) && !isSpaceborne() && (position != null)) {
             secondaryPositions.put(0, position);
             secondaryPositions.put(1, position.translated(getFacing()));

With it applied, clearing a walk-on deployment takes secondaryPositions, getOccupiedCoords() and the game's registered hexes from 7, 7, 7 down to 0, 0, 0. Full suite on your branch with it: 16,606 tests, 0 failures, 8 skipped.

Two things worth doing alongside it, which the patch deliberately does not touch since they are your code to change:

  1. Put the three redrawEntity calls in MovementDisplay back to being unconditional, and call it after setPosition(null) rather than before. That method exists to strip the sprites of an entity with no position, and it already null-checks internally.
  2. Drop the @author James Magnan tag from DeployStep.java. The project rule is that no individual is named in an @author tag or licence text.

Still digging on why an airborne aero's DEPLOY step comes out illegal. Will follow up separately.

# Conflicts:
#	megamek/src/megamek/client/ui/panels/phaseDisplay/DeploymentDisplay.java
#	megamek/src/megamek/common/units/Entity.java
#	megamek/src/megamek/server/totalWarfare/TWGameManager.java
# Conflicts:
#	megamek/src/megamek/server/totalWarfare/TWGameManager.java
Comment thread megamek/src/megamek/server/totalWarfare/TWGameManager.java Fixed
@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

After making the changes, still getting an NPE when pressing escape twice with a dropship at altitude.

@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

09:34:52,164 ERROR [megamek.MegaMek] {AWT-EventQueue-0}
megamek.MegaMek.lambda$main$1(MegaMek.java:118) - Uncaught java.lang.NullPointerException detected. Please open up an issue containing all logs, the game save file, and customs at https://github.com/MegaMek/megamek/issues . If Sentry is enabled, the error has also been logged.
java.lang.NullPointerException: Cannot read field "x" because "p" is null
at megamek.client.ui.clientGUI.boardview.sprite.isometric.IsometricSprite.drawOnto(IsometricSprite.java:140)
at megamek.client.ui.clientGUI.boardview.BoardView.drawIsometricSprites(BoardView.java:1474)
at megamek.client.ui.clientGUI.boardview.BoardView.drawHexes(BoardView.java:2533)
at megamek.client.ui.clientGUI.boardview.BoardView.drawHexes(BoardView.java:2483)
at megamek.client.ui.clientGUI.boardview.BoardView.draw(BoardView.java:1186)
at megamek.client.ui.clientGUI.boardview.BoardViewPanel.paintComponent(BoardViewPanel.java:85)
at java.desktop/javax.swing.JComponent.paint(JComponent.java:1128)
at java.desktop/javax.swing.JComponent.paintToOffscreen(JComponent.java:5318)
at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBufferedImpl(RepaintManager.java:1656)
at java.desktop/javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1631)
at java.desktop/javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1569)
at java.desktop/javax.swing.RepaintManager.paint(RepaintManager.java:1336)
at java.desktop/javax.swing.JComponent._paintImmediately(JComponent.java:5266)
at java.desktop/javax.swing.JComponent.paintImmediately(JComponent.java:5076)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:878)
at java.desktop/javax.swing.RepaintManager$4.run(RepaintManager.java:861)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:861)
at java.desktop/javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:834)
at java.desktop/javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:784)
at java.desktop/javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1897)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

Testing has shown the following:

  1. It is not the dropship causing the NPE. While guards are now in place, it is occurring on Aerospace fighters too.
  2. It is not occurring when Meks are used
  3. It is not occurring when I comment out setPosition(null) in MovementDisplay
  4. It occurs when clear is called and it is not keeping the unit (thus calling setPosition(null))
  5. The error is not occuring while stepping through the clear and completion of keypress event, but is occurring right after the keypress completes

@HammerGS

HammerGS commented Sep 12, 2026

Copy link
Copy Markdown
Member

Picking this up at 3b4b7ec123, against the stack trace and the five test findings above. Short version: the crash is real, it is one missing call, and it is entirely client-side. I have it reproduced in a throwaway test, fixed, and the full suite green with the fix: test 16,743 tests, 0 failures, 8 skipped.

First, an apology. Two comments ago I said the paint path could not throw. I traced it with a landed DropShip, and a landed DropShip takes the branch of IsometricSprite.drawOnto that never reads the hex location. The airborne branch does. That is the whole reason it looked clean to me and crashes for you, and it is also why finding (2) says Meks are fine: a Mek on flat ground takes the same safe branch. I also said "the three redrawEntity calls" - the Escape branch never had one, so it was not on my list.

Where it lands, and why only airborne units

IsometricSprite.java:140 is this line, inside the entity.isAirborne() branch:

g.drawImage(image, p.x, p.y - (int) (bv.DROP_SHADOW_DISTANCE * bv.getScale()), this);

p is bv.getHexLocation(entity.getPosition()), and getHexLocation returns null for a null position (BoardView.java:3454). The ground branches at the bottom of the method draw at the x, y the caller passed in and never touch p, so a Mek at elevation 0 survives with a ghost sprite instead of a crash. Note the middle branch, entity.getElevation() != 0, reads p too - a Mek that walked on to a building roof or a bridge would crash exactly the same way.

The flat EntitySprite already guards this (EntitySprite.java:295, if (ePos != null)); the isometric one is the odd one out.

Why the sprite is still there

BoardView.redrawEntity is what removes an entity's sprites when its position is null (BoardView.java:3676). The Escape path never calls it:

  • cancel() (MovementDisplay.java:421) works out keepDeployment, and when the path is only the DEPLOY step it is false.
  • clear(false) then takes the else if (wasWalkOn) branch (MovementDisplay.java:2011-2021): clearMarkedHexes, clearMovementSprites, markDeploymentHexes, setDeployed(false), setPosition(null), refreshButtons. No redrawEntity.
  • The key handler returns, Swing repaints, drawIsometricSprites finds the fighter's sprite still in the list, and line 140 dereferences the null.

That is findings (3), (4) and (5) exactly: comment out setPosition(null) and there is no null to hit; the throw is on the repaint, not inside the handler. Backspace does not crash because removeLastStep (MovementDisplay.java:2110-2112) does call redrawEntity, after the null, which is right. The Reset deployment button goes through the same clear(false) branch, so it has the same hole.

Numbers, from a throwaway test with a mocked BoardView recording the entity's position at each redrawEntity call. Aerospace fighter, altitude 3, path = DEPLOY only, then cancel():

deployed position redrawEntity calls
head 3b4b7ec123 false null (none)
with the patch false null one, position already null

Same harness on removeLastStep(): one call, position null, path length 0. Backspace is fine.

The fix

Two hunks. The first is the actual fix; the second is the guard so a stray sprite can never take the client down again. Applies cleanly to 3b4b7ec123 with git apply:

diff --git a/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java b/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java
--- a/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java
+++ b/megamek/src/megamek/client/ui/panels/phaseDisplay/MovementDisplay.java
@@ -2016,6 +2016,10 @@ public class MovementDisplay extends ActionPhaseDisplay {
                 markDeploymentHexes(currentlySelectedEntity);
                 currentlySelectedEntity.setDeployed(false);
                 currentlySelectedEntity.setPosition(null);
+                // The unit no longer has a position, so its sprites must go. redrawEntity() is the method that
+                // strips the sprites of an entity with a null position; without this call the BoardView keeps an
+                // IsometricSprite for the unit and the next repaint dereferences the null position.
+                clientgui.boardViews().forEach(bv -> ((BoardView) bv).redrawEntity(currentlySelectedEntity));
                 refreshButtons();
             }
             return;
diff --git a/megamek/src/megamek/client/ui/clientGUI/boardview/sprite/isometric/IsometricSprite.java b/megamek/src/megamek/client/ui/clientGUI/boardview/sprite/isometric/IsometricSprite.java
--- a/megamek/src/megamek/client/ui/clientGUI/boardview/sprite/isometric/IsometricSprite.java
+++ b/megamek/src/megamek/client/ui/clientGUI/boardview/sprite/isometric/IsometricSprite.java
@@ -117,6 +117,11 @@ public class IsometricSprite extends HexSprite {
         } else {
             p = bv.getHexLocation(entity.getSecondaryPositions().get(secondaryPos));
         }
+        if (p == null) {
+            // The entity has no position (for example a walk-on deployment that was taken back with Escape).
+            // There is nothing to draw; BoardView.redrawEntity(entity) is what removes this sprite.
+            return;
+        }
         Graphics2D g2 = (Graphics2D) g;
 
         if (onlyDetectedBySensors()) {

The guard alone would stop the crash, but it would leave the un-deployed unit's flat sprite painted at the hex it just left until something else redraws it, so I would take both.

Worth keeping as a regression test: the MovementDisplayTest setup already builds the display with a mocked ClientGUI. Have boardViews() return one mocked BoardView, capture the entity's position inside a doAnswer on redrawEntity, give the unit an owner (refreshButtons reaches getStartingPos, which reads the owner), set the private towedUnits field to an empty list, and assert one call arrives with the position already null. That is the test that would have caught this round.

Two things I hit on the way, same commit range

  • Board.getDeploymentCenter has east and west swapped, and the far edges land one hex off the board.
    Board.java:339-359. On a 16x17 board, from a throwaway test that also asks isLegalDeployment which zone each "centre" really lies in:

    zone returned on board actually lies in
    N (8, 0) yes N
    NE (0, 0) yes NW
    E (0, 8) yes W
    SE (0, 17) no -
    S (8, 17) no -
    SW (16, 17) no -
    W (16, 8) no -
    NW (16, 0) no -

    Only North is right. START_E is the high-x edge (Board.java:1043, c.getX() >= maxX - startingWidth), and the last row and column are height - 1 and width - 1. Two things read it: DeploymentHelper.setStartingFacing (:400-402), which is the new face-the-enemy logic, and MovementDisplay.selectEntity (:671), which centres the view on the zone for an undeployed walk-on unit. So a lance deploying on the East edge against an enemy on the West edge starts facing 2 (south-east), back to the enemy, where 4 or 5 is expected - measured, same throwaway. And the view centres on the wrong edge, or one hex past it.
    Suggest: NW (0, 0), N (w/2, 0), NE (w-1, 0), E (w-1, h/2), SE (w-1, h-1), S (w/2, h-1), SW (0, h-1), W (0, h/2), with w = getWidth() and h = getHeight().

  • The Reset deployment tooltip shows its message key.
    MovementDisplay.java:972 - setToolTipText("MovementDisplay.moveClearDeployTip") passes the key as the text, so hovering the button reads literally MovementDisplay.moveClearDeployTip. The key itself is in messages.properties:3676; it just needs Messages.getString(...) round it, like :966 two lines up.

Small

  • DeploymentDisplay.clear() (:1199-1201) calls selectEntity(currentEntity().getId()) and then checks currentEntity() != null on the next line. The only caller today guards it, so it cannot throw yet, but the null check is on the wrong side of the dereference.

Both throwaway tests were deleted after taking the numbers.

@HammerGS

Copy link
Copy Markdown
Member

Follow-up on the airborne walk-on path, at 3b4b7ec123 with the Escape patch above applied. I compiled the paths the display would build, with and without a DEPLOY step in front, and compared them. The drawing code is not the problem; it faithfully draws a path that compiles wrong. Three small hunks fix the compile, and the full suite is green with them: test 16,747 tests, 0 failures, 8 skipped.

One correction to my 2026-09-11 comment first: the elevation 999 I flagged on aero steps is by design. Aero.getElevation() returns 999 whenever the unit is airborne, so that was a red herring, and the compile was not as broken then as I read it.

What compiles today

Aerospace fighter, altitude 3, on a 16x17 ground map, facing south at 0303. Each row is one step: the movement type it gets, its MP, hexes counted as flown (nMoved), and whether it carries the first-step flag.

With the lobby default velocity of 0, which is what a fighter has unless someone typed a Start Velocity in the lobby:

path steps
DEPLOY, F DEPLOY MOVE_ILLEGAL (nMoved 1, velocity left 0), F MOVE_ILLEGAL
DEPLOY, ACC, F DEPLOY MOVE_ILLEGAL, ACC MOVE_ILLEGAL, F MOVE_ILLEGAL
control ACC, F (already deployed) ACC SAFE_THRUST, F SAFE_THRUST

So a fighter that walks on cannot plot anything: the deploy step is red before the first click, Accelerate is red too, and there is no way out. Two rules do it together. DeployStep returns COMPILE, so MoveStep.compileMove (MoveStep.java:557-562) treats the entry hex as a hex flown and sets nMoved to 1; then the aero check at MoveStep.java:1949-1953 says "velocity left 0 and hexes moved above 0" is illegal. After that MoveStep.java:1800 ("can't let players do an illegal move and use that to go less than velocity") makes every following step illegal because its predecessor is, Accelerate included.

With velocity 3 the deploy step is legal, but it still counts as a flown hex (nMoved 1, distance 1) and it takes the first-step flag, so the first real forward step is not the first step:

path steps
DEPLOY, F, F DEPLOY first, nMoved 1; F not first, nMoved 2; F nMoved 3
control F, F F first, nMoved 1; F nMoved 2

Landed fighter, altitude 0:

path steps
DEPLOY, TAKEOFF DEPLOY MOVE_WALK, 1 MP; TAKEOFF MOVE_ILLEGAL
control TAKEOFF TAKEOFF SAFE_THRUST

TAKEOFF is only legal on the first step (MoveStep.java:2659), and the deploy step is the first step, so a grounded fighter or DropShip that walks on cannot take off that turn. It also pays 1 MP for the deploy hex.

Two things worth knowing that are not bugs. The client never sets a velocity on walk-on; liftOff only sets altitude and movement mode (IAero.java:952), so velocity comes from the lobby Start Velocity field, and the server respects the same field (DeploymentServerHelper.java:97-102). A fighter walking on at velocity 0 has to Accelerate first, exactly like a normally deployed one. And MovePathHandler.java:1093 re-compiles the path on the server after placement with the same rules, so whatever the client compiles, the server agrees with.

The method: DEPLOY is placement, not movement

Three hunks, all in megamek.common.moves. Applies cleanly to 3b4b7ec123 with git apply.

  1. DeployStep.preCompilation: for any aerospace unit, airborne or landed, set MP to 0 and BREAK instead of COMPILE. That skips compileMove, so no hex is counted as flown, no straight-flight or distance counter moves, and velocity is untouched. The step keeps the position, facing, altitude and velocity it was seeded with from the entity. Ground units still COMPILE and pay terrain MP as you designed.
  2. MoveStep.evaluateFirstStep: a previous step that is DEPLOY counts as "no action yet", the same way a climb-mode change already does, so the step after it is the first step. That is what makes TAKEOFF legal after a walk-on and starts the velocity bookkeeping at the first real hex.
  3. MoveStep.compileIllegal: a DEPLOY step is MOVE_NONE by default. Without this a zero-MP deploy on the ground has no legality path at all, because the walk/run grading at MoveStep.java:2384 only runs when the step costs MP. A ground unit's deploy hex costs terrain MP, so it is still graded walk/run further down, unchanged.
diff --git a/megamek/src/megamek/common/moves/DeployStep.java b/megamek/src/megamek/common/moves/DeployStep.java
--- a/megamek/src/megamek/common/moves/DeployStep.java
+++ b/megamek/src/megamek/common/moves/DeployStep.java
@@ -59,7 +59,10 @@ class DeployStep implements PhasePass {
                                           final Entity entity,
                                           MoveStep prev,
                                           final CachedEntityState cachedEntityState) {
-        if (entity.isDropShip() && entity.isAeroLandedOnGroundMap()) {
+        if (entity.isAero()) {
+            // Walk-on placement of an aerospace unit, airborne or landed, is not a hex flown or taxied: no MP,
+            // and none of the velocity, straight-flight or distance bookkeeping that compileMove() would add.
+            // The step keeps the position, facing, altitude and velocity it was seeded with from the entity.
             moveStep.setMp(0);
             return PhasePassResult.BREAK;
         }
diff --git a/megamek/src/megamek/common/moves/MoveStep.java b/megamek/src/megamek/common/moves/MoveStep.java
--- a/megamek/src/megamek/common/moves/MoveStep.java
+++ b/megamek/src/megamek/common/moves/MoveStep.java
@@ -912,6 +912,11 @@ public class MoveStep implements Serializable {
             // A climb mode change is only meta info and does not count as an action
             setFirstStep();
 
+        } else if (prev.isFirstStep() && (prev.getType() == MoveStepType.DEPLOY)) {
+            // Walk-on placement puts the unit on the map; it is not an action, so the step after it is the first
+            // real step (take-off, first hex flown, first-step amnesties all key on this)
+            setFirstStep();
+
         } else if (prev.isFirstStep()
                    && prev.isTurning
                    && entity instanceof ConvInfantry infantry
@@ -2181,6 +2186,11 @@ public class MoveStep implements Serializable {
         if ((type == MoveStepType.CLIMB_MODE_ON) || (type == MoveStepType.CLIMB_MODE_OFF)) {
             movementType = prev.movementType;
         }
+        if (type == MoveStepType.DEPLOY) {
+            // Walk-on placement is legal in itself. An aerospace unit's deploy step costs nothing and stays
+            // MOVE_NONE; a ground unit's deploy hex costs terrain MP and is graded walk/run like any other hex below.
+            movementType = EntityMovementType.MOVE_NONE;
+        }
         // check for ejection (always legal?)
         if (type == MoveStepType.EJECT) {
             movementType = EntityMovementType.MOVE_NONE;

Same paths with the patch

Every walk-on path now matches its already-deployed control step for step, with DEPLOY in front as a free, zero-distance step:

path steps
velocity 0: DEPLOY, ACC, F DEPLOY SAFE_THRUST 0 MP nMoved 0; ACC first, SAFE_THRUST; F SAFE_THRUST nMoved 1
velocity 3: DEPLOY, F, F DEPLOY nMoved 0; F first, nMoved 1; F nMoved 2
velocity 3: DEPLOY then the A* pathfinder to 0306 DEPLOY, F, F, F - same as the control plus the deploy step
landed: DEPLOY, TAKEOFF DEPLOY MOVE_NONE 0 MP; TAKEOFF first, SAFE_THRUST - path legal
landed: DEPLOY, F DEPLOY MOVE_NONE 0 MP; F MOVE_WALK 1 MP
BipedMek: DEPLOY, F, F DEPLOY MOVE_WALK 1 MP (terrain, unchanged); F first, MOVE_WALK; F MOVE_WALK

On the drawing side nothing needs to change. BoardView.drawMovementData draws one StepSprite per step and then displayFlightPathIndicator, which projects the remaining velocity forward from the last step and is skipped outright when the last step is illegal (BoardView.java:4211). That skip is why the airborne path looked wrong: with the deploy step illegal there was no indicator and every arrow was the illegal colour. One cosmetic note: StepSprite has no case for DEPLOY, so the deploy hex itself draws nothing but the facing arrow when it is the last legal step. Fine as is, or a small "deploy" announcement like LOAD and LAND have if you want the entry hex marked.

Not proven here

  • I compiled paths and did not run a client, so the on-screen result is inferred from the compile, not seen. If it still draws wrong with the patch, a screenshot plus the path from megamek.log would pin it fast.
  • Spheroid DropShips at altitude: not measured. They go through the same DeployStep branch, and DropShips are excluded from walk-on right now anyway (RulesGame.java:166).

The throwaway test was deleted after taking the numbers.

@HammerGS

Copy link
Copy Markdown
Member

Revised to the rule Roundtop set out: a DropShip walks on for 0 MP whether it is landed or flying; a landed fighter walks on like a ground unit and pays the terrain cost of its entry hex; anything airborne gets a flight path with accelerate and decelerate working; and the server has to accept what the client sends. This supersedes the diff in my previous comment - only the DeployStep hunk changed, the two MoveStep hunks are the same. Full suite with it: test 16,744 tests, 0 failures, 8 skipped.

Why there is never a valid path today, end to end

I built the path the client builds for a walk-on fighter, velocity 0 from the lobby, altitude 3, facing south: DEPLOY, Accelerate, then 16 forward hexes (one velocity point on a ground map). Then I handed that path to MovePathHandler.processMovement on a real TWGameManager, with the entity reset to what the server holds before the packet arrives: not deployed, no position.

head 3b4b7ec123 with the patch
client path last step MOVE_ILLEGAL, isMoveLegal false last step SAFE_THRUST, velocity left 0, isMoveLegal true
after clipToPossible 0 steps 18 steps
server after processMovement not deployed, no position, velocity 0, MOVE_NONE deployed at 0417, altitude 3, velocity 1, MOVE_SAFE_THRUST, 16 hexes moved, done

The chain on the head: DeployStep returns COMPILE, so compileMove counts the entry hex as flown (nMoved 1). With velocity 0 the aero check at MoveStep.java:1949-1953 makes the deploy step illegal, and MoveStep.java:1800 then makes every later step illegal because its predecessor is. updateDonePanel (MovementDisplay.java:1665) clips the path to its legal prefix, gets nothing, and disables Move - that is "move is not selectable". Accelerate is red for the same reason. And if the path did get sent, the server's walk-on block only runs when the path still contains a DEPLOY step (MovePathHandler.java:980); a clipped path has none, so the unit would not even be placed.

The method, three hunks

  1. DeployStep.preCompilation: an airborne unit, or a DropShip in any state, gets MP 0 and BREAK, so compileMove never runs for it: no hex counted as flown, no straight-flight or distance counters, velocity untouched. Everything else, a landed fighter included, still COMPILEs and pays terrain MP like a Mek.
  2. MoveStep.evaluateFirstStep: a DEPLOY predecessor counts as "no action yet", so the step after it is the first step. This is what lets a landed fighter take off after walking on, and what starts the aero velocity bookkeeping at the first real hex.
  3. MoveStep.compileIllegal: DEPLOY is MOVE_NONE by default. A zero-MP step on the ground has no legality path otherwise, because the walk/run grading at MoveStep.java:2384 only runs when the step costs MP; a ground unit's deploy hex costs MP, so it is still graded walk/run further down.

Applies cleanly to 3b4b7ec123 with git apply:

diff --git a/megamek/src/megamek/common/moves/DeployStep.java b/megamek/src/megamek/common/moves/DeployStep.java
--- a/megamek/src/megamek/common/moves/DeployStep.java
+++ b/megamek/src/megamek/common/moves/DeployStep.java
@@ -59,7 +59,11 @@ class DeployStep implements PhasePass {
                                           final Entity entity,
                                           MoveStep prev,
                                           final CachedEntityState cachedEntityState) {
-        if (entity.isDropShip() && entity.isAeroLandedOnGroundMap()) {
+        if (entity.isAirborne() || entity.isDropShip()) {
+            // Walk-on placement of an airborne unit, or of a DropShip landed or not, is not a hex flown or taxied:
+            // no MP, and none of the velocity, straight-flight or distance bookkeeping that compileMove() would add.
+            // The step keeps the position, facing, altitude and velocity it was seeded with from the entity.
+            // A landed fighter walks on like a ground unit and pays the terrain cost of its entry hex.
             moveStep.setMp(0);
             return PhasePassResult.BREAK;
         }
diff --git a/megamek/src/megamek/common/moves/MoveStep.java b/megamek/src/megamek/common/moves/MoveStep.java
--- a/megamek/src/megamek/common/moves/MoveStep.java
+++ b/megamek/src/megamek/common/moves/MoveStep.java
@@ -912,6 +912,11 @@ public class MoveStep implements Serializable {
             // A climb mode change is only meta info and does not count as an action
             setFirstStep();
 
+        } else if (prev.isFirstStep() && (prev.getType() == MoveStepType.DEPLOY)) {
+            // Walk-on placement puts the unit on the map; it is not an action, so the step after it is the first
+            // real step (take-off, first hex flown, first-step amnesties all key on this)
+            setFirstStep();
+
         } else if (prev.isFirstStep()
                    && prev.isTurning
                    && entity instanceof ConvInfantry infantry
@@ -2181,6 +2186,11 @@ public class MoveStep implements Serializable {
         if ((type == MoveStepType.CLIMB_MODE_ON) || (type == MoveStepType.CLIMB_MODE_OFF)) {
             movementType = prev.movementType;
         }
+        if (type == MoveStepType.DEPLOY) {
+            // Walk-on placement is legal in itself. An aerospace unit's deploy step costs nothing and stays
+            // MOVE_NONE; a ground unit's deploy hex costs terrain MP and is graded walk/run like any other hex below.
+            movementType = EntityMovementType.MOVE_NONE;
+        }
         // check for ejection (always legal?)
         if (type == MoveStepType.EJECT) {
             movementType = EntityMovementType.MOVE_NONE;

Every case, step by step, with the patch

Each walk-on path was compiled next to the same path for an already-deployed unit (the control). Airborne fighter at altitude 3:

path result
velocity 0: DEPLOY, ACC, F DEPLOY SAFE_THRUST 0 MP; ACC first step, velocity left 1; F SAFE_THRUST - matches control
velocity 0: DEPLOY, DEC DEC illegal, velocity left -1 - matches control (nothing to shed)
velocity 2: DEPLOY, DEC, F DEC first step, velocity left 1; F SAFE_THRUST - matches control
velocity 3: DEPLOY then the A* pathfinder to a hex 3 south DEPLOY, F, F, F - the control plus the deploy step

Landed fighter and DropShips:

path result
landed fighter: DEPLOY, TAKEOFF DEPLOY MOVE_WALK 1 MP (terrain); TAKEOFF first step, SAFE_THRUST - path legal
landed fighter: DEPLOY, F DEPLOY MOVE_WALK 1 MP; F MOVE_WALK 1 MP - same as the control plus the deploy hex
landed spheroid DropShip: DEPLOY MOVE_NONE, 0 MP, legal
spheroid DropShip at altitude 4: DEPLOY, HOVER DEPLOY SAFE_THRUST 0 MP; HOVER first step, 2 MP - matches control
BipedMek: DEPLOY, F, F DEPLOY MOVE_WALK 1 MP; F first step - ground units unchanged

The DropShip rows are compile-only; DropShips are still excluded from walk-on by RulesGame.java:166, so they cannot reach this code until that comes out.

Flight path, accelerate and decelerate, display

Nothing extra is needed to get a flight path rather than a ground path. Which path a click builds is decided by the entity, not the step: MovePath.getNextMoves (MovePath.java:1663) generates the aero move set (F, LF, RF, LLF, RRF and so on) for any isAero() unit, and the compile takes the airborne branch whenever the previous step's altitude is above 0 (MoveStep.java:1766), which the DEPLOY step is because it is seeded from the entity's altitude. The pathfinder row in the table above is that path.

Accelerate and decelerate: the buttons already open up after a deploy-only path through pathZeroOrDeploy() (MovementDisplay.java:3714), and with the patch the ACC and DEC steps compile exactly as they do for a normally deployed fighter. On the head they were red because the illegal deploy step poisoned them, not because of the buttons.

Display: BoardView.drawMovementData draws one sprite per step and then the remaining-velocity indicator, which is skipped when the last step is illegal (BoardView.java:4211). Once the steps compile legal, the arrows and the indicator follow. The deploy hex itself draws no marker - StepSprite has no case for DEPLOY - only the facing arrow when it is the last legal step. A small "deploy" announcement like LOAD or LAND have would mark the entry hex, if you want it.

Server side

MovePathHandler finds the DEPLOY step, places the unit through DeploymentServerHelper.processDeployment with the step's altitude and facing (:980-1035), then re-compiles the whole path against the placed entity with the same rules (:1093) and walks it. The probe above is that path with a fresh TWGameManager; the unit ends where the client said, at the client's altitude, with the client's velocity. The client never sets velocity on walk-on - liftOff only sets altitude and movement mode (IAero.java:952) - so a fighter whose lobby Start Velocity is 0 must Accelerate on its first walk-on turn, the same as a normally deployed one. The server reads the same lobby value (DeploymentServerHelper.java:97-102).

Not proven here

  • No client was run; the on-screen path is inferred from what compiles.
  • The probe called MovePathHandler directly. It did not go through receiveMovement and the turn check, which the tester has already exercised by reaching the movement phase with a walk-on fighter.
  • Spheroid DropShips are compile-only, as noted.

Both throwaway tests were deleted after taking the numbers.

@TenkawaBC

Copy link
Copy Markdown
Collaborator Author

One issue remaining that I have found. When switching to jumping movement, the cost of the first hex terrain is still applied. eg: clicking into a light woods hex to deploy, click jump, then click the next hex it shows 3, not 2. it correctly ignores the terrain and elevation of the next hex when jumping, so this is only when the deploy step is used and the unit is jumping.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Assisted Development This project/code contains AI use under the supervision of a human developer. With Human testing. New Core Rules Summer 2026 Core Rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants