From 99f7e2f8aa3217af811e089d0b2a6b33235e6580 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Thu, 13 Feb 2025 12:52:02 -0500 Subject: [PATCH 1/9] Issue 6450: RFE - Tow from Lobby/Depoloyment - Initial Lobby UI --- .../client/ui/swing/lobby/LobbyMekPopup.java | 24 +++++++++++++++++++ .../ui/swing/lobby/LobbyMekPopupActions.java | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java index d65cda11c8e..e405d415b90 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java @@ -93,6 +93,7 @@ class LobbyMekPopup { static final String LMP_C3CM = "C3CM"; static final String LMP_SQUADRON = "SQUADRON"; static final String LMP_LOAD = "LOAD"; + static final String LMP_TOW = "TOW"; static final String LMP_FADDTO = "FADDTO"; static final String LMP_FREMOVE = "FREMOVE"; static final String LMP_FPROMOTE = "FPROMOTE"; @@ -216,6 +217,9 @@ static ScalingPopup getPopup(List entities, List forces, ActionLi popup.add(ScalingPopup.spacer()); popup.add(changeOwnerMenu(!entities.isEmpty() || !forces.isEmpty(), clientGui, listener, entities, forces)); popup.add(loadMenu(clientGui, true, listener, joinedEntities)); + if (entities.size() == 1) { + popup.add(towMenu(clientGui, true, listener, entities.get(0))); + } if (accessibleCarriers) { popup.add( @@ -355,6 +359,26 @@ private static JMenu loadMenu(ClientGUI cg, boolean enabled, ActionListener list return menu; } + /** + * Returns the "Tow" submenu, allowing towing + */ + private static JMenu towMenu(ClientGUI cg, boolean enabled, ActionListener listener, + Entity entity) { + Game game = cg.getClient().getGame(); + JMenu menu = new JMenu("Towed by"); + if (enabled && entity.isTrailer()) { + game.getEntitiesVector().stream() + .filter( e -> e.canTow(entity.getId())) + .filter(e -> !e.equals(entity)) + .forEach(e -> menu.add(menuItem("" + e.getShortNameRaw() + idString(game, e.getId()), + LMP_TOW + "|" + e.getId() + ":-1|" + entity.getId(), enabled, listener + + ))); + } + menu.setEnabled(enabled && (menu.getItemCount() > 0)); + return menu; + } + /** * Returns the "Load ProtoMek" submenu */ diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java index 07fc2ed0587..2010ffeb60e 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java @@ -244,6 +244,10 @@ private void multiEntityAction(String command, Set entities, String info lobby.lobbyActions.load(entities, info); break; + case LMP_TOW: + //lobby.lobbyActions.tow(entities, info); + break; + case LMP_UNLOAD: Set updateCandidates = new HashSet<>(); lobby.disembarkAll(entities); From d5dd71af7b9496f1657a3f2cf9204a85a73e2316 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:49:57 -0500 Subject: [PATCH 2/9] Issue 6450: RFE - Tow from Lobby/Depoloyment - Deployable --- megamek/src/megamek/client/Client.java | 7 +++ .../client/ui/swing/lobby/ChatLounge.java | 14 ++++++ .../client/ui/swing/lobby/LobbyActions.java | 31 ++++++++++++ .../client/ui/swing/lobby/LobbyErrors.java | 47 ++++++++++--------- .../client/ui/swing/lobby/LobbyMekPopup.java | 8 ++-- .../ui/swing/lobby/LobbyMekPopupActions.java | 9 ++-- .../common/net/enums/PacketCommand.java | 1 + .../server/totalwarfare/TWGameManager.java | 27 +++++++++++ 8 files changed, 115 insertions(+), 29 deletions(-) diff --git a/megamek/src/megamek/client/Client.java b/megamek/src/megamek/client/Client.java index 788d008fc7b..85b17aa4597 100644 --- a/megamek/src/megamek/client/Client.java +++ b/megamek/src/megamek/client/Client.java @@ -476,6 +476,13 @@ public void sendLoadEntity(int id, int loaderId, int bayNumber) { send(new Packet(PacketCommand.ENTITY_LOAD, id, loaderId, bayNumber)); } + /** + * Sends a "tow entity" packet + */ + public void sendTowEntity(int id, int tractorId) { + send(new Packet(PacketCommand.ENTITY_TOW, id, tractorId)); + } + public void sendExplodeBuilding(Building.DemolitionCharge charge) { send(new Packet(PacketCommand.BLDG_EXPLODE, charge)); } diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java index f2bf7bd5889..bbd9577da91 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java +++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java @@ -1542,6 +1542,20 @@ void offloadFromDifferentOwner(Entity entity, Collection updateCandidate } } + /** + * Set the provided trailer to be towed by the tractor. + */ + void towBy(Entity trailer, int tractorId) { + Entity tractor = game().getEntity(tractorId); + if (tractor == null || !tractor.canTow(trailer.getId())) { + return; + } + + getLocalClient(trailer).sendTowEntity(trailer.getId(), tractor.getId()); + // TODO: it would probably be a good idea + // to disable some settings for loaded units in customMekDialog + } + /** * Sends the entities in the given Collection to the Server. * Sends only those that can be edited, i.e. the player's own diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java index f5c7c12a683..e453d34ea67 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java @@ -661,6 +661,37 @@ public void load(Collection selEntities, String info) { } } + /** + * + * @param trailer Entity to be towed + * @param info + */ + public void tow(Entity trailer, String info) { + StringTokenizer sTow = new StringTokenizer(info, ":"); + int tractorId = Integer.parseInt(sTow.nextToken()); + Entity tractor = game().getEntity(tractorId); + // Remove those entities from the candidates that are already carried by that + // loader + if (trailer == null || tractor == null || trailer.getTractor() == tractorId) { + return; + } + + // If a unit of the selected units is currently loaded onto another, 2nd unit of + // the selected + // units, do not continue. The player should unload units first. This would + // require + // a server update offloading that second unit AND embarking it. Currently not + // possible + // as a single server update and updates for one unit shouldn't be chained. + if (tractor.getTowing() != Entity.NONE || trailer.getTractor() != Entity.NONE) { + LobbyErrors.showNoDualTow(frame()); + } + + StringBuilder errorMsg = new StringBuilder(); + + lobby.towBy(trailer, tractorId); + } + /** Asks for a new name for the provided forceId and applies it. */ void forceRename(int forceId) { if (forceId == Force.NO_FORCE) { diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyErrors.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyErrors.java index 21ed0de06d9..63d5a9cbee9 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyErrors.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyErrors.java @@ -25,7 +25,7 @@ /** Contains static methods that show common info/error messages for the lobby. */ public final class LobbyErrors { - + private static final String SINGLE_OWNER = "For this action, the selected units must have a single owner."; private static final String CONFIG_ENEMY = "Cannot configure units of other players except units of your bots."; private static final String VIEW_HIDDEN = "Cannot view or set details on hidden units."; @@ -51,51 +51,56 @@ public final class LobbyErrors { "converted to SBF Formations. Please select only the topmost forces to be converted, no subforces. " + "A converted force must conform to the rules given in Interstellar Operations. Conversion " + "will typically work with companies created in the Force Generator."; + private static final String NO_DUAL_TOW = "Both units must have an open appropriate tow hitch."; public static void showOnlyOwnBot(JFrame owner) { JOptionPane.showMessageDialog(owner, ONLY_OWN_BOT); } - + public static void showOnlySingleEntityOrForce(JFrame owner) { JOptionPane.showMessageDialog(owner, SINGLE_UNIT_OR_FORCE); } - + public static void showSingleOwnerRequired(JFrame owner) { JOptionPane.showMessageDialog(owner, SINGLE_OWNER); } - + public static void showForceNoAttachSubForce(JFrame owner) { JOptionPane.showMessageDialog(owner, FORCE_ATTACH_TOSUB); } - + public static void showOnlyTeam(JFrame owner) { JOptionPane.showMessageDialog(owner, ONLY_TEAM); } - + public static void showOnlyC3M(JFrame owner) { JOptionPane.showMessageDialog(owner, ONLY_C3M); } - + public static void showNoDualLoad(JFrame owner) { JOptionPane.showMessageDialog(owner, NO_DUAL_LOAD); } - + + public static void showNoDualTow(JFrame owner) { + JOptionPane.showMessageDialog(owner, NO_DUAL_TOW); + } + public static void showNoSuchBay(JFrame owner) { JOptionPane.showMessageDialog(owner, NO_BAY); } - + public static void showSquadronTooMany(JFrame owner) { JOptionPane.showMessageDialog(owner, Messages.getString("FighterSquadron.toomany")); } - + public static void showOnlyFighter(JFrame owner) { JOptionPane.showMessageDialog(owner, ONLY_FIGHTERS); } - + public static void showLoadOnlyAllied(JFrame owner) { JOptionPane.showMessageDialog(owner, LOAD_ONLY_ALLIED); } - + public static void showExceedC3Capacity(JFrame owner) { JOptionPane.showMessageDialog(owner, EXCEED_C3_CAPACITY); } @@ -107,35 +112,35 @@ public static void showSameC3(JFrame owner) { public static void showCannotConfigEnemies(JFrame owner) { JOptionPane.showMessageDialog(owner, CONFIG_ENEMY); } - + public static void showCannotViewHidden(JFrame owner) { JOptionPane.showMessageDialog(owner, VIEW_HIDDEN); } - + public static void showSingleUnit(JFrame owner, String action) { JOptionPane.showMessageDialog(owner, MessageFormat.format(SINGLE_UNIT, action)); } - + public static void showTenUnits(JFrame owner) { JOptionPane.showMessageDialog(owner, TEN_UNITS); } - + public static void showHeatTracking(JFrame owner) { JOptionPane.showMessageDialog(owner, HEAT_TRACKING); } - + public static void showOnlyMeks(JFrame owner) { JOptionPane.showMessageDialog(owner, ONLY_MEKS); } - + public static void showOnlyTeammate(JFrame owner) { JOptionPane.showMessageDialog(owner, FORCE_ASSIGN_ONLYTEAM); } - + public static void showOnlyEntityOrForce(JFrame owner) { JOptionPane.showMessageDialog(owner, ENTITY_OR_FORCE); } - + public static void showOnlyEmptyForce(JFrame owner) { JOptionPane.showMessageDialog(owner, FORCE_EMPTY); } @@ -145,4 +150,4 @@ public static void showSBFConversion(JFrame owner) { } private LobbyErrors() { } -} \ No newline at end of file +} diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java index e405d415b90..171469764e3 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java @@ -368,10 +368,10 @@ private static JMenu towMenu(ClientGUI cg, boolean enabled, ActionListener liste JMenu menu = new JMenu("Towed by"); if (enabled && entity.isTrailer()) { game.getEntitiesVector().stream() - .filter( e -> e.canTow(entity.getId())) - .filter(e -> !e.equals(entity)) - .forEach(e -> menu.add(menuItem("" + e.getShortNameRaw() + idString(game, e.getId()), - LMP_TOW + "|" + e.getId() + ":-1|" + entity.getId(), enabled, listener + .filter( tractor -> tractor.canTow(entity.getId())) + .filter(tractor -> !tractor.equals(entity)) + .forEach(tractor -> menu.add(menuItem("" + tractor.getShortNameRaw() + idString(game, tractor.getId()), + LMP_TOW + "|" + tractor.getId() + ":-1|" + entity.getId(), enabled, listener ))); } diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java index 2010ffeb60e..3ce95a4db2b 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java @@ -65,6 +65,7 @@ public void actionPerformed(ActionEvent e) { switch (command) { // Single entity commands case LMP_CONFIGURE: + case LMP_TOW: if (!entities.isEmpty()) { Entity randomSelected = entities.stream().findAny().get(); singleEntityAction(command, randomSelected, info); @@ -244,10 +245,6 @@ private void multiEntityAction(String command, Set entities, String info lobby.lobbyActions.load(entities, info); break; - case LMP_TOW: - //lobby.lobbyActions.tow(entities, info); - break; - case LMP_UNLOAD: Set updateCandidates = new HashSet<>(); lobby.disembarkAll(entities); @@ -493,6 +490,10 @@ private void singleEntityAction(String command, Entity entity, String info) { lobby.lobbyActions.customizeMek(entity); break; + case LMP_TOW: + lobby.lobbyActions.tow(entity, info); + break; + } } diff --git a/megamek/src/megamek/common/net/enums/PacketCommand.java b/megamek/src/megamek/common/net/enums/PacketCommand.java index 3e37e6f71ad..405ed8b8293 100644 --- a/megamek/src/megamek/common/net/enums/PacketCommand.java +++ b/megamek/src/megamek/common/net/enums/PacketCommand.java @@ -151,6 +151,7 @@ public enum PacketCommand { ENTITY_MOUNTED_FACING_CHANGE, SENDING_AVAILABLE_MAP_SIZES, ENTITY_LOAD, + ENTITY_TOW, ENTITY_NOVA_NETWORK_CHANGE, RESET_ROUND_DEPLOYMENT, SENDING_TAG_INFO, diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index a815bea7c36..45c621deff0 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -768,6 +768,10 @@ public void handlePacket(int connId, Packet packet) { receiveEntityLoad(packet, connId); resetPlayersDone(); break; + case ENTITY_TOW: + receiveEntityTow(packet, connId); + resetPlayersDone(); + break; case ENTITY_MODECHANGE: receiveEntityModeChange(packet, connId); break; @@ -26906,6 +26910,29 @@ private void receiveEntityLoad(Packet c, int connIndex) { } } + /** + * loads an entity into another one. Meant to be called from the chat lounge + * + * @param c the packet to be processed + * @param connIndex the id for connection that received the packet. + */ + private void receiveEntityTow(Packet c, int connIndex) { + int trailerId = (Integer) c.getObject(0); + int tractorId = (Integer) c.getObject(1); + Entity trailer = getGame().getEntity(trailerId); + Entity tractor = getGame().getEntity(tractorId); + + if ((trailer != null) && (tractor != null)) { + towUnit(tractor, trailer); + // In the chat lounge, notify players of customizing of unit + if (getGame().getPhase().isLounge()) { + ServerLobbyHelper.entityUpdateMessage(trailer, getGame()); + // Set this so units can be unloaded in the first movement phase + trailer.setLoadedThisTurn(false); + } + } + } + /** * * @param c the packet to be processed From 0de1de6d81db7e95d6cb26af7ff3b0b75e109c38 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:58:16 -0500 Subject: [PATCH 3/9] Issue 6540: Warnings for towing in lobby --- .../client/ui/swing/DeploymentDisplay.java | 7 + .../client/ui/swing/TowLinkWarning.java | 184 ++++++++++++++++++ megamek/src/megamek/common/Entity.java | 26 ++- .../client/ui/swing/TowLinkWarningTest.java | 134 +++++++++++++ 4 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 megamek/src/megamek/client/ui/swing/TowLinkWarning.java create mode 100644 megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java diff --git a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java index 983127dce6f..8be093822a2 100644 --- a/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java +++ b/megamek/src/megamek/client/ui/swing/DeploymentDisplay.java @@ -246,6 +246,7 @@ public void selectEntity(int en) { clientgui.updateFiringArc(ce()); clientgui.showSensorRanges(ce()); computeCFWarningHexes(ce()); + computeTowLinkBreakageHexes(ce()); } else { disableButtons(); setNextEnabled(true); @@ -260,6 +261,12 @@ private void computeCFWarningHexes(Entity ce) { clientgui.showCollapseWarning(warnList); } + private void computeTowLinkBreakageHexes(Entity ce) { + Game game = clientgui.getClient().getGame(); + List warnList = TowLinkWarning.findTowLinkIssues(game, ce, game.getBoard()); + clientgui.showCollapseWarning(warnList); + } + /** Enables relevant buttons and sets up for your turn. */ private void beginMyTurn() { clientgui.maybeShowUnitDisplay(); diff --git a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java new file mode 100644 index 00000000000..a60b5f4642e --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek 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. + * + * MegaMek 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 MegaMek. If not, see . + */ + +package megamek.client.ui.swing; + +import megamek.common.*; +import megamek.logging.MMLogger; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * When deploying units that are towing units, let's mark hexes that would break the tow linkage + * with warning. or something + */ +public class TowLinkWarning { + private final static MMLogger logger = MMLogger.create(TowLinkWarning.class); + + /** + * + * This is + * used by the {@link MovementDisplay} class. + * + * @param game {@link Game} provided by the phase display class + * @param entity {@link Entity} currently selected in the deployment phase. + * @param board {@link Board} board object with building data. + * + * @return returns a list of {@link Coords} that where warning flags + * should be placed. + */ + public static List findTowLinkIssues(Game game, Entity entity, Board board) { + List warnList = new ArrayList(); + + List validTractorCoords = findCoordsForTractor(game, entity, board); + List validTrailerCoords = findCoordsForTrailer(game, entity, board); + + //int tractorId = entity.getTowedBy(); + //Entity tractor = game.getEntity(tractorId); + //if (tractorId != Entity.NONE && tractor != null) { + // validTrailerCoords = findCoordsForTrailer(game, tractor, board); + //} + + if (validTractorCoords == null && validTrailerCoords == null) { + return warnList; + } + var boardHeight = board.getHeight(); + var boardWidth = board.getWidth(); + for (int x = 0; x < boardWidth; x++) { + for (int y = 0; y < boardHeight; y++) { + Coords coords = new Coords(x, y); + if (board.isLegalDeployment(coords, entity)) { + if (validTractorCoords != null && !validTractorCoords.contains(coords)) { + warnList.add(coords); + } else if (validTrailerCoords != null && !validTrailerCoords.contains(coords)) { + warnList.add(coords); + } + } + } + } + + return warnList; + } + + /** + * When deploying a tractor, return the coords that would let it attach to its assigned trailer. + * @param game + * @param tractor + * @param board + * @return List of coords that a tractor could go, empty if there are none. Null if the tractor isn't a tractor or pulling anything + */ + protected static List findCoordsForTractor(Game game, Entity tractor, Board board) { + int trailerId = tractor.getTowing(); + Entity trailer = game.getEntity(trailerId); + if (trailerId == Entity.NONE || trailer == null || trailer.getDeployRound() != tractor.getDeployRound()) { + return null; + } + + List validCoords = new ArrayList(); + + if (trailer.isDeployed()) { + //Can they stack? If so, add the trailer's hex as valid + if (Compute.stackingViolation(game, tractor.getId(), tractor.getPosition(), false) == null) { + validCoords.add(trailer.getPosition()); + } + validCoords.add(trailer.getPosition().translated(trailer.getFacing(), 1)); + // Let's add the typical adjacent hexes + validCoords.addAll(trailer.getPosition().allAdjacent()); + + //Except the one behind the trailer, a tractor can't be there! + validCoords.remove(trailer.getPosition().translated(trailer.getFacing(), -1)); + + } else { + var boardHeight = board.getHeight(); + var boardWidth = board.getWidth(); + for (int x = 0; x < boardWidth; x++) { + for (int y = 0; y < boardHeight; y++) { + Coords coords = new Coords(x, y); + if (board.isLegalDeployment(coords, tractor)) { + int facing = tractor.getFacing(); + // Can our trailer deploy in any of the adjacent hexes? + if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, trailer) && !trailer.isLocationProhibited(c))) { + validCoords.add(coords); + } + } + } + } + } + + return validCoords; + } + + /** + * When deploying a tractor, return the coords that would let it attach to its assigned trailer. + * @param game + * @param trailer + * @param board + * @return List of coords that a tractor could go, empty if there are none. Null if the tractor isn't a tractor or pulling anything + */ + protected static List findCoordsForTrailer(Game game, Entity trailer, Board board) { + int tractorId = trailer.getTractor(); + Entity tractor = game.getEntity(tractorId); + if (tractorId == Entity.NONE || tractor == null || tractor.getDeployRound() != trailer.getDeployRound()) { + return null; + } + + List validCoords = new ArrayList(); + + if (tractor.isDeployed()) { + //Can they stack? If so, add the trailer's hex as valid + if (Compute.stackingViolation(game, trailer.getId(), trailer.getPosition(), false) == null) { + validCoords.add(tractor.getPosition()); + } + // Let's add the hex behind us + validCoords.add(tractor.getPosition().translated(tractor.getFacing(), -1)); + //validCoords.addAll(tractor.getPosition().allAdjacent()); + + } else { + var boardHeight = board.getHeight(); + var boardWidth = board.getWidth(); + for (int x = 0; x < boardWidth; x++) { + for (int y = 0; y < boardHeight; y++) { + Coords coords = new Coords(x, y); + if (board.isLegalDeployment(coords, trailer)) { + // Can our trailer deploy in any of the adjacent hexes? + if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, tractor) && !tractor.isLocationProhibited(c))) { + validCoords.add(coords); + } + } + } + } + } + + return validCoords; + } + + private HashSet getAllCoords(Board board) { + var boardHeight = board.getHeight(); + var boardWidth = board.getWidth(); + var coordsSet = new HashSet(); + for (int x = 0; x < boardWidth; x++) { + for (int y = 0; y < boardHeight; y++) { + coordsSet.add(new Coords(x, y)); + } + } + return coordsSet; + } +} diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 15a0283fc55..9138e705011 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -15049,18 +15049,34 @@ public List getConnectedUnits() { public void towUnit(int id) { Entity towed = game.getEntity(id); // Add this trailer to the connected list for all trailers already in this train - for (int tr : getAllTowedUnits()) { + List otherTrailerIds = getAllTowedUnits(); + List otherTrailers = new ArrayList<>(); + for (int tr : otherTrailerIds) { Entity trailer = game.getEntity(tr); trailer.connectedUnits.add(id); + otherTrailers.add(trailer); } addTowedUnit(id); towed.setTractor(getId()); + Entity towingEnt = null; // Now, find the transporter and the actual towing entity (trailer or tractor) - Entity towingEnt = game.getEntity(towed.towedBy); + // If it's the only thing being towed, this entity is towing it + + if (otherTrailers.isEmpty()) { + towingEnt = this; + } else { + for (Entity trailer : otherTrailers) { + if (trailer.getTowing() == Entity.NONE) { + towingEnt = trailer; + } + } + } if (towingEnt != null) { - Transporter hitch = towingEnt.getHitchById(towed.getTargetBay()); - if (hitch != null) { - hitch.load(towed); + for (Transporter transporter : towingEnt.getTransports()) { + if (transporter instanceof TankTrailerHitch hitch) { + hitch.load(towed); + towingEnt.setTowing(id); + } } } } diff --git a/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java b/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java new file mode 100644 index 00000000000..3685f48bdf4 --- /dev/null +++ b/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek 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. + * + * MegaMek 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 MegaMek. If not, see . + */ + +package megamek.client.ui.swing; + +import megamek.common.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class TowLinkWarningTest { + Game mockGame; + Board mockBoard; + Entity mockTractor; + Entity mockTrailer; + + + @BeforeEach + void initialize() { + mockTractor = mock(Entity.class); + when(mockTractor.getId()).thenReturn(1); + mockTrailer = mock(Entity.class); + when(mockTrailer.getId()).thenReturn(2); + mockGame = mock(Game.class); + mockBoard = mock(Board.class); + when(mockBoard.getHeight()).thenReturn(5); + when(mockBoard.getWidth()).thenReturn(5); + when(mockBoard.isLegalDeployment(any(Coords.class), any(Entity.class))).thenReturn(true); + when(mockGame.getBoard()).thenReturn(mockBoard); + when(mockGame.getEntity(1)).thenReturn(mockTractor); + when(mockGame.getEntity(2)).thenReturn(mockTrailer); + } + + /** + * @see TowLinkWarning#findCoordsForTractor(Game, Entity, Board) + */ + @Nested + class testFindCoordsForTractor { + + @Test + void testNoTrailer() { + // Arrange + TowLinkWarning towLinkWarning = new TowLinkWarning(); + + // Act + List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + + + // Assert + assertNull(coords); + } + + @Test + void testTrailerNotDeployedYet() { + // Arrange + TowLinkWarning towLinkWarning = new TowLinkWarning(); + + when(mockTractor.getTowing()).thenReturn(2); + + // Act + List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + + + // Assert + assertEquals(25, coords.size()); + } + + @Test + void testTrailerDeployed() { + // Arrange + TowLinkWarning towLinkWarning = new TowLinkWarning(); + + when(mockTractor.getTowing()).thenReturn(2); + when(mockTrailer.isDeployed()).thenReturn(true); + when(mockTrailer.getPosition()).thenReturn(new Coords(3, 3)); + + // Act + List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + + + // Assert + assertEquals(7, coords.size()); + } + + void testLargeTrailerDeployed() { + // Arrange + TowLinkWarning towLinkWarning = new TowLinkWarning(); + + when(mockTractor.getTowing()).thenReturn(2); + when(mockTrailer.isDeployed()).thenReturn(true); + when(mockTrailer.getPosition()).thenReturn(new Coords(3, 3)); + + List testCoords = null; + + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + compute.when(() -> Compute.stackingViolation(any(Game.class), anyInt(), any(Coords.class), anyBoolean())).thenReturn(mock(Entity.class)); + + // Act + testCoords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + } + + // Assert + assertEquals(6, testCoords.size()); + } + } + +} From 8602bcb79b9446212aba1cc314bc0d2b50388d61 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:01:37 -0500 Subject: [PATCH 4/9] Issue 6540: RFE - Additional Lobby-Towing Support - Detach Tractor/Trailer and UI --- .../i18n/megamek/client/messages.properties | 1 + .../client/ui/swing/lobby/ChatLounge.java | 67 ++++++++++++++++++- .../ui/swing/lobby/LobbyMekCellFormatter.java | 31 ++++++--- .../client/ui/swing/lobby/LobbyMekPopup.java | 35 +++++++++- .../ui/swing/lobby/LobbyMekPopupActions.java | 21 ++++-- megamek/src/megamek/common/Entity.java | 3 +- 6 files changed, 141 insertions(+), 17 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index aeb687031fa..575af8488c0 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -623,6 +623,7 @@ ChatLounge.AlertExistsBot.message=That name is already used by another player or ChatLounge.AlertExistsBot.title=Error ChatLounge.AutoEjectDisabled=Auto Eject Disabled ChatLounge.aboard=aboard +ChatLounge.towedBy=towed by ChatLounge.board.generatedMessage=This board is generated using the current Generated
Map Settings when the game is started. ChatLounge.board.randomlySelectedMessage=This board is selected randomly from the following list
of boards when the game is started:
ChatLounge.board.serverSide=Server-side board diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java index bbd9577da91..bd3ff2a1e95 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java +++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2000-2006 Ben Mazur (bmazur@sev.org) * Copyright © 2013 Edward Cullen (eddy@obsessedcomputers.co.uk) - * Copyright (c) 2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * @@ -1493,6 +1493,71 @@ void disembark(Entity entity, Collection updateCandidates) { } } + /** + * Have the given entities detach from their tractors if it's a trailer. + * Entities that are modified and need an update to be sent to the server + * are added to the given updateCandidates. + */ + void detachFromTractors(Set trailers, Collection updateCandidates) { + for (Entity trailer : trailers) { + detachFromTractor(trailer, updateCandidates); + } + } + + /** + * Have the given entity detach from its tractor if it's a trailer. + * Entities that are modified and need an update to be sent to the server + * are added to the given updateCandidates. + */ + void detachFromTractor(Entity trailer, Collection updateCandidates) { + if (trailer.getTowedBy() == Entity.NONE) { + return; + } + Entity tractor = game().getEntity(trailer.getTowedBy()); + disconnectTrain(tractor, trailer, updateCandidates); + } + + /** + * Have the given entities detach their towed trailers. + * Entities that are modified and need an update to be sent to the server + * are added to the given updateCandidates. + */ + void detachTrailers(Set tractors, Collection updateCandidates) { + for (Entity tractor : tractors) { + detachTrailer(tractor, updateCandidates); + } + } + + /** + * Have the given entity detach a towed trailer. + * Entities that are modified and need an update to be sent to the server + * are added to the given updateCandidates. + */ + void detachTrailer(Entity tractor, Collection updateCandidates) { + if (tractor.getTowing() == Entity.NONE) { + return; + } + Entity trailer = game().getEntity(tractor.getTowing()); + disconnectTrain(tractor, trailer, updateCandidates); + } + + private void disconnectTrain(Entity tractor, Entity trailer, Collection updateCandidates) { + if (tractor != null && trailer != null) { + List otherTowedUnitIds = tractor.getAllTowedUnits(); + tractor.disconnectUnit(trailer.getId()); + updateCandidates.add(trailer); + updateCandidates.add(tractor); + for (int otherTowedUnitId : otherTowedUnitIds) { + Entity otherTowedUnit = game().getEntity(otherTowedUnitId); + if (otherTowedUnit != null) { + updateCandidates.add(otherTowedUnit); + } + } + } + } + + + /** * Have the given entity disembark if it is carried by a unit of another player. * Entities that were modified and need an update to be sent to the server diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java index 28c55331ab7..7b630bf59de 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekCellFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * @@ -92,7 +92,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) Player owner = entity.getOwner(); boolean localGM = localPlayer.isGameMaster(); boolean hideEntity = !localGM && owner.isEnemyOf(localPlayer) - && options.booleanOption(OptionsConstants.BASE_BLIND_DROP); + && options.booleanOption(OptionsConstants.BASE_BLIND_DROP); if (hideEntity) { result.append(DOT_SPACER); if (entity instanceof Infantry) { @@ -117,6 +117,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) } boolean isCarried = entity.getTransportId() != Entity.NONE; + boolean isTowed = entity.getTowedBy() != Entity.NONE; boolean hasWarning = false; boolean hasCritical = false; int mapType = lobby.mapSettings.getMedium(); @@ -130,10 +131,10 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) // Critical (Red) Warnings if ((game.getPlanetaryConditions().whyDoomed(entity, entity.getGame()) != null) - || (entity.doomedInAtmosphere() && mapType == MapSettings.MEDIUM_ATMOSPHERE) - || (entity.doomedOnGround() && mapType == MapSettings.MEDIUM_GROUND) - || (entity.doomedInSpace() && mapType == MapSettings.MEDIUM_SPACE) - || (!entity.isDesignValid())) { + || (entity.doomedInAtmosphere() && mapType == MapSettings.MEDIUM_ATMOSPHERE) + || (entity.doomedOnGround() && mapType == MapSettings.MEDIUM_GROUND) + || (entity.doomedInSpace() && mapType == MapSettings.MEDIUM_SPACE) + || (!entity.isDesignValid())) { result.append(UIUtil.fontHTML(GUIP.getWarningColor())); result.append(WARNING_SIGN + ""); hasCritical = true; @@ -261,7 +262,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) int sp = entity.getStartingPos(true); int spe = entity.getStartingPos(false); if ((!entity.isOffBoard()) - && (sp >= 0)) { + && (sp >= 0)) { firstEntry = dotSpacer(result, firstEntry); if (spe != Board.START_NONE) { result.append(UIUtil.fontHTML(uiLightGreen())); @@ -339,7 +340,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) if (entity.hasC3S()) { firstEntry = dotSpacer(result, firstEntry); result.append( - UIUtil.fontHTML(uiC3Color()) + Messages.getString("ChatLounge.C3S") + UNCONNECTED_SIGN); + UIUtil.fontHTML(uiC3Color()) + Messages.getString("ChatLounge.C3S") + UNCONNECTED_SIGN); result.append(""); } @@ -360,7 +361,7 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) if (entity.hasC3MM()) { String msg_freec3mnodes = Messages.getString("ChatLounge.FreeC3MNodes"); result.append(MessageFormat.format(" " + msg_freec3mnodes, - entity.calculateFreeC3MNodes(), entity.calculateFreeC3Nodes())); + entity.calculateFreeC3MNodes(), entity.calculateFreeC3Nodes())); } else { result.append(getString("ChatLounge.C3MNodes", entity.calculateFreeC3MNodes())); } @@ -397,7 +398,17 @@ static String formatUnitFull(Entity entity, ChatLounge lobby, boolean forceView) } result.append(""); - } else { // Hide deployment info when a unit is carried + } else if (isTowed) { // Towed + firstEntry = dotSpacer(result, firstEntry); + Entity tractor = entity.getGame().getEntity(entity.getTowedBy()); + result.append(UIUtil.fontHTML(uiGreen()) + LOADED_SIGN); + result.append(" " + Messages.getString("ChatLounge.towedBy") + " " + tractor.getChassis()); + if (PreferenceManager.getClientPreferences().getShowUnitId()) { + result.append(" [" + entity.getTransportId() + "]"); + } + result.append(""); + + } else { // Hide deployment info when a unit is carried or towed if (entity.isHidden() && mapType == MapSettings.MEDIUM_GROUND) { firstEntry = dotSpacer(result, firstEntry); result.append(getString("ChatLounge.compact.hidden")); diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java index 171469764e3..cb515920ef0 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2021-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * @@ -106,6 +106,8 @@ class LobbyMekPopup { static final String LMP_DELETE = "DELETE"; static final String LMP_UNLOADALL = "UNLOADALL"; static final String LMP_UNLOAD = "UNLOAD"; + static final String LMP_DETACH_FROM_TRACTOR = "DETACHFROMTRACTOR"; + static final String LMP_DETACH_TRAILER = "DETACHTRAILER"; static final String LMP_MOVE_DOWN = "MOVE_DOWN"; static final String LMP_INDI_CAMO = "INDI_CAMO"; static final String LMP_DAMAGE = "DAMAGE"; @@ -151,11 +153,15 @@ static ScalingPopup getPopup(List entities, List forces, ActionLi boolean accessibleFighters = accessibleEntities.stream().anyMatch(Entity::isFighter); boolean accessibleTransportBays = accessibleEntities.stream().anyMatch(e -> !e.getTransportBays().isEmpty()); boolean accessibleCarriers = accessibleEntities.stream().anyMatch(e -> !e.getLoadedUnits().isEmpty()); + boolean accessibleTractors = accessibleEntities.stream().anyMatch(e -> e.getTowing() != Entity.NONE); + boolean accessibleTrailers= accessibleEntities.stream().anyMatch(e -> e.getTowedBy() != Entity.NONE); // Find what can be done with the selected entities incl. those in selected // forces boolean anyCarrier = joinedEntities.stream().anyMatch(e -> !e.getLoadedUnits().isEmpty()); boolean noneEmbarked = joinedEntities.stream().allMatch(e -> e.getTransportId() == Entity.NONE); + boolean anyTractor = joinedEntities.stream().anyMatch(e -> e.getTowing() != Entity.NONE); + boolean anyTrailer = joinedEntities.stream().anyMatch(e -> e.getTowedBy() != Entity.NONE);; boolean allProtomeks = !joinedEntities.isEmpty() && joinedEntities.stream().allMatch(e -> e instanceof ProtoMek); boolean anyRFMGOn = joinedEntities.stream().anyMatch(LobbyMekPopup::hasRapidFireMG); @@ -227,6 +233,14 @@ static ScalingPopup getPopup(List entities, List forces, ActionLi popup.add(menuItem("Offload all carried units", LMP_UNLOADALL + NOINFO + seIds, anyCarrier, listener)); } + if (accessibleTrailers) { + popup.add(menuItem("Detach from Tractor", LMP_DETACH_FROM_TRACTOR + NOINFO + seIds, anyTrailer, listener)); + } + + if (accessibleTractors) { + popup.add(menuItem("Detach Trailer", LMP_DETACH_TRAILER + NOINFO + seIds, anyTractor, listener)); + } + if (accessibleTransportBays) { popup.add(offloadBayMenu(anyCarrier, joinedEntities, listener)); } @@ -366,7 +380,9 @@ private static JMenu towMenu(ClientGUI cg, boolean enabled, ActionListener liste Entity entity) { Game game = cg.getClient().getGame(); JMenu menu = new JMenu("Towed by"); + menu.setVisible(false); if (enabled && entity.isTrailer()) { + menu.setVisible(true); game.getEntitiesVector().stream() .filter( tractor -> tractor.canTow(entity.getId())) .filter(tractor -> !tractor.equals(entity)) @@ -376,6 +392,23 @@ private static JMenu towMenu(ClientGUI cg, boolean enabled, ActionListener liste ))); } menu.setEnabled(enabled && (menu.getItemCount() > 0)); + + return menu; + } + + private static JMenu detachFromTractorMenu(ClientGUI cg, boolean enabled, ActionListener listener, Entity trailer) { + Game game = cg.getClient().getGame(); + JMenu menu = new JMenu("Detach from Tractor"); + if (enabled && trailer.isTrailer() && trailer.getTowedBy() != Entity.NONE) { + menu.setVisible(true); + menu.setEnabled(true); + menu.addActionListener(listener); + } else { + menu.setVisible(false); + menu.setEnabled(false); + } + + return menu; } diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java index 3ce95a4db2b..b135eb89605 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopupActions.java @@ -1,5 +1,5 @@ /* - * MegaMek - Copyright (C) 2021 - The MegaMek Team + * MegaMek - Copyright (C) 2021-2025 - The MegaMek Team * * 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 @@ -105,6 +105,8 @@ public void actionPerformed(ActionEvent e) { case LMP_LOAD: case LMP_UNLOAD: case LMP_UNLOADALL: + case LMP_DETACH_TRAILER: + case LMP_DETACH_FROM_TRACTOR: case LMP_DEPLOY: case LMP_ASSIGN: case LMP_HEAT: @@ -202,6 +204,7 @@ private void forceAction(String command, Set entities, String info) { /** Calls lobby actions for multiple entities. */ private void multiEntityAction(String command, Set entities, String info) { + Set updateCandidates; switch (command) { case LMP_INDI_CAMO: lobby.lobbyActions.individualCamo(entities); @@ -246,7 +249,7 @@ private void multiEntityAction(String command, Set entities, String info break; case LMP_UNLOAD: - Set updateCandidates = new HashSet<>(); + updateCandidates = new HashSet<>(); lobby.disembarkAll(entities); break; @@ -262,6 +265,18 @@ private void multiEntityAction(String command, Set entities, String info lobby.lobbyActions.changeOwner(entities, LobbyUtility.getForces(lobby.game(), st.nextToken()), newOwnerId); break; + case LMP_DETACH_TRAILER: + updateCandidates = new HashSet<>(); + lobby.detachTrailers(entities, updateCandidates); + lobby.sendUpdate(updateCandidates); + break; + + case LMP_DETACH_FROM_TRACTOR: + updateCandidates = new HashSet<>(); + lobby.detachFromTractors(entities, updateCandidates); + lobby.sendUpdate(updateCandidates); + break; + case LMP_DEPLOY: int round = Integer.parseInt(info); lobby.lobbyActions.applyDeployment(entities, round); @@ -493,8 +508,6 @@ private void singleEntityAction(String command, Entity entity, String info) { case LMP_TOW: lobby.lobbyActions.tow(entity, info); break; - - } } } diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 9138e705011..a25cf943b55 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -15076,6 +15076,7 @@ public void towUnit(int id) { if (transporter instanceof TankTrailerHitch hitch) { hitch.load(towed); towingEnt.setTowing(id); + towed.setTowedBy(towingEnt.getId()); } } } @@ -15096,8 +15097,8 @@ public void disconnectUnit(int id) { // Now, find and empty the transporter on the actual towing entity (trailer or // tractor) Entity towingEnt = game.getEntity(towed.getTowedBy()); - towingEnt.connectedUnits.clear(); if (towingEnt != null) { + towingEnt.connectedUnits.clear(); Transporter hitch = towingEnt.getHitchCarrying(id); if (hitch != null) { hitch.unload(towed); From da17ba971cdd2864ae3f41ce8576544264be3a42 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:11:25 -0500 Subject: [PATCH 5/9] Issue 6540: RFE - Github Bot Advice --- .../client/ui/swing/TowLinkWarning.java | 28 ++----------------- .../client/ui/swing/lobby/LobbyActions.java | 4 +-- .../client/ui/swing/lobby/LobbyMekPopup.java | 16 ----------- 3 files changed, 4 insertions(+), 44 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java index a60b5f4642e..6654ae7847d 100644 --- a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java +++ b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java @@ -20,10 +20,8 @@ package megamek.client.ui.swing; import megamek.common.*; -import megamek.logging.MMLogger; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; /** @@ -31,7 +29,6 @@ * with warning. or something */ public class TowLinkWarning { - private final static MMLogger logger = MMLogger.create(TowLinkWarning.class); /** * @@ -46,17 +43,11 @@ public class TowLinkWarning { * should be placed. */ public static List findTowLinkIssues(Game game, Entity entity, Board board) { - List warnList = new ArrayList(); + List warnList = new ArrayList<>(); List validTractorCoords = findCoordsForTractor(game, entity, board); List validTrailerCoords = findCoordsForTrailer(game, entity, board); - //int tractorId = entity.getTowedBy(); - //Entity tractor = game.getEntity(tractorId); - //if (tractorId != Entity.NONE && tractor != null) { - // validTrailerCoords = findCoordsForTrailer(game, tractor, board); - //} - if (validTractorCoords == null && validTrailerCoords == null) { return warnList; } @@ -92,7 +83,7 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo return null; } - List validCoords = new ArrayList(); + List validCoords = new ArrayList<>(); if (trailer.isDeployed()) { //Can they stack? If so, add the trailer's hex as valid @@ -113,7 +104,6 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo for (int y = 0; y < boardHeight; y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, tractor)) { - int facing = tractor.getFacing(); // Can our trailer deploy in any of the adjacent hexes? if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, trailer) && !trailer.isLocationProhibited(c))) { validCoords.add(coords); @@ -140,7 +130,7 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo return null; } - List validCoords = new ArrayList(); + List validCoords = new ArrayList<>(); if (tractor.isDeployed()) { //Can they stack? If so, add the trailer's hex as valid @@ -169,16 +159,4 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo return validCoords; } - - private HashSet getAllCoords(Board board) { - var boardHeight = board.getHeight(); - var boardWidth = board.getWidth(); - var coordsSet = new HashSet(); - for (int x = 0; x < boardWidth; x++) { - for (int y = 0; y < boardHeight; y++) { - coordsSet.add(new Coords(x, y)); - } - } - return coordsSet; - } } diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java index e453d34ea67..7fd0dc5a1a1 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java @@ -687,9 +687,7 @@ public void tow(Entity trailer, String info) { LobbyErrors.showNoDualTow(frame()); } - StringBuilder errorMsg = new StringBuilder(); - - lobby.towBy(trailer, tractorId); + lobby.towBy(trailer, tractorId); } /** Asks for a new name for the provided forceId and applies it. */ diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java index cb515920ef0..66bfd830d82 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyMekPopup.java @@ -396,22 +396,6 @@ private static JMenu towMenu(ClientGUI cg, boolean enabled, ActionListener liste return menu; } - private static JMenu detachFromTractorMenu(ClientGUI cg, boolean enabled, ActionListener listener, Entity trailer) { - Game game = cg.getClient().getGame(); - JMenu menu = new JMenu("Detach from Tractor"); - if (enabled && trailer.isTrailer() && trailer.getTowedBy() != Entity.NONE) { - menu.setVisible(true); - menu.setEnabled(true); - menu.addActionListener(listener); - } else { - menu.setVisible(false); - menu.setEnabled(false); - } - - - return menu; - } - /** * Returns the "Load ProtoMek" submenu */ From 855f6fc146a7b165403098c9d65ec508a0b3c01b Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:33:49 -0500 Subject: [PATCH 6/9] Issue 6540: RFE - Towing from MM Lobby - Expanded Unit Tests & Fixed Minor Issues --- .../client/ui/swing/TowLinkWarning.java | 33 +++--- .../client/ui/swing/TowLinkWarningTest.java | 109 ++++++++++++++---- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java index 6654ae7847d..ce8ddbd55dd 100644 --- a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java +++ b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java @@ -87,16 +87,15 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo if (trailer.isDeployed()) { //Can they stack? If so, add the trailer's hex as valid - if (Compute.stackingViolation(game, tractor.getId(), tractor.getPosition(), false) == null) { + if (Compute.stackingViolation(game, tractor.getId(), trailer.getPosition(), false) == null) { validCoords.add(trailer.getPosition()); - } - validCoords.add(trailer.getPosition().translated(trailer.getFacing(), 1)); - // Let's add the typical adjacent hexes - validCoords.addAll(trailer.getPosition().allAdjacent()); - - //Except the one behind the trailer, a tractor can't be there! - validCoords.remove(trailer.getPosition().translated(trailer.getFacing(), -1)); + } else { + // Let's add the typical adjacent hexes + validCoords.addAll(trailer.getPosition().allAdjacent()); + //Except the one behind the trailer, a tractor can't be there! + validCoords.remove(trailer.getPosition().translated(trailer.getFacing(), -1)); + } } else { var boardHeight = board.getHeight(); var boardWidth = board.getWidth(); @@ -121,10 +120,10 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo * @param game * @param trailer * @param board - * @return List of coords that a tractor could go, empty if there are none. Null if the tractor isn't a tractor or pulling anything + * @return List of coords that a trailer could go, empty if there are none. Null if the trailer isn't a trailer or being pulled */ protected static List findCoordsForTrailer(Game game, Entity trailer, Board board) { - int tractorId = trailer.getTractor(); + int tractorId = trailer.getTowedBy(); Entity tractor = game.getEntity(tractorId); if (tractorId == Entity.NONE || tractor == null || tractor.getDeployRound() != trailer.getDeployRound()) { return null; @@ -133,14 +132,14 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo List validCoords = new ArrayList<>(); if (tractor.isDeployed()) { - //Can they stack? If so, add the trailer's hex as valid - if (Compute.stackingViolation(game, trailer.getId(), trailer.getPosition(), false) == null) { + //Can they stack? If so, add the tractor's hex as valid + if (Compute.stackingViolation(game, trailer.getId(), tractor.getPosition(), false) == null) { validCoords.add(tractor.getPosition()); + } else { + // Let's add all the adjacent hexes - even the spot in front of the tractor is valid, afaik it's + // possible for the tractor to turn any orientation at the end of its movement. + validCoords.addAll(tractor.getPosition().allAdjacent()); } - // Let's add the hex behind us - validCoords.add(tractor.getPosition().translated(tractor.getFacing(), -1)); - //validCoords.addAll(tractor.getPosition().allAdjacent()); - } else { var boardHeight = board.getHeight(); var boardWidth = board.getWidth(); @@ -148,7 +147,7 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo for (int y = 0; y < boardHeight; y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, trailer)) { - // Can our trailer deploy in any of the adjacent hexes? + // Can the tractor deploy in any of the adjacent hexes? if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, tractor) && !tractor.isLocationProhibited(c))) { validCoords.add(coords); } diff --git a/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java b/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java index 3685f48bdf4..95ca4a8a4c8 100644 --- a/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java +++ b/megamek/unittests/megamek/client/ui/swing/TowLinkWarningTest.java @@ -36,26 +36,28 @@ public class TowLinkWarningTest { + final int TRACTOR_ID = 1; + final int TRAILER_ID = 2; + Game mockGame; Board mockBoard; Entity mockTractor; Entity mockTrailer; - @BeforeEach void initialize() { mockTractor = mock(Entity.class); - when(mockTractor.getId()).thenReturn(1); + when(mockTractor.getId()).thenReturn(TRACTOR_ID); mockTrailer = mock(Entity.class); - when(mockTrailer.getId()).thenReturn(2); + when(mockTrailer.getId()).thenReturn(TRAILER_ID); mockGame = mock(Game.class); mockBoard = mock(Board.class); when(mockBoard.getHeight()).thenReturn(5); when(mockBoard.getWidth()).thenReturn(5); when(mockBoard.isLegalDeployment(any(Coords.class), any(Entity.class))).thenReturn(true); when(mockGame.getBoard()).thenReturn(mockBoard); - when(mockGame.getEntity(1)).thenReturn(mockTractor); - when(mockGame.getEntity(2)).thenReturn(mockTrailer); + when(mockGame.getEntity(TRACTOR_ID)).thenReturn(mockTractor); + when(mockGame.getEntity(TRAILER_ID)).thenReturn(mockTrailer); } /** @@ -67,11 +69,9 @@ class testFindCoordsForTractor { @Test void testNoTrailer() { // Arrange - TowLinkWarning towLinkWarning = new TowLinkWarning(); // Act - List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); - + List coords = TowLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); // Assert assertNull(coords); @@ -80,12 +80,11 @@ void testNoTrailer() { @Test void testTrailerNotDeployedYet() { // Arrange - TowLinkWarning towLinkWarning = new TowLinkWarning(); - - when(mockTractor.getTowing()).thenReturn(2); + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); // Act - List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + List coords = TowLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); // Assert @@ -95,25 +94,24 @@ void testTrailerNotDeployedYet() { @Test void testTrailerDeployed() { // Arrange - TowLinkWarning towLinkWarning = new TowLinkWarning(); - - when(mockTractor.getTowing()).thenReturn(2); + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); when(mockTrailer.isDeployed()).thenReturn(true); when(mockTrailer.getPosition()).thenReturn(new Coords(3, 3)); // Act - List coords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + List coords = TowLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); // Assert - assertEquals(7, coords.size()); + assertEquals(1, coords.size()); } + @Test void testLargeTrailerDeployed() { // Arrange - TowLinkWarning towLinkWarning = new TowLinkWarning(); - - when(mockTractor.getTowing()).thenReturn(2); + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); when(mockTrailer.isDeployed()).thenReturn(true); when(mockTrailer.getPosition()).thenReturn(new Coords(3, 3)); @@ -123,7 +121,76 @@ void testLargeTrailerDeployed() { compute.when(() -> Compute.stackingViolation(any(Game.class), anyInt(), any(Coords.class), anyBoolean())).thenReturn(mock(Entity.class)); // Act - testCoords = towLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + testCoords = TowLinkWarning.findCoordsForTractor(mockGame, mockTractor, mockBoard); + } + + // Assert + assertEquals(5, testCoords.size()); + } + } + + /** + * @see TowLinkWarning#findCoordsForTrailer(Game, Entity, Board) + */ + @Nested + class testFindCoordsForTrailer { + + @Test + void testNoTractor() { + // Arrange + + // Act + List coords = TowLinkWarning.findCoordsForTrailer(mockGame, mockTrailer, mockBoard); + + // Assert + assertNull(coords); + } + + @Test + void testTractorNotDeployedYet() { + // Arrange + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); + + // Act + List coords = TowLinkWarning.findCoordsForTrailer(mockGame, mockTrailer, mockBoard); + + // Assert + assertEquals(25, coords.size()); + } + + @Test + void testTractorDeployed() { + // Arrange + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); + + when(mockTractor.isDeployed()).thenReturn(true); + when(mockTractor.getPosition()).thenReturn(new Coords(3, 3)); + + // Act + List coords = TowLinkWarning.findCoordsForTrailer(mockGame, mockTrailer, mockBoard); + + // Assert + assertEquals(1, coords.size()); + } + + @Test + void testLargeTrailerAndTractorDeployed() { + // Arrange + when(mockTractor.getTowing()).thenReturn(TRAILER_ID); + when(mockTrailer.getTowedBy()).thenReturn(TRACTOR_ID); + + when(mockTractor.isDeployed()).thenReturn(true); + when(mockTractor.getPosition()).thenReturn(new Coords(3, 3)); + + List testCoords = null; + + try (MockedStatic compute = Mockito.mockStatic(Compute.class)) { + compute.when(() -> Compute.stackingViolation(any(Game.class), anyInt(), any(Coords.class), anyBoolean())).thenReturn(mock(Entity.class)); + + // Act + testCoords = TowLinkWarning.findCoordsForTrailer(mockGame, mockTrailer, mockBoard); } // Assert From 0a2ad6a98ac66b21360a83a3760b669a35acb136 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:25:12 -0500 Subject: [PATCH 7/9] Issues 6540: Princess Support for Towing from MM Lobby --- .../megamek/client/bot/princess/Princess.java | 59 ++++++++++++++ .../client/ui/swing/TowLinkWarning.java | 76 +++++++++++++------ 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index 22d9311d5b2..14db76e2411 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import megamek.client.ui.swing.TowLinkWarning; import org.apache.logging.log4j.Level; import megamek.client.bot.BotClient; @@ -725,6 +726,64 @@ protected Coords rankDeploymentCoords(Entity deployedUnit, List possible sb.append("Ranking deployment hexes..."); } + // If we're a trailer, get out of here and let the tractor find the better position. + if (deployedUnit.getTowedBy() != Entity.NONE) { + Entity tractor = game.getEntity(deployedUnit.getTowedBy()); + if (tractor != null) { + if (tractor.isDeployed()) { + List trailerDeployCoords = TowLinkWarning.getCoordsForTrailerGivenTractor(game, deployedUnit, tractor); + // Would any of our possible deploy coords let us be towed by our tractor? Filter to that. + // Else use the default list, guess we can't tow right now. + if (possibleDeployCoords.stream().anyMatch(trailerDeployCoords::contains)) { + possibleDeployCoords = possibleDeployCoords.stream().filter(trailerDeployCoords::contains).toList(); + } + } else { + // If the tractor isn't deployed let's find the best place to deploy it instead and base our location on that + final List startingCoords = getStartingCoordsArray(tractor); + if (startingCoords.isEmpty()) { + logger.error("No valid locations to deploy " + tractor.getDisplayName()); + } + + Coords bestTractorCoords = getFirstValidCoords(tractor, startingCoords); + List filteredDeployCoords = new ArrayList<>(); + if (deployedUnit instanceof LargeSupportTank) { + filteredDeployCoords = possibleDeployCoords.stream().filter(c -> bestTractorCoords.allAdjacent().contains(c)).toList(); + if (!filteredDeployCoords.isEmpty()) { + possibleDeployCoords = filteredDeployCoords; + } + } else if (possibleDeployCoords.contains(bestTractorCoords)){ + filteredDeployCoords.add(bestTractorCoords); + possibleDeployCoords = filteredDeployCoords; + } + } + } + } + + // If we're a tractor, let's make sure we can deploy with our trailer + if (deployedUnit.getTowing() != Entity.NONE) { + Entity trailer = game.getEntity(deployedUnit.getTowing()); + if (trailer != null) { + if (trailer.isDeployed()) { + List tractorDeployCoords = TowLinkWarning.getCoordsForTractorGivenTrailer(game, deployedUnit, trailer); + // Would any of our possible deploy coords let us be tow our trailer? Filter to that. + // Else use the default list, guess we can't tow right now. + if (possibleDeployCoords.stream().anyMatch(tractorDeployCoords::contains)) { + possibleDeployCoords = possibleDeployCoords.stream().filter(tractorDeployCoords::contains).toList(); + } + } else { + List towLinkIssues = TowLinkWarning.findTowLinkIssues(game, deployedUnit, game.getBoard()); + // If there are any coords we can deploy to without tow link issues, filter our list to just that + if (possibleDeployCoords.stream().anyMatch(c -> !towLinkIssues.contains(c))) { + List filteredDeployCoords = possibleDeployCoords.stream().filter(c -> !towLinkIssues.contains(c)).toList(); + if (!filteredDeployCoords.isEmpty()) { + possibleDeployCoords = filteredDeployCoords; + } + } + } + } + } + + // Sample LIMIT number of valid starting hexes, check accessibility and hazards within RADIUS int LIMIT = 20; int RADIUS = 3; diff --git a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java index ce8ddbd55dd..1b147c12546 100644 --- a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java +++ b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java @@ -86,16 +86,7 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo List validCoords = new ArrayList<>(); if (trailer.isDeployed()) { - //Can they stack? If so, add the trailer's hex as valid - if (Compute.stackingViolation(game, tractor.getId(), trailer.getPosition(), false) == null) { - validCoords.add(trailer.getPosition()); - } else { - // Let's add the typical adjacent hexes - validCoords.addAll(trailer.getPosition().allAdjacent()); - - //Except the one behind the trailer, a tractor can't be there! - validCoords.remove(trailer.getPosition().translated(trailer.getFacing(), -1)); - } + validCoords.addAll(getCoordsForTractorGivenTrailer(game, tractor, trailer)); } else { var boardHeight = board.getHeight(); var boardWidth = board.getWidth(); @@ -103,9 +94,16 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo for (int y = 0; y < boardHeight; y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, tractor)) { - // Can our trailer deploy in any of the adjacent hexes? - if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, trailer) && !trailer.isLocationProhibited(c))) { - validCoords.add(coords); + if (trailer instanceof LargeSupportTank) { + // Can our trailer deploy in any of the adjacent hexes? + if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, trailer) && !trailer.isLocationProhibited(c))) { + validCoords.add(coords); + } + } + else { + if (board.isLegalDeployment(coords, trailer) && !trailer.isLocationProhibited(coords)) { + validCoords.add(coords); + } } } } @@ -115,6 +113,23 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo return validCoords; } + public static List getCoordsForTractorGivenTrailer(Game game, Entity tractor, Entity trailer) { + List validCoords = new ArrayList<>(); + + //Can they stack? If so, add the trailer's hex as valid + if (Compute.stackingViolation(game, tractor.getId(), trailer.getPosition(), false) == null) { + validCoords.add(trailer.getPosition()); + } else { + // Let's add the typical adjacent hexes + validCoords.addAll(trailer.getPosition().allAdjacent()); + + //Except the one behind the trailer, a tractor can't be there! + validCoords.remove(trailer.getPosition().translated(trailer.getFacing(), -1)); + } + + return validCoords; + } + /** * When deploying a tractor, return the coords that would let it attach to its assigned trailer. * @param game @@ -132,14 +147,7 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo List validCoords = new ArrayList<>(); if (tractor.isDeployed()) { - //Can they stack? If so, add the tractor's hex as valid - if (Compute.stackingViolation(game, trailer.getId(), tractor.getPosition(), false) == null) { - validCoords.add(tractor.getPosition()); - } else { - // Let's add all the adjacent hexes - even the spot in front of the tractor is valid, afaik it's - // possible for the tractor to turn any orientation at the end of its movement. - validCoords.addAll(tractor.getPosition().allAdjacent()); - } + validCoords.addAll(getCoordsForTrailerGivenTractor(game, trailer, tractor)); } else { var boardHeight = board.getHeight(); var boardWidth = board.getWidth(); @@ -147,9 +155,16 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo for (int y = 0; y < boardHeight; y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, trailer)) { - // Can the tractor deploy in any of the adjacent hexes? - if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, tractor) && !tractor.isLocationProhibited(c))) { - validCoords.add(coords); + if (trailer instanceof LargeSupportTank) { + // Can the tractor deploy in any of the adjacent hexes? + if (coords.allAdjacent().stream().anyMatch(c -> board.isLegalDeployment(c, tractor) && !tractor.isLocationProhibited(c))) { + validCoords.add(coords); + } + } + else { + if (board.isLegalDeployment(coords, tractor) && !tractor.isLocationProhibited(coords)) { + validCoords.add(coords); + } } } } @@ -158,4 +173,17 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo return validCoords; } + + public static List getCoordsForTrailerGivenTractor(Game game, Entity trailer, Entity tractor) { + List validCoords = new ArrayList(); + //Can they stack? If so, add the tractor's hex as valid + if (Compute.stackingViolation(game, trailer.getId(), tractor.getPosition(), false) == null) { + validCoords.add(tractor.getPosition()); + } else { + // Let's add all the adjacent hexes - even the spot in front of the tractor is valid, afaik it's + // possible for the tractor to turn any orientation at the end of its movement. + validCoords.addAll(tractor.getPosition().allAdjacent()); + } + return validCoords; + } } From 30bd00add932a8babb08086c8eda82a738c66816 Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:33:49 -0500 Subject: [PATCH 8/9] Issues 6540: Fixed bug with showing invalid tractors in MM lobby --- .../megamek/client/ui/swing/lobby/LobbyActions.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java index 7fd0dc5a1a1..add85e9fb41 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java @@ -671,19 +671,20 @@ public void tow(Entity trailer, String info) { int tractorId = Integer.parseInt(sTow.nextToken()); Entity tractor = game().getEntity(tractorId); // Remove those entities from the candidates that are already carried by that - // loader - if (trailer == null || tractor == null || trailer.getTractor() == tractorId) { + // tractor/trailer + if (trailer == null || tractor == null || trailer.getTractor() == tractorId + || tractor.getTowing() != Entity.NONE || tractor.getTractor() == trailer.getId()) { return; } - // If a unit of the selected units is currently loaded onto another, 2nd unit of + // If a unit of the selected units is currently towed onto another, 2nd unit of // the selected - // units, do not continue. The player should unload units first. This would + // units, do not continue. The player should detach units first. This would // require // a server update offloading that second unit AND embarking it. Currently not // possible // as a single server update and updates for one unit shouldn't be chained. - if (tractor.getTowing() != Entity.NONE || trailer.getTractor() != Entity.NONE) { + if (tractor.getTowing() != Entity.NONE && trailer.getTractor() != Entity.NONE) { LobbyErrors.showNoDualTow(frame()); } From e874ae296db8752bd5f01595453c5e8e83bc3b7b Mon Sep 17 00:00:00 2001 From: psikomonkie <189469115+psikomonkie@users.noreply.github.com> Date: Sun, 16 Feb 2025 13:35:15 -0500 Subject: [PATCH 9/9] Issues 6540: Minor fixes from SJuliez's review. --- .../client/ui/swing/TowLinkWarning.java | 53 ++++++++++++------- .../client/ui/swing/lobby/LobbyActions.java | 18 +++---- .../server/totalwarfare/TWGameManager.java | 3 +- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java index 1b147c12546..e3a5e7ca3ee 100644 --- a/megamek/src/megamek/client/ui/swing/TowLinkWarning.java +++ b/megamek/src/megamek/client/ui/swing/TowLinkWarning.java @@ -31,13 +31,12 @@ public class TowLinkWarning { /** + * This is used by the {@link MovementDisplay} class. * - * This is - * used by the {@link MovementDisplay} class. * * @param game {@link Game} provided by the phase display class * @param entity {@link Entity} currently selected in the deployment phase. - * @param board {@link Board} board object with building data. + * @param board {@link Board} board object with hex data. * * @return returns a list of {@link Coords} that where warning flags * should be placed. @@ -48,18 +47,40 @@ public static List findTowLinkIssues(Game game, Entity entity, Board boa List validTractorCoords = findCoordsForTractor(game, entity, board); List validTrailerCoords = findCoordsForTrailer(game, entity, board); + + // Null list means the unit is not a tractor/trailer - if both + // are null then there are no warnings to display, return the + // empty warn list. if (validTractorCoords == null && validTrailerCoords == null) { return warnList; } - var boardHeight = board.getHeight(); - var boardWidth = board.getWidth(); - for (int x = 0; x < boardWidth; x++) { - for (int y = 0; y < boardHeight; y++) { + + List validTowCoords = new ArrayList<>(); + + if (validTractorCoords == null) { + // We've established both aren't null - + // So if validTractorCoords is null, we can add + // validTrailerCoords as the only valid coords + validTowCoords.addAll(validTrailerCoords); + } else if (validTrailerCoords == null) { + // On the other hand, if validTrailerCoords is null, + // we can add validTractorCoords as the only valid coords + validTowCoords.addAll(validTractorCoords); + } else { + // If neither is null then we need to get the coords in common + // So add one, then retainAll with the other. + validTowCoords.addAll(validTractorCoords); + validTowCoords.retainAll(validTrailerCoords); + } + + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { Coords coords = new Coords(x, y); + // We still need to check if it's a legal deployment + // again so we don't mark hexes that aren't even + // valid to deploy in as warning. if (board.isLegalDeployment(coords, entity)) { - if (validTractorCoords != null && !validTractorCoords.contains(coords)) { - warnList.add(coords); - } else if (validTrailerCoords != null && !validTrailerCoords.contains(coords)) { + if (!validTowCoords.contains(coords)) { warnList.add(coords); } } @@ -88,10 +109,8 @@ protected static List findCoordsForTractor(Game game, Entity tractor, Bo if (trailer.isDeployed()) { validCoords.addAll(getCoordsForTractorGivenTrailer(game, tractor, trailer)); } else { - var boardHeight = board.getHeight(); - var boardWidth = board.getWidth(); - for (int x = 0; x < boardWidth; x++) { - for (int y = 0; y < boardHeight; y++) { + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, tractor)) { if (trailer instanceof LargeSupportTank) { @@ -149,10 +168,8 @@ protected static List findCoordsForTrailer(Game game, Entity trailer, Bo if (tractor.isDeployed()) { validCoords.addAll(getCoordsForTrailerGivenTractor(game, trailer, tractor)); } else { - var boardHeight = board.getHeight(); - var boardWidth = board.getWidth(); - for (int x = 0; x < boardWidth; x++) { - for (int y = 0; y < boardHeight; y++) { + for (int x = 0; x < board.getWidth(); x++) { + for (int y = 0; y < board.getHeight(); y++) { Coords coords = new Coords(x, y); if (board.isLegalDeployment(coords, trailer)) { if (trailer instanceof LargeSupportTank) { diff --git a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java index add85e9fb41..71a3626eb2c 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java +++ b/megamek/src/megamek/client/ui/swing/lobby/LobbyActions.java @@ -670,20 +670,20 @@ public void tow(Entity trailer, String info) { StringTokenizer sTow = new StringTokenizer(info, ":"); int tractorId = Integer.parseInt(sTow.nextToken()); Entity tractor = game().getEntity(tractorId); - // Remove those entities from the candidates that are already carried by that - // tractor/trailer + // Remove those entities from the candidates + // that are already carried by that tractor/trailer if (trailer == null || tractor == null || trailer.getTractor() == tractorId || tractor.getTowing() != Entity.NONE || tractor.getTractor() == trailer.getId()) { return; } - // If a unit of the selected units is currently towed onto another, 2nd unit of - // the selected - // units, do not continue. The player should detach units first. This would - // require - // a server update offloading that second unit AND embarking it. Currently not - // possible - // as a single server update and updates for one unit shouldn't be chained. + // If a unit of the selected units is currently + // towed by another, 2nd unit of the selected + // units, do not continue. The player should + // detach units first. This would require a + // server update detaching that second unit AND + // re-attaching it. Currently not possible as a single + // server update and updates for one unit shouldn't be chained. if (tractor.getTowing() != Entity.NONE && trailer.getTractor() != Entity.NONE) { LobbyErrors.showNoDualTow(frame()); } diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 45c621deff0..a89f006dde1 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -26911,7 +26911,8 @@ private void receiveEntityLoad(Packet c, int connIndex) { } /** - * loads an entity into another one. Meant to be called from the chat lounge + * Set an entity to be towed by another entity. + * Meant to be called from the chat lounge. * * @param c the packet to be processed * @param connIndex the id for connection that received the packet.