diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 4c96ba383f0..c6b459d8112 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -1972,6 +1972,10 @@ KeyBinds.cmdNames.undoSingleStep=Undo Single Step KeyBinds.cmdDesc.undoSingleStep=Undo single step in movement phase KeyBinds.cmdNames.extendTurnTimer=Activates Turn Timer Extension KeyBinds.cmdDesc.extendTurnTimer=When Turn Timer gets to 2 seconds left, reset timer. +KeyBinds.cmdNames.pause=Pause the Game +KeyBinds.cmdDesc.pause=Pauses the game. Works only when only Princess players with active units remain. +KeyBinds.cmdNames.unpause=Unpause the Game +KeyBinds.cmdDesc.unpause=Unpauses the game #Key Bindings Overlay KeyBindingsDisplay.fixedBinds=Toggle Unit Display and Minimap: Mouse Button 4\nZoom: Mouse Wheel\nTurn / Torso twist: Shift + Left-Click\nLine of Sight tool: Ctrl + Left-Click (two hexes)\nRuler tool: Alt + Left-Click (two hexes) diff --git a/megamek/mmconf/defaultKeyBinds.xml b/megamek/mmconf/defaultKeyBinds.xml index 2df36f7a237..7b0ecbd7599 100644 --- a/megamek/mmconf/defaultKeyBinds.xml +++ b/megamek/mmconf/defaultKeyBinds.xml @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="keyBindingSchema.xsd"> scrollN 87 @@ -78,6 +78,20 @@ false + + pause + 80 + 192 + false + + + + unpause + 80 + 640 + false + + nextWeapon 40 diff --git a/megamek/src/megamek/client/AbstractClient.java b/megamek/src/megamek/client/AbstractClient.java index 6a30c1c5ace..4f944b39c4c 100644 --- a/megamek/src/megamek/client/AbstractClient.java +++ b/megamek/src/megamek/client/AbstractClient.java @@ -235,6 +235,16 @@ public synchronized void sendDone(boolean done) { flushConn(); } + public void sendPause() { + send(new Packet(PacketCommand.PAUSE)); + flushConn(); + } + + public void sendUnpause() { + send(new Packet(PacketCommand.UNPAUSE)); + flushConn(); + } + /** * Receives player information from the message packet. * diff --git a/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java b/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java index 1181c81be5a..a0590743f68 100644 --- a/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java +++ b/megamek/src/megamek/client/ui/swing/StatusBarPhaseDisplay.java @@ -45,6 +45,7 @@ import javax.swing.ToolTipManager; import javax.swing.border.EmptyBorder; +import megamek.client.AbstractClient; import megamek.client.ui.GBC; import megamek.client.ui.Messages; import megamek.client.ui.swing.util.KeyBindReceiver; @@ -157,8 +158,25 @@ public void actionPerformed(ActionEvent e) { KeyBindParser.addPreferenceChangeListener(this); MegaMekGUI.getKeyDispatcher().registerCommandAction(KeyCommandBind.EXTEND_TURN_TIMER, this, this::extendTimer); + MegaMekGUI.getKeyDispatcher().registerCommandAction(KeyCommandBind.PAUSE.cmd, this::pauseGameWhenOnlyBotUnitsRemain); + MegaMekGUI.getKeyDispatcher().registerCommandAction(KeyCommandBind.UNPAUSE.cmd, + () -> ((AbstractClient) clientgui.getClient()).sendUnpause()); } + private void pauseGameWhenOnlyBotUnitsRemain() { + if (isIgnoringEvents() || !isVisible()) { + return; + } + IGame game = getClientgui().getClient().getGame(); + List nonBots = game.getPlayersList().stream().filter(p -> !p.isBot()).toList(); + boolean liveUnitsRemaining = nonBots.stream().anyMatch(p -> game.getEntitiesOwnedBy(p) > 0); + if (liveUnitsRemaining) { + clientgui.getClient().sendChat("Pausing the game only works when only bot units remain."); + } else { + clientgui.getClient().sendChat("Requesting game pause."); + ((AbstractClient) clientgui.getClient()).sendPause(); + } + } /** Returns the list of buttons that should be displayed. */ protected abstract List getButtonList(); diff --git a/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java b/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java index baf35a27737..2b0f54f4ac1 100644 --- a/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java +++ b/megamek/src/megamek/client/ui/swing/util/KeyCommandBind.java @@ -90,6 +90,8 @@ public enum KeyCommandBind { TOGGLE_CONVERSIONMODE("toggleConversion", VK_M), PREV_MODE("prevMode", VK_KP_DOWN), NEXT_MODE("nextMode", VK_KP_UP), + PAUSE("pause", VK_P, CTRL_DOWN_MASK | SHIFT_DOWN_MASK), + UNPAUSE("unpause", VK_P, CTRL_DOWN_MASK | ALT_DOWN_MASK), // --------- The following binds are used by the CommonMenuBar: // Toggles isometric view on/off @@ -218,4 +220,4 @@ public static String getDesc(KeyCommandBind k) { String key = getKeyText(k.key); return (mod.isEmpty() ? "" : mod + "+") + key; } -} \ No newline at end of file +} diff --git a/megamek/src/megamek/common/net/enums/PacketCommand.java b/megamek/src/megamek/common/net/enums/PacketCommand.java index 1b249ec1d58..3e37e6f71ad 100644 --- a/megamek/src/megamek/common/net/enums/PacketCommand.java +++ b/megamek/src/megamek/common/net/enums/PacketCommand.java @@ -44,6 +44,10 @@ public enum PacketCommand { /** A packet setting a Client's ready status (S -> C) or updating the Server on the Client's status (C -> S). */ PLAYER_READY, + /** A packet telling the server to pause / unpause packet handling (to interrupt a Princess-only game) */ + PAUSE, + UNPAUSE, + CHAT, ENTITY_ADD, ENTITY_REMOVE, diff --git a/megamek/src/megamek/server/Server.java b/megamek/src/megamek/server/Server.java index d62d8555e92..cc51549c8c2 100644 --- a/megamek/src/megamek/server/Server.java +++ b/megamek/src/megamek/server/Server.java @@ -209,6 +209,10 @@ public void run() { private static final String WARGAMES_RESPONSE = "Let's play global thermonuclear war."; private final ConnectionListener connectionListener = new ConnectionListener() { + + private boolean isPaused = false; + private final List pausedWaitingList = new ArrayList<>(); + /** * Called when it is sensed that a connection has terminated. */ @@ -252,11 +256,34 @@ public void packetReceived(PacketReceivedEvent e) { // Some packets should be handled immediately handle(rp.getConnectionId(), rp.getPacket()); break; - default: + case PAUSE: + if (!isPaused) { + logger.info("Pause packet received - pausing packet handling"); + sendServerChat("Game is paused."); + } + isPaused = true; + break; + case UNPAUSE: + if (isPaused) { + logger.info("Unpause packet received - resuming packet handling"); + sendServerChat("Game is resumed."); + } + isPaused = false; synchronized (packetQueue) { - packetQueue.add(rp); + packetQueue.addAll(pausedWaitingList); packetQueue.notifyAll(); } + pausedWaitingList.clear(); + break; + default: + if (isPaused) { + pausedWaitingList.add(rp); + } else { + synchronized (packetQueue) { + packetQueue.add(rp); + packetQueue.notifyAll(); + } + } break; } }