From 40186e2d6748611c3d669b5ea2ce69a1db74d0e8 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Wed, 4 Dec 2024 00:55:08 -0300 Subject: [PATCH] feat: new GM commands and client commands --- .../i18n/megamek/client/messages.properties | 19 ++ .../src/megamek/client/ui/swing/MapMenu.java | 27 ++- .../ClientCommandPanel.java} | 62 +++++- megamek/src/megamek/common/Game.java | 44 +++- megamek/src/megamek/server/IGameManager.java | 2 + megamek/src/megamek/server/Server.java | 15 ++ .../commands/ChangeOwnershipCommand.java | 3 +- .../server/commands/ChangeTeamCommand.java | 74 ++++++ .../server/commands/ChangeWeatherCommand.java | 7 +- .../server/commands/ClientServerCommand.java | 210 ++++++++++++++++++ .../server/commands/DisasterCommand.java | 3 +- .../server/commands/EndGameCommand.java | 66 ++++++ .../server/commands/FirefightCommand.java | 3 +- .../server/commands/FirestarterCommand.java | 3 +- .../server/commands/FirestormCommand.java | 5 +- .../commands/GamemasterServerCommand.java | 156 +------------ .../server/commands/JoinTeamCommand.java | 12 +- .../megamek/server/commands/KickCommand.java | 100 ++++++--- .../megamek/server/commands/KillCommand.java | 3 +- .../server/commands/NoFiresCommand.java | 8 +- .../megamek/server/commands/NukeCommand.java | 74 +++--- .../commands/OrbitalBombardmentCommand.java | 3 +- .../server/commands/RemoveSmokeCommand.java | 8 +- .../server/commands/RescueCommand.java | 3 +- .../server/commands/VictoryCommand.java | 4 +- .../server/commands/arguments/Argument.java | 1 + .../server/commands/arguments/Arguments.java | 27 +++ .../commands/arguments/BooleanArgument.java | 56 +++++ .../arguments/OptionalPasswordArgument.java | 54 +++++ .../arguments/OptionalStringArgument.java | 54 +++++ .../commands/arguments/StringArgument.java | 56 +++++ .../megamek/server/sbf/SBFGameManager.java | 5 + .../server/totalwarfare/TWGameManager.java | 103 +++++++-- .../totalwarfare/TWPhaseEndManager.java | 4 +- 34 files changed, 994 insertions(+), 280 deletions(-) rename megamek/src/megamek/client/ui/swing/{gmCommands/GamemasterCommandPanel.java => commands/ClientCommandPanel.java} (74%) create mode 100644 megamek/src/megamek/server/commands/ChangeTeamCommand.java create mode 100644 megamek/src/megamek/server/commands/ClientServerCommand.java create mode 100644 megamek/src/megamek/server/commands/EndGameCommand.java create mode 100644 megamek/src/megamek/server/commands/arguments/Arguments.java create mode 100644 megamek/src/megamek/server/commands/arguments/BooleanArgument.java create mode 100644 megamek/src/megamek/server/commands/arguments/OptionalPasswordArgument.java create mode 100644 megamek/src/megamek/server/commands/arguments/OptionalStringArgument.java create mode 100644 megamek/src/megamek/server/commands/arguments/StringArgument.java diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 948ae58efe7..729906b54eb 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4755,10 +4755,29 @@ Gamemaster.cmd.orbitalbombardment.success=Orbital bombardment incoming! Gamemaster.cmd.firefight.longName=Firefight Gamemaster.cmd.firefight.reason=Fire extinguished Gamemaster.cmd.firefight.help=Extinguishes a fire on the board. + # No Fire Gamemaster.cmd.nofire.longName=No Fires Gamemaster.cmd.nofire.help=Extinguishes all fires on the board. +# Change player team +Gamemaster.cmd.changeteam.help=Changes the team of a player. +Gamemaster.cmd.changeteam.longName=Change Player Team +Gamemaster.cmd.changeteam.playerNotFound=No such player. +Gamemaster.cmd.changeteam.success=Player {0} has been moved to team {1}. +Gamemaster.cmd.changeteam.playerID=ID of the player to change team. +Gamemaster.cmd.changeteam.teamID=ID of the team to move the player to. +Gamemaster.cmd.changeteam.playerCantJoinUnassigned=Player must have no more units to join the unassigned team! + +# End game +Gamemaster.cmd.endgame.success=This is the end of the game +Gamemaster.cmd.endgame.force=Force the game to finish before the end of the round +Gamemaster.cmd.endgame.help=Ends the game, declaring one player the winner. If the player is part of a team then their team wins. +Gamemaster.cmd.endgame.playerID=ID of the player to win the game, or whose team is to be declared winner. +Gamemaster.cmd.endgame.longName=End Game +Gamemaster.cmd.endgame.playerNotFound=No such player. + + # Orbital Bombardment text OrbitalBombardment.source=Unknown warship in orbit OrbitalBombardment.hitOnRound=Orbital bombardment incoming, hit on round {0} diff --git a/megamek/src/megamek/client/ui/swing/MapMenu.java b/megamek/src/megamek/client/ui/swing/MapMenu.java index 84a8baf6554..eb974ae5acc 100644 --- a/megamek/src/megamek/client/ui/swing/MapMenu.java +++ b/megamek/src/megamek/client/ui/swing/MapMenu.java @@ -32,7 +32,7 @@ import megamek.client.bot.princess.CardinalEdge; import megamek.client.event.BoardViewEvent; import megamek.client.ui.Messages; -import megamek.client.ui.swing.gmCommands.GamemasterCommandPanel; +import megamek.client.ui.swing.commands.ClientCommandPanel; import megamek.client.ui.swing.lobby.LobbyUtility; import megamek.common.*; import megamek.common.Building.DemolitionCharge; @@ -630,7 +630,7 @@ JMenuItem createTargetHexMenuItem(Player bot) { /** * Create various menus related to GameMaster (GM) mode * - * @return + * @return JMenu */ private JMenu createGamemasterMenu() { JMenu menu = new JMenu(Messages.getString("Gamemaster.Gamemaster")); @@ -679,21 +679,32 @@ private JMenu createGamemasterMenu() { */ private JMenu createGMSpecialCommandsMenu() { JMenu menu = new JMenu(Messages.getString("Gamemaster.SpecialCommands")); + + var nukesAllowed = client.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_REALLY_ALLOW_NUKES) + && client.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_ALLOW_NUKES); + List.of( - new ChangeOwnershipCommand(null, null), new ChangeWeatherCommand(null, null), - new DisasterCommand(null, null), + new ChangeTeamCommand(null, null), + new ChangeOwnershipCommand(null, null), new KillCommand(null, null), - new FirefightCommand(null, null), + new RescueCommand(null, null), new FirestarterCommand(null, null), new FirestormCommand(null, null), + new FirefightCommand(null, null), new NoFiresCommand(null, null), - new OrbitalBombardmentCommand(null, null), new RemoveSmokeCommand(null, null), - new RescueCommand(null, null) + new OrbitalBombardmentCommand(null, null), + new DisasterCommand(null, null), + new NukeCommand(null, null), + new EndGameCommand(null, null) ).forEach(cmd -> { JMenuItem item = new JMenuItem(cmd.getLongName()); - item.addActionListener(evt -> new GamemasterCommandPanel(gui.getFrame(), gui, cmd, coords).setVisible(true)); + if (cmd instanceof NukeCommand) { + item.setEnabled(nukesAllowed); + } + item.addActionListener(evt -> new ClientCommandPanel(gui.getFrame(), gui, cmd, coords).setVisible(true)); + menu.add(item); }); diff --git a/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java b/megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java similarity index 74% rename from megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java rename to megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java index d2e4d7fa70a..b50abd4e844 100644 --- a/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java +++ b/megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java @@ -1,13 +1,10 @@ -package megamek.client.ui.swing.gmCommands; +package megamek.client.ui.swing.commands; import megamek.client.ui.swing.ClientGUI; import megamek.common.Coords; import megamek.common.annotations.Nullable; -import megamek.server.commands.GamemasterServerCommand; -import megamek.server.commands.arguments.Argument; -import megamek.server.commands.arguments.EnumArgument; -import megamek.server.commands.arguments.IntegerArgument; -import megamek.server.commands.arguments.OptionalEnumArgument; +import megamek.server.commands.ClientServerCommand; +import megamek.server.commands.arguments.*; import javax.swing.*; import java.awt.*; @@ -17,21 +14,21 @@ import java.util.Objects; /** - * Dialog for executing a gamemaster command. + * Dialog for executing a client command. */ -public class GamemasterCommandPanel extends JDialog { - private final GamemasterServerCommand command; +public class ClientCommandPanel extends JDialog { + private final ClientServerCommand command; private final ClientGUI client; private final Coords coords; /** - * Constructor for the dialog for executing a gamemaster command. + * Constructor for the dialog for executing a client command. * * @param parent The parent frame. * @param client The client GUI. * @param command The command to render. */ - public GamemasterCommandPanel(JFrame parent, ClientGUI client, GamemasterServerCommand command, @Nullable Coords coords) { + public ClientCommandPanel(JFrame parent, ClientGUI client, ClientServerCommand command, @Nullable Coords coords) { super(parent, command.getName(), true); this.command = command; this.client = client; @@ -88,6 +85,10 @@ private JComponent getArgumentComponent(Argument argument, JPanel argumentPan JSpinner spinner = createSpinner(intArg); argumentPanel.add(spinner); return spinner; + } else if (argument instanceof OptionalIntegerArgument intArg) { + JSpinner spinner = createSpinner(intArg); + argumentPanel.add(spinner); + return spinner; } else if (argument instanceof OptionalEnumArgument enumArg) { JComboBox comboBox = createOptionalEnumComboBox(enumArg); argumentPanel.add(comboBox); @@ -96,6 +97,31 @@ private JComponent getArgumentComponent(Argument argument, JPanel argumentPan JComboBox comboBox = createEnumComboBox(enumArg); argumentPanel.add(comboBox); return comboBox; + } else if (argument instanceof OptionalPasswordArgument) { + JPasswordField passwordField = new JPasswordField(); + argumentPanel.add(passwordField); + return passwordField; + } else if (argument instanceof StringArgument stringArg) { + JTextField textField = new JTextField(); + if (stringArg.hasDefaultValue()) { + textField.setText(stringArg.getValue()); + } + argumentPanel.add(textField); + return textField; + } else if (argument instanceof OptionalStringArgument stringArg) { + JTextField textField = new JTextField(); + if (stringArg.getValue().isPresent()) { + textField.setText(stringArg.getValue().get()); + } + argumentPanel.add(textField); + return textField; + } else if (argument instanceof BooleanArgument boolArg) { + JCheckBox checkBox = new JCheckBox(); + if (boolArg.hasDefaultValue()) { + checkBox.setSelected(boolArg.getValue()); + } + argumentPanel.add(checkBox); + return checkBox; } return null; } @@ -113,6 +139,14 @@ private int getIntArgumentDefaultValue(IntegerArgument intArg) { isArgumentY(intArg) ? coords.getY()+1 : 0; } + private JSpinner createSpinner(OptionalIntegerArgument intArg) { + return new JSpinner(new SpinnerNumberModel( + (int) intArg.getValue().orElse(0), + intArg.getMinValue(), + intArg.getMaxValue(), + 1)); + } + private JSpinner createSpinner(IntegerArgument intArg) { return new JSpinner(new SpinnerNumberModel( getIntArgumentDefaultValue(intArg), @@ -184,6 +218,12 @@ private void executeCommand(Map argumentComponents) { if (component instanceof JSpinner) { args[i] = argument.getName() + "=" + ((JSpinner) component).getValue().toString(); + } else if (component instanceof JPasswordField) { + args[i] = argument.getName() + "=" + new String(((JPasswordField) component).getPassword()); + } else if (component instanceof JTextField) { + args[i] = argument.getName() + "=" + ((JTextField) component).getText(); + } else if (component instanceof JCheckBox) { + args[i] = argument.getName() + "=" + (((JCheckBox) component).isSelected() ? "true" : "false"); } else if (component instanceof JComboBox) { if (argument instanceof OptionalEnumArgument) { String selectedItem = (String) ((JComboBox) component).getSelectedItem(); diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java index 80b553b9c9f..1e072fb41a8 100644 --- a/megamek/src/megamek/common/Game.java +++ b/megamek/src/megamek/common/Game.java @@ -107,6 +107,8 @@ public final class Game extends AbstractGame implements Serializable, PlanetaryC private final GameReports gameReports = new GameReports(); private boolean forceVictory = false; + private boolean endImmediately = false; + private boolean ignorePlayerDefeatVotes = false; private int victoryPlayerId = Player.PLAYER_NONE; private int victoryTeam = Player.TEAM_NONE; @@ -2528,13 +2530,6 @@ public void setRoundCount(int roundCount) { setCurrentRound(roundCount); } - /** - * Increments the round counter - */ - public void incrementRoundCount() { - incrementCurrentRound(); - } - /** * Getter for property forceVictory. This tells us that there is an active claim * for victory. @@ -2546,6 +2541,24 @@ public boolean isForceVictory() { return forceVictory; } + /** + * Getter for property ignorePlayerDefeatVotes. + * @return Value of property ignorePlayerDefeatVotes. + */ + public boolean isIgnorePlayerDefeatVotes() { + return ignorePlayerDefeatVotes; + } + + /** + * Getter for property endImmediately. This tells us that the game should end even if it is not the end of the round in a + * forced victory + * + * @return Value of property endImmediately. + */ + public boolean isEndImmediately() { + return endImmediately; + } + /** * Setter for property forceVictory. * @@ -2555,6 +2568,23 @@ public void setForceVictory(boolean forceVictory) { this.forceVictory = forceVictory; } + /** + * Setter for property endImmediately. + * + * @param endImmediately New value of property endImmediately. + */ + public void setEndImmediately(boolean endImmediately) { + this.endImmediately = endImmediately; + } + + /** + * Setter for property ignorePlayerDefeatVotes. + * @param ignorePlayerDefeatVotes New value of property ignorePlayerDefeatVotes. + */ + public void setIgnorePlayerDefeatVotes(boolean ignorePlayerDefeatVotes) { + this.ignorePlayerDefeatVotes = ignorePlayerDefeatVotes; + } + /** * Adds the given reports vector to the GameReport collection. * diff --git a/megamek/src/megamek/server/IGameManager.java b/megamek/src/megamek/server/IGameManager.java index b55801f5bd9..70ae0d3b020 100644 --- a/megamek/src/megamek/server/IGameManager.java +++ b/megamek/src/megamek/server/IGameManager.java @@ -134,6 +134,8 @@ default void saveGame(String fileName) { void requestTeamChange(int teamId, Player player); + void requestTeamChangeForPlayer(int teamID, Player player); + List getCommandList(Server server); void addReport(ReportEntry r); diff --git a/megamek/src/megamek/server/Server.java b/megamek/src/megamek/server/Server.java index cc51549c8c2..aee0f844b86 100644 --- a/megamek/src/megamek/server/Server.java +++ b/megamek/src/megamek/server/Server.java @@ -1183,10 +1183,25 @@ private void transmitAllPlayerUpdates() { } } + /** + * Player can request its own change of team + * @param teamId target team id + * @param player player requesting the change + * @deprecated Planned to be removed. Use {@link #requestTeamChangeForPlayer(int, Player)} instead. + */ public void requestTeamChange(int teamId, Player player) { gameManager.requestTeamChange(teamId, player); } + /** + * Player can request its own change of team + * @param teamID target team id + * @param player player requesting the change + */ + public void requestTeamChangeForPlayer(int teamID, Player player) { + gameManager.requestTeamChangeForPlayer(teamID, player); + } + public void requestGameMaster(Player player) { gameManager.requestGameMaster(player); } diff --git a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java index a8971ffc63e..ec113305ddd 100644 --- a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java +++ b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java @@ -18,6 +18,7 @@ import megamek.common.Player; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -50,7 +51,7 @@ public List> defineArguments() { } @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { IntegerArgument unitID = (IntegerArgument) args.get(UNIT_ID); IntegerArgument playerID = (IntegerArgument) args.get(PLAYER_ID); diff --git a/megamek/src/megamek/server/commands/ChangeTeamCommand.java b/megamek/src/megamek/server/commands/ChangeTeamCommand.java new file mode 100644 index 00000000000..74507dec788 --- /dev/null +++ b/megamek/src/megamek/server/commands/ChangeTeamCommand.java @@ -0,0 +1,74 @@ +/* + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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. + */ +package megamek.server.commands; + +import megamek.client.ui.Messages; +import megamek.common.Entity; +import megamek.common.Player; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; + +/** + * The Server Command "/changeOwner" that will switch an entity's owner to another player. + * + * @author Luana Coppio + */ +public class ChangeTeamCommand extends GamemasterServerCommand { + + public static final String PLAYER_ID = "playerID"; + public static final String TEAM_ID = "teamID"; + + public ChangeTeamCommand(Server server, TWGameManager gameManager) { + super(server, + gameManager, + "changeTeam", + Messages.getString("Gamemaster.cmd.changeteam.help"), + Messages.getString("Gamemaster.cmd.changeteam.longName")); + } + + @Override + public List> defineArguments() { + return List.of( + new IntegerArgument(PLAYER_ID, Messages.getString("Gamemaster.cmd.changeteam.playerID")), + new IntegerArgument(TEAM_ID, Messages.getString("Gamemaster.cmd.changeteam.teamID"))); + } + + @Override + protected void runCommand(int connId, Arguments args) { + int teamID = ((IntegerArgument) args.get(TEAM_ID)).getValue(); + int playerID = ((IntegerArgument) args.get(PLAYER_ID)).getValue(); + + Player player = server.getGame().getPlayer(playerID); + if (null == player) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.changeteam.playerNotFound")); + return; + } + + int numEntities = server.getGame().getEntitiesOwnedBy(player); + if ((Player.TEAM_UNASSIGNED == teamID) && (numEntities != 0)) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.changeteam.playerCantJoinUnassigned")); + return; + } + + server.requestTeamChangeForPlayer(teamID, player); + gameManager.allowTeamChange(); + + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.changeteam.success", player.getName(), teamID)); + } +} diff --git a/megamek/src/megamek/server/commands/ChangeWeatherCommand.java b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java index 657eaf5fe50..28320a4e598 100644 --- a/megamek/src/megamek/server/commands/ChangeWeatherCommand.java +++ b/megamek/src/megamek/server/commands/ChangeWeatherCommand.java @@ -17,6 +17,7 @@ import megamek.common.planetaryconditions.*; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.OptionalEnumArgument; import megamek.server.totalwarfare.TWGameManager; @@ -70,7 +71,7 @@ public void updatePlanetaryCondition(Enum value, int connId, Server server) { * Run this command with the arguments supplied */ @Override - public void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { if (getGameManager().getGame().getBoard().inSpace()) { server.sendServerChat(connId, "There is no planetary conditions to change outside of a planet"); return; @@ -81,9 +82,9 @@ public void runAsGM(int connId, Map> args) { getGameManager().getGame().setPlanetaryConditions(planetaryConditions); } - private BiConsumer> updatePlanetaryConditions(int connId, Map> args) { + private BiConsumer> updatePlanetaryConditions(int connId, Arguments args) { return (prefix, condition) -> { - if (args.containsKey(prefix) && ((OptionalEnumArgument) args.get(prefix)).isPresent()) { + if (args.hasArg(prefix) && ((OptionalEnumArgument) args.get(prefix)).isPresent()) { var value = ((OptionalEnumArgument) args.get(prefix)).getValue(); condition.updatePlanetaryCondition(value, connId, server); } diff --git a/megamek/src/megamek/server/commands/ClientServerCommand.java b/megamek/src/megamek/server/commands/ClientServerCommand.java new file mode 100644 index 00000000000..7244d546853 --- /dev/null +++ b/megamek/src/megamek/server/commands/ClientServerCommand.java @@ -0,0 +1,210 @@ +/* + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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. + */ +package megamek.server.commands; + +import megamek.client.ui.Messages; +import megamek.logging.MMLogger; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A ServerCommand that can only be used by Game Masters, + * This abstract class implements many features that are common to all Game Master commands, + * like the isGM check for users, it also uses the Argument class for building the command arguments + * and to abstract the parsing of the arguments, limit assertion and error handling, and for building + * a more dynamic "help" feature. + * It also has a more advanced parser and argument handling than the ServerCommand class, which allows for + * named arguments, positional arguments, optional arguments and default values. + * named arguments can be passed in any order, and positional arguments are parsed in order and MUST appear before named + * arguments. + * + * @author Luana Coppio + */ +public abstract class ClientServerCommand extends ServerCommand { + private static final String NEWLINE = "\n"; + private static final String WHITESPACE = " "; + private static final String LONG_WHITESPACE = " "; + private static final String EMPTY_ARGUMENT = null; + protected final TWGameManager gameManager; + protected final static MMLogger logger = MMLogger.create(ClientServerCommand.class); + protected final String errorMsg; + private final String longName; + + /** + * Creates new ServerCommand that can only be used by Game Masters + * + * @param server instance of the server + * @param gameManager instance of the game manager + * @param name the name of the command + * @param helpText the help text for the command + */ + public ClientServerCommand(Server server, TWGameManager gameManager, String name, String helpText, String longName) { + super(server, name, helpText); + this.gameManager = gameManager; + this.errorMsg = "Error executing command: " + name; + this.longName = longName; + } + + protected TWGameManager getGameManager() { + return gameManager; + } + + @Override + public void run(int connId, String[] args) { + if (!preRun(connId)) { + server.sendServerChat(connId, "Can't run command " + this.longName + " for user " + server.getPlayer(connId).getName()); + return; + } + safeParseArgumentsAndRun(connId, args); + } + + protected boolean preRun(int connId) { + // Override to add pre-run checks, return false to cancel the command + return true; + } + + private void safeParseArgumentsAndRun(int connId, String[] args) { + try { + var parsedArguments = new Arguments(parseArguments(args)); + runCommand(connId, parsedArguments); + } catch (IllegalArgumentException e) { + server.sendServerChat(connId, "Invalid arguments: " + e.getMessage() + "\nUsage: " + this.getHelp()); + } catch (Exception e) { + server.sendServerChat(connId, "An error occurred while executing the command. Check the log for more information"); + logger.error(errorMsg, e); + } + } + + // Method to parse arguments, to be implemented by the specific command class + public List> defineArguments() { + return List.of(); + } + + + // Parses the arguments using the definition + private Map> parseArguments(String[] args) { + + List> argumentDefinitions = defineArguments(); + Map> parsedArguments = new HashMap<>(); + List positionalArguments = new ArrayList<>(); + + // Map argument names to definitions for easy lookup + Map> argumentMap = new HashMap<>(); + for (Argument argument : argumentDefinitions) { + argumentMap.put(argument.getName(), argument); + } + + // Separate positional arguments and named arguments + boolean namedArgumentStarted = false; + for (int i = 1; i < args.length; i++) { + String arg = args[i]; + String[] keyValue = arg.split("="); + + if (keyValue.length == 2) { + // Handle named arguments + namedArgumentStarted = true; + String key = keyValue[0]; + String value = keyValue[1]; + + if (!argumentMap.containsKey(key)) { + throw new IllegalArgumentException("Unknown argument: " + key); + } + + Argument argument = argumentMap.get(key); + argument.parse(value); + parsedArguments.put(key, argument); + } else { + // Handle positional arguments + if (namedArgumentStarted) { + throw new IllegalArgumentException("Positional arguments cannot come after named arguments."); + } + positionalArguments.add(arg); + } + } + + // Parse positional arguments + int index = 0; + for (Argument argument : argumentDefinitions) { + if (parsedArguments.containsKey(argument.getName())) { + continue; + } + if (index < positionalArguments.size()) { + String value = positionalArguments.get(index); + argument.parse(value); + parsedArguments.put(argument.getName(), argument); + index++; + } else { + // designed to throw an error if the arg doesn't have a default value + argument.parse(EMPTY_ARGUMENT); + parsedArguments.put(argument.getName(), argument); + } + } + + return parsedArguments; + } + + public String getHelpHtml() { + return "" + + this.getHelp() + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll(LONG_WHITESPACE, "| ") + .replaceAll(NEWLINE, "
")+ + ""; + } + + @Override + public String getHelp() { + StringBuilder help = new StringBuilder(); + help.append(super.getHelp()) + .append(NEWLINE) + .append(Messages.getString("Gamemaster.cmd.help")) + .append(NEWLINE) + .append(NEWLINE) + .append("/") + .append(getName()); + + for (Argument arg : defineArguments()) { + help.append(WHITESPACE) + .append(arg.getRepr()); + } + + help.append(NEWLINE) + .append(NEWLINE); + + for (var arg : defineArguments()) { + help.append(LONG_WHITESPACE) + .append(arg.getName()) + .append(":") + .append(WHITESPACE) + .append(arg.getHelp()) + .append(NEWLINE); + } + return help.toString(); + } + + public String getLongName() { + return longName; + } + + // The new method for game master commands that uses parsed arguments + protected abstract void runCommand(int connId, Arguments args); +} diff --git a/megamek/src/megamek/server/commands/DisasterCommand.java b/megamek/src/megamek/server/commands/DisasterCommand.java index 10642a42686..6150ef019b8 100644 --- a/megamek/src/megamek/server/commands/DisasterCommand.java +++ b/megamek/src/megamek/server/commands/DisasterCommand.java @@ -17,6 +17,7 @@ import megamek.common.Coords; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.EnumArgument; import megamek.server.totalwarfare.TWGameManager; @@ -188,7 +189,7 @@ private void orbitalBombardment(int connId) { * Run this command with the arguments supplied */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { if (args.get(TYPE).getValue().equals(Disaster.RANDOM)) { if (getGameManager().getGame().getBoard().inSpace()) { runDisasterCommand(connId, Disaster.getRandomSpaceDisaster()); diff --git a/megamek/src/megamek/server/commands/EndGameCommand.java b/megamek/src/megamek/server/commands/EndGameCommand.java new file mode 100644 index 00000000000..2390f534435 --- /dev/null +++ b/megamek/src/megamek/server/commands/EndGameCommand.java @@ -0,0 +1,66 @@ +/* + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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. + */ +package megamek.server.commands; + +import megamek.client.ui.Messages; +import megamek.common.options.OptionsConstants; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.commands.arguments.BooleanArgument; +import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; + +/** + * The Server Command "/end" that will finish a game immediately declaring forced victory for a player or their team. + * + * @author Luana Coppio + */ +public class EndGameCommand extends GamemasterServerCommand { + + public static final String PLAYER_ID = "playerID"; + public static final String FORCE = "force"; + + public EndGameCommand(Server server, TWGameManager gameManager) { + super(server, + gameManager, + "end", + Messages.getString("Gamemaster.cmd.endgame.help"), + Messages.getString("Gamemaster.cmd.endgame.longName")); + } + + @Override + public List> defineArguments() { + return List.of( + new IntegerArgument(PLAYER_ID, Messages.getString("Gamemaster.cmd.endgame.playerID")), + new BooleanArgument(FORCE, Messages.getString("Gamemaster.cmd.endgame.force"), false)); + } + + @Override + protected void runCommand(int connId, Arguments args) { + int playerID = ((IntegerArgument) args.get(PLAYER_ID)).getValue(); + boolean force = ((BooleanArgument) args.get(FORCE)).getValue(); + + var player = server.getGame().getPlayer(playerID); + if (player == null) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.endgame.playerNotFound")); + return; + } + + gameManager.forceVictory(player, force, true); + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.endgame.success")); + } +} diff --git a/megamek/src/megamek/server/commands/FirefightCommand.java b/megamek/src/megamek/server/commands/FirefightCommand.java index 956f3508d1b..1e1ba0d6254 100644 --- a/megamek/src/megamek/server/commands/FirefightCommand.java +++ b/megamek/src/megamek/server/commands/FirefightCommand.java @@ -18,6 +18,7 @@ import megamek.common.Hex; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -59,7 +60,7 @@ public List> defineArguments() { * @see ServerCommand#run(int, String[]) */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { int xArg = (int) args.get(X).getValue() - 1; int yArg = (int) args.get(Y).getValue() - 1; firefight(new Coords(xArg, yArg)); diff --git a/megamek/src/megamek/server/commands/FirestarterCommand.java b/megamek/src/megamek/server/commands/FirestarterCommand.java index be52e9ba099..702f57b02ca 100644 --- a/megamek/src/megamek/server/commands/FirestarterCommand.java +++ b/megamek/src/megamek/server/commands/FirestarterCommand.java @@ -18,6 +18,7 @@ import megamek.common.Hex; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -59,7 +60,7 @@ public List> defineArguments() { * @see ServerCommand#run(int, String[]) */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { if (getGameManager().getGame().getBoard().inSpace()) { server.sendServerChat(connId, "Can't start a fire in space"); return; diff --git a/megamek/src/megamek/server/commands/FirestormCommand.java b/megamek/src/megamek/server/commands/FirestormCommand.java index c85eda5738b..046d1773330 100644 --- a/megamek/src/megamek/server/commands/FirestormCommand.java +++ b/megamek/src/megamek/server/commands/FirestormCommand.java @@ -18,12 +18,12 @@ import megamek.common.Hex; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; import java.util.HashSet; import java.util.List; -import java.util.Map; /** * The Server Command "/firestorm" that starts a blazing inferno on the board. @@ -52,13 +52,14 @@ public List> defineArguments() { ); } + /** * Run this command with the arguments supplied * * @see ServerCommand#run(int, String[]) */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { if (getGameManager().getGame().getBoard().inSpace()) { server.sendServerChat(connId, "Can't start a firestorm in space"); } diff --git a/megamek/src/megamek/server/commands/GamemasterServerCommand.java b/megamek/src/megamek/server/commands/GamemasterServerCommand.java index dc346a6a147..e4e5a82a7ec 100644 --- a/megamek/src/megamek/server/commands/GamemasterServerCommand.java +++ b/megamek/src/megamek/server/commands/GamemasterServerCommand.java @@ -13,14 +13,10 @@ */ package megamek.server.commands; -import megamek.client.ui.Messages; -import megamek.logging.MMLogger; import megamek.server.Server; -import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.totalwarfare.TWGameManager; -import java.util.*; - /** * A ServerCommand that can only be used by Game Masters, * This abstract class implements many features that are common to all Game Master commands, @@ -34,15 +30,7 @@ * * @author Luana Coppio */ -public abstract class GamemasterServerCommand extends ServerCommand { - private static final String NEWLINE = "\n"; - private static final String WHITESPACE = " "; - private static final String LONG_WHITESPACE = " "; - private static final String EMPTY_ARGUMENT = null; - protected final TWGameManager gameManager; - protected final static MMLogger logger = MMLogger.create(GamemasterServerCommand.class); - private final String errorMsg; - private final String longName; +public abstract class GamemasterServerCommand extends ClientServerCommand { /** * Creates new ServerCommand that can only be used by Game Masters @@ -53,148 +41,24 @@ public abstract class GamemasterServerCommand extends ServerCommand { * @param helpText the help text for the command */ public GamemasterServerCommand(Server server, TWGameManager gameManager, String name, String helpText, String longName) { - super(server, name, helpText); - this.gameManager = gameManager; - this.errorMsg = "Error executing command: " + name; - this.longName = longName; + super(server, gameManager, name, helpText, longName); } private boolean isGM(int connId) { return server.getGameManager().getGame().getPlayer(connId).getGameMaster(); } - protected TWGameManager getGameManager() { - return gameManager; - } - - @Override - public void run(int connId, String[] args) { - if (!isGM(connId)) { - server.sendServerChat(connId, "This command can only be used by a game master."); - return; - } - - try { - Map> parsedArguments = parseArguments(args); - runAsGM(connId, parsedArguments); - } catch (IllegalArgumentException e) { - server.sendServerChat(connId, "Invalid arguments: " + e.getMessage() + "\nUsage: " + this.getHelp()); - } catch (Exception e) { - server.sendServerChat(connId, "An error occurred while executing the command. Check the log for more information"); - logger.error(errorMsg, e); - } - } - - // Method to parse arguments, to be implemented by the specific command class - public abstract List> defineArguments(); - - - // Parses the arguments using the definition - private Map> parseArguments(String[] args) { - - List> argumentDefinitions = defineArguments(); - Map> parsedArguments = new HashMap<>(); - List positionalArguments = new ArrayList<>(); - - // Map argument names to definitions for easy lookup - Map> argumentMap = new HashMap<>(); - for (Argument argument : argumentDefinitions) { - argumentMap.put(argument.getName(), argument); - } - - // Separate positional arguments and named arguments - boolean namedArgumentStarted = false; - for (int i = 1; i < args.length; i++) { - String arg = args[i]; - String[] keyValue = arg.split("="); - - if (keyValue.length == 2) { - // Handle named arguments - namedArgumentStarted = true; - String key = keyValue[0]; - String value = keyValue[1]; - - if (!argumentMap.containsKey(key)) { - throw new IllegalArgumentException("Unknown argument: " + key); - } - - Argument argument = argumentMap.get(key); - argument.parse(value); - parsedArguments.put(key, argument); - } else { - // Handle positional arguments - if (namedArgumentStarted) { - throw new IllegalArgumentException("Positional arguments cannot come after named arguments."); - } - positionalArguments.add(arg); - } - } - - // Parse positional arguments - int index = 0; - for (Argument argument : argumentDefinitions) { - if (parsedArguments.containsKey(argument.getName())) { - continue; - } - if (index < positionalArguments.size()) { - String value = positionalArguments.get(index); - argument.parse(value); - parsedArguments.put(argument.getName(), argument); - index++; - } else { - // designed to throw an error if the arg doesn't have a default value - argument.parse(EMPTY_ARGUMENT); - parsedArguments.put(argument.getName(), argument); - } - } - - return parsedArguments; - } - - public String getHelpHtml() { - return "" + - this.getHelp() - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll(LONG_WHITESPACE, "| ") - .replaceAll(NEWLINE, "
")+ - ""; - } - @Override - public String getHelp() { - StringBuilder help = new StringBuilder(); - help.append(super.getHelp()) - .append(NEWLINE) - .append(Messages.getString("Gamemaster.cmd.help")) - .append(NEWLINE) - .append(NEWLINE) - .append("/") - .append(getName()); + protected boolean preRun(int connId) { + // Override to add pre-run checks + var userIsGm = isGM(connId); - for (Argument arg : defineArguments()) { - help.append(WHITESPACE) - .append(arg.getRepr()); + if (!userIsGm) { + server.sendServerChat(connId, "This command is restricted to GMs."); + return false; } - help.append(NEWLINE) - .append(NEWLINE); - - for (var arg : defineArguments()) { - help.append(LONG_WHITESPACE) - .append(arg.getName()) - .append(":") - .append(WHITESPACE) - .append(arg.getHelp()) - .append(NEWLINE); - } - return help.toString(); - } - - public String getLongName() { - return longName; + return true; } - // The new method for game master commands that uses parsed arguments - protected abstract void runAsGM(int connId, Map> args); } diff --git a/megamek/src/megamek/server/commands/JoinTeamCommand.java b/megamek/src/megamek/server/commands/JoinTeamCommand.java index befd62a0738..afd7c35ffb5 100644 --- a/megamek/src/megamek/server/commands/JoinTeamCommand.java +++ b/megamek/src/megamek/server/commands/JoinTeamCommand.java @@ -27,7 +27,9 @@ * This command allows a player to join a specified team. * * @author arlith + * @deprecated Planned to be removed, use the GM command {@link ChangeTeamCommand} instead. */ +@Deprecated public class JoinTeamCommand extends ServerCommand { public static String SERVER_VOTE_PROMPT_MSG = "All players with an assigned team " @@ -35,10 +37,12 @@ public class JoinTeamCommand extends ServerCommand { + "to allow this change."; public JoinTeamCommand(Server server) { - super(server, "joinTeam", "Switches a player's team at the end phase. " - + "Usage: /joinTeam # where the first number is the team " - + "number to join. 0 is for no team, " + - "-1 is for unassigned team"); + super(server, "joinTeam", "Planned to be removed, use the GM command /changeTeam instead. " + + "Switches a player's team at the end phase. " + + "Usage: /joinTeam # where the first number is the team " + + "number to join. 0 is for no team, " + + "-1 is for unassigned team. " + + "Only one player can be changed teams per round."); } /** diff --git a/megamek/src/megamek/server/commands/KickCommand.java b/megamek/src/megamek/server/commands/KickCommand.java index 8838955d55a..3eff8c527ee 100644 --- a/megamek/src/megamek/server/commands/KickCommand.java +++ b/megamek/src/megamek/server/commands/KickCommand.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2000-2002 - Ben Mazur (bmazur@sev.org). - * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. * * 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 @@ -17,52 +17,94 @@ import megamek.common.net.enums.PacketCommand; import megamek.common.net.packets.Packet; import megamek.server.Server; +import megamek.server.commands.arguments.*; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; /** * Kicks a player off the server. - * + * * @author Ben + * @author Luana Coppio * @since April 5, 2002, 8:31 PM */ -public class KickCommand extends ServerCommand { +public class KickCommand extends ClientServerCommand { /** Creates a new KickCommand */ - public KickCommand(Server server) { - super(server, "kick", - "Disconnects a player. Usage: /kick [player id number]. For a list of player id #s, use the /who command."); + public KickCommand(Server server, TWGameManager gameManager) { + super(server, gameManager, "kick", + "Disconnects a player. Usage: /kick [player id number]. For a list of player id #s, use the /who command.", "kick"); } - /** - * Run this command with the arguments supplied - */ @Override - public void run(int connId, String[] args) { - int kickArg = server.isPassworded() ? 2 : 1; + public List> defineArguments() { + return List.of( + new IntegerArgument("playerID", "The player ID to kick."), + new OptionalPasswordArgument("password", "The server password.") + ); + } + @Override + protected boolean preRun(int connId) { if (!canRunRestrictedCommand(connId)) { server.sendServerChat(connId, "Observers are restricted from kicking others."); + return false; + } + return true; + } + + @Override + protected void runCommand(int connId, Arguments args) { + var kickedId = ((IntegerArgument) args.get("playerId")).getValue(); + var passwordOpt = (OptionalPasswordArgument) args.get("password"); + + if (serverPasswordCheckFailed(connId, passwordOpt)) { + // The password failed return; } - if (server.isPassworded() && ((args.length < 3) || !server.isPassword(args[1]))) { - server.sendServerChat(connId, "The password is incorrect. Usage: /kick [id#]"); - } else { - try { - int kickedId = Integer.parseInt(args[kickArg]); - - if (kickedId == connId) { - server.sendServerChat("Don't be silly."); - return; - } - - server.sendServerChat(server.getPlayer(connId).getName() - + " attempts to kick player #" + kickedId + " (" - + server.getPlayer(kickedId).getName() + ")..."); - server.send(kickedId, new Packet(PacketCommand.CLOSE_CONNECTION)); - server.getConnection(kickedId).close(); - } catch (Exception ex) { - server.sendServerChat("/kick : kick failed. Type /who for a list of players with id #s."); + try { + if (kickedId == connId) { + server.sendServerChat("Don't be silly."); + return; + } + + server.sendServerChat(server.getPlayer(connId).getName() + + " attempts to kick player #" + kickedId + " (" + + server.getPlayer(kickedId).getName() + ")..."); + + server.send(kickedId, new Packet(PacketCommand.CLOSE_CONNECTION)); + + server.getConnection(kickedId).close(); + } catch (Exception ex) { + server.sendServerChat("/kick : kick failed. Type /who for a list of players with id #s."); + } + + } + + /** + * Checks the password argument given by the player, if the server is passworded and the check fails it returns + * true + * + * @param connId The connection ID of the player issuing the command + * @param passwordOptArg The password argument + * @return Returns true if the password fails + */ + private boolean serverPasswordCheckFailed(int connId, OptionalPasswordArgument passwordOptArg) { + var passwordOpt = passwordOptArg.getValue(); + + if (server.isPassworded()) { + if (passwordOpt.isEmpty()) { + server.sendServerChat(connId, "The password is missing. Usage: /kick [id#]"); + return true; + } + if (!server.isPassword(passwordOpt.get())) { + server.sendServerChat(connId, "The password is incorrect. Usage: /kick [id#]"); + return true; } } + + return false; } } diff --git a/megamek/src/megamek/server/commands/KillCommand.java b/megamek/src/megamek/server/commands/KillCommand.java index bbbe191067c..d727f9d487e 100644 --- a/megamek/src/megamek/server/commands/KillCommand.java +++ b/megamek/src/megamek/server/commands/KillCommand.java @@ -16,6 +16,7 @@ import megamek.client.ui.Messages; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -44,7 +45,7 @@ public List> defineArguments() { * Run this command with the arguments supplied */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { int unitId = (int) args.get(UNIT_ID).getValue(); // is the unit on the board? var unit = gameManager.getGame().getEntity(unitId); diff --git a/megamek/src/megamek/server/commands/NoFiresCommand.java b/megamek/src/megamek/server/commands/NoFiresCommand.java index f097ff1fbb2..18e876dd074 100644 --- a/megamek/src/megamek/server/commands/NoFiresCommand.java +++ b/megamek/src/megamek/server/commands/NoFiresCommand.java @@ -18,6 +18,7 @@ import megamek.common.Hex; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -43,18 +44,13 @@ public NoFiresCommand(Server server, TWGameManager gameManager) { this.reason = Messages.getString("Gamemaster.cmd.firefight.reason"); } - @Override - public List> defineArguments() { - return List.of(); - } - /** * Run this command with the arguments supplied * * @see ServerCommand#run(int, String[]) */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { try { getAllCoords().forEach(this::firefight); } catch (Exception e) { diff --git a/megamek/src/megamek/server/commands/NukeCommand.java b/megamek/src/megamek/server/commands/NukeCommand.java index 322b5893c06..07800d5729a 100644 --- a/megamek/src/megamek/server/commands/NukeCommand.java +++ b/megamek/src/megamek/server/commands/NukeCommand.java @@ -1,5 +1,6 @@ /* * MegaMek - Copyright (C) 2000-2002 Ben Mazur (bmazur@sev.org) + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. * * 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 @@ -14,78 +15,97 @@ package megamek.server.commands; import megamek.common.options.OptionsConstants; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.commands.arguments.OptionalIntegerArgument; import megamek.server.totalwarfare.TWGameManager; import megamek.server.Server; +import java.util.List; + /** * @author fastsammy + * @author Luana Coppio */ -public class NukeCommand extends ServerCommand { +public class NukeCommand extends ClientServerCommand { private final TWGameManager gameManager; /** Creates new NukeCommand */ public NukeCommand(Server server, TWGameManager gameManager) { - super(server, "nuke", "Drops a nuke onto the board, to be exploded at" + + super(server, gameManager, "nuke", "Drops a nuke onto the board, to be exploded at" + "the end of the next weapons attack phase." + "Allowed formats:"+ "/nuke and" + - "/nuke " + + "/nuke " + "where type is 0-4 (0: Davy-Crockett-I, 1: Davy-Crockett-M, 2: Alamo, 3: Santa Ana, 4: Peacemaker)" + - "and hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)"); + "and hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)", "Nuclear Strike"); this.gameManager = gameManager; } - /** - * Run this command with the arguments supplied - */ @Override - public void run(int connId, String[] args) { + public List> defineArguments() { + return List.of( + new IntegerArgument("x", "The x-coordinate of the hex to nuke."), + new IntegerArgument("y", "The y-coordinate of the hex to nuke."), + new OptionalIntegerArgument("type", "The type of nuke to drop. " + + "(0: Davy-Crockett-I, 1: Davy-Crockett-M, 2: Alamo, 3: Santa Ana, 4: Peacemaker)", 0, 4), + new OptionalIntegerArgument("dmg", "The damage of the nuke.", 0, 1_000_000), + new OptionalIntegerArgument("deg", "The degredation of the nuke.", 0, 1_000_000), + new OptionalIntegerArgument("radius", "The secondary radius of the nuke.", 1, 1000), + new OptionalIntegerArgument("depth", "The crater depth of the nuke.", 0, 100) + ); + } + @Override + protected void runCommand(int connId, Arguments args) { // Check to make sure nuking is allowed by game options! - if (!(server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_REALLY_ALLOW_NUKES) && server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_ALLOW_NUKES))) { + if (!(server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_REALLY_ALLOW_NUKES) + && server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_ALLOW_NUKES))) { server.sendServerChat(connId, "Command-line nukes are not enabled in this game."); return; } - // Check argument integrity. - if (args.length == 4) { - // Check command type 1 + var typeOpt = ((OptionalIntegerArgument) args.get("type")).getValue(); + + if (typeOpt.isPresent()) { + // try { int[] nuke = new int[3]; - for (int i = 1; i < 4; i++) { - nuke[i - 1] = Integer.parseInt(args[i]); - } + nuke[0] = ((IntegerArgument) args.get("x")).getValue() - 1; + nuke[1] = ((IntegerArgument) args.get("y")).getValue() - 1; + nuke[2] = typeOpt.orElseThrow(); // is the hex on the board? - if (!gameManager.getGame().getBoard().contains(nuke[0] - 1, nuke[1] - 1)) { + if (!gameManager.getGame().getBoard().contains(nuke[0] , nuke[1])) { server.sendServerChat(connId, "Specified hex is not on the board."); return; } gameManager.addScheduledNuke(nuke); server.sendServerChat(connId, "A nuke is incoming! Take cover!"); } catch (Exception e) { - server.sendServerChat(connId, "Nuke command failed (1). Proper format is \"/nuke \" or \"/nuke \" where type is 0-4 (0: Davy-Crockett-I, 1: Davy-Crockett-M, 2: Alamo, 3: Santa Ana, 4: Peacemaker) and hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)"); + server.sendServerChat(connId, "Nuke command failed (1). " + getHelp()); } - } else if (args.length == 7) { - // Check command type 2. + } else { try { int[] nuke = new int[6]; - for (int i = 1; i < 7; i++) { - nuke[i-1] = Integer.parseInt(args[i]); - } + nuke[0] = ((IntegerArgument) args.get("x")).getValue() - 1; + nuke[1] = ((IntegerArgument) args.get("y")).getValue() - 1; + nuke[2] = ((OptionalIntegerArgument) args.get("dmg")).getValue().orElseThrow(); + nuke[3] = ((OptionalIntegerArgument) args.get("deg")).getValue().orElseThrow(); + nuke[4] = ((OptionalIntegerArgument) args.get("radius")).getValue().orElseThrow(); + nuke[5] = ((OptionalIntegerArgument) args.get("depth")).getValue().orElseThrow(); + // is the hex on the board? - if (!gameManager.getGame().getBoard().contains(nuke[0] - 1, nuke[1] - 1)) { + if (!gameManager.getGame().getBoard().contains(nuke[0], nuke[1])) { server.sendServerChat(connId, "Specified hex is not on the board."); return; } gameManager.addScheduledNuke(nuke); server.sendServerChat(connId, "A nuke is incoming! Take cover!"); } catch (Exception e) { - server.sendServerChat(connId, "Nuke command failed (2). Proper format is \"/nuke \" or \"/nuke \""); + server.sendServerChat(connId, "Nuke command failed (2). " + getHelp()); } - } else { - // Error out; it's not a valid call. - server.sendServerChat(connId, "Nuke command failed (3). Proper format is \"/nuke \" or \"/nuke \" where type is 0-4 (0: Davy-Crockett-I, 1: Davy-Crockett-M, 2: Alamo, 3: Santa Ana, 4: Peacemaker) and hex x, y is x=column number and y=row number (hex 0923 would be x=9 and y=23)"); } } } diff --git a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java index 450338e312a..b3e4cd530b6 100644 --- a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java +++ b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java @@ -16,6 +16,7 @@ import megamek.client.ui.Messages; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.props.OrbitalBombardment; import megamek.server.totalwarfare.TWGameManager; @@ -51,7 +52,7 @@ public List> defineArguments() { * Run this command with the arguments supplied */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { var orbitalBombardmentBuilder = new OrbitalBombardment.Builder(); diff --git a/megamek/src/megamek/server/commands/RemoveSmokeCommand.java b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java index 25e24540d0a..134c2e48d8c 100644 --- a/megamek/src/megamek/server/commands/RemoveSmokeCommand.java +++ b/megamek/src/megamek/server/commands/RemoveSmokeCommand.java @@ -16,6 +16,7 @@ import megamek.client.ui.Messages; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.totalwarfare.TWGameManager; import java.util.List; @@ -33,12 +34,7 @@ public RemoveSmokeCommand(Server server, TWGameManager gameManager) { } @Override - public List> defineArguments() { - return List.of(); - } - - @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { gameManager.getSmokeCloudList().forEach(gameManager::removeSmokeTerrain); server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.removesmoke.success")); } diff --git a/megamek/src/megamek/server/commands/RescueCommand.java b/megamek/src/megamek/server/commands/RescueCommand.java index 62c9df1c491..efabdebad47 100644 --- a/megamek/src/megamek/server/commands/RescueCommand.java +++ b/megamek/src/megamek/server/commands/RescueCommand.java @@ -17,6 +17,7 @@ import megamek.common.MovePath; import megamek.server.Server; import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; import megamek.server.commands.arguments.IntegerArgument; import megamek.server.totalwarfare.TWGameManager; @@ -45,7 +46,7 @@ public List> defineArguments() { * Run this command with the arguments supplied */ @Override - protected void runAsGM(int connId, Map> args) { + protected void runCommand(int connId, Arguments args) { int unitId = (int) args.get(UNIT_ID).getValue(); // is the unit on the board? var unit = gameManager.getGame().getEntity(unitId); diff --git a/megamek/src/megamek/server/commands/VictoryCommand.java b/megamek/src/megamek/server/commands/VictoryCommand.java index 39d7a5e2952..674e7b34fc9 100644 --- a/megamek/src/megamek/server/commands/VictoryCommand.java +++ b/megamek/src/megamek/server/commands/VictoryCommand.java @@ -19,7 +19,7 @@ /** * Causes automatic victory at the end of the current turn. - * + * * @author Ben * @since July 11, 2002, 2:24 PM */ @@ -85,7 +85,7 @@ private void reset(int connId) { } else { server.sendServerChat(getDeclareTeam(player.getName())); } - gameManager.forceVictory(player); + gameManager.forceVictory(player, false, false); } } diff --git a/megamek/src/megamek/server/commands/arguments/Argument.java b/megamek/src/megamek/server/commands/arguments/Argument.java index 3b77343129a..e01085562ba 100644 --- a/megamek/src/megamek/server/commands/arguments/Argument.java +++ b/megamek/src/megamek/server/commands/arguments/Argument.java @@ -42,4 +42,5 @@ public String getRepr() { public abstract String getHelp(); public abstract void parse(String input) throws IllegalArgumentException; + } diff --git a/megamek/src/megamek/server/commands/arguments/Arguments.java b/megamek/src/megamek/server/commands/arguments/Arguments.java new file mode 100644 index 00000000000..e3fd5d16187 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/Arguments.java @@ -0,0 +1,27 @@ +package megamek.server.commands.arguments; + +import java.util.Map; + +public class Arguments { + + private final Map> arguments; + + public Arguments(Map> arguments) { + this.arguments = arguments; + } + + public Argument get(String name) { + return arguments.get(name); + } + + public boolean hasArg(String name) { + return arguments.containsKey(name); + } + + @Override + public String toString() { + return "Arguments{" + + "arguments=" + arguments + + '}'; + } +} diff --git a/megamek/src/megamek/server/commands/arguments/BooleanArgument.java b/megamek/src/megamek/server/commands/arguments/BooleanArgument.java new file mode 100644 index 00000000000..6ecbeeed741 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/BooleanArgument.java @@ -0,0 +1,56 @@ +package megamek.server.commands.arguments; + +import megamek.client.ui.Messages; + +import java.util.List; + +/** + * Argument for a boolean type. + * @author Luana Coppio + */ +public class BooleanArgument extends Argument { + private final Boolean defaultValue; + + public BooleanArgument(String name, String description, Boolean defaultValue) { + super(name, description); + this.defaultValue = defaultValue; + } + + public BooleanArgument(String name, String description) { + this(name, description, null); + } + + @Override + public Boolean getValue() { + if (value == null && defaultValue != null) { + return defaultValue; + } + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null && defaultValue != null) { + value = defaultValue; + return; + } else { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + } + value = List.of("true", "yes", "1", "on", "y").contains(input.toLowerCase()); + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + @Override + public String getHelp() { + return getDescription() + + (defaultValue != null ? + " [default: " + defaultValue + "]. " + Messages.getString("Gamemaster.cmd.params.optional") : + " " + Messages.getString("Gamemaster.cmd.params.required")); + } + +} diff --git a/megamek/src/megamek/server/commands/arguments/OptionalPasswordArgument.java b/megamek/src/megamek/server/commands/arguments/OptionalPasswordArgument.java new file mode 100644 index 00000000000..7dd8a5db224 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/OptionalPasswordArgument.java @@ -0,0 +1,54 @@ +/* + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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. + */ +package megamek.server.commands.arguments; + +import megamek.client.ui.Messages; + +import java.util.Optional; + +/** + * Optional Argument for a Password String type. + * @author Luana Coppio + */ +public class OptionalPasswordArgument extends Argument> { + + public OptionalPasswordArgument(String name, String description) { + super(name, description); + } + + @Override + public Optional getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + value = Optional.empty(); + return; + } + value = Optional.of(input); + + } + + @Override + public String getRepr() { + return "[" + getName() + "]"; + } + + @Override + public String getHelp() { + return getDescription() + ". " + Messages.getString("Gamemaster.cmd.params.optional"); + } +} diff --git a/megamek/src/megamek/server/commands/arguments/OptionalStringArgument.java b/megamek/src/megamek/server/commands/arguments/OptionalStringArgument.java new file mode 100644 index 00000000000..cc149d79e5b --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/OptionalStringArgument.java @@ -0,0 +1,54 @@ +/* + * MegaMek - Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * 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 Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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. + */ +package megamek.server.commands.arguments; + +import megamek.client.ui.Messages; + +import java.util.Optional; + +/** + * Optional Argument for a String type. + * @author Luana Coppio + */ +public class OptionalStringArgument extends Argument> { + + public OptionalStringArgument(String name, String description) { + super(name, description); + } + + @Override + public Optional getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + value = Optional.empty(); + return; + } + value = Optional.of(input); + + } + + @Override + public String getRepr() { + return "[" + getName() + "]"; + } + + @Override + public String getHelp() { + return getDescription() + ". " + Messages.getString("Gamemaster.cmd.params.optional"); + } +} diff --git a/megamek/src/megamek/server/commands/arguments/StringArgument.java b/megamek/src/megamek/server/commands/arguments/StringArgument.java new file mode 100644 index 00000000000..5b91cb4fe1c --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/StringArgument.java @@ -0,0 +1,56 @@ +package megamek.server.commands.arguments; + +import megamek.client.ui.Messages; + +import java.util.List; + +/** + * Argument for a String type. + * @author Luana Coppio + */ +public class StringArgument extends Argument { + private final String defaultValue; + + public StringArgument(String name, String description, String defaultValue) { + super(name, description); + this.defaultValue = defaultValue; + } + + public StringArgument(String name, String description) { + this(name, description, null); + } + + @Override + public String getValue() { + if (value == null && defaultValue != null) { + return defaultValue; + } + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null && defaultValue != null) { + value = defaultValue; + return; + } else { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + } + value = input; + } + + public boolean hasDefaultValue() { + return defaultValue != null; + } + + @Override + public String getHelp() { + return getDescription() + + (defaultValue != null ? + " [default: " + defaultValue + "]. " + Messages.getString("Gamemaster.cmd.params.optional") : + " " + Messages.getString("Gamemaster.cmd.params.required")); + } + +} diff --git a/megamek/src/megamek/server/sbf/SBFGameManager.java b/megamek/src/megamek/server/sbf/SBFGameManager.java index f079a9fa1d2..96377e008a4 100644 --- a/megamek/src/megamek/server/sbf/SBFGameManager.java +++ b/megamek/src/megamek/server/sbf/SBFGameManager.java @@ -163,6 +163,11 @@ public void requestGameMaster(Player player) { public void requestTeamChange(int teamId, Player player) { } + @Override + public void requestTeamChangeForPlayer(int teamID, Player player) { + } + + @Override public List getCommandList(Server server) { return Collections.emptyList(); diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 9cad65ee4da..069ea437082 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -122,6 +122,8 @@ public Vector getvPhaseReport() { */ private Player playerChangingTeam = null; + private List playersChangingTeam = new ArrayList<>(); + /** * Flag that is set to true when all players have voted to allow another * player to change teams. @@ -208,6 +210,8 @@ public List getCommandList(Server server) { commands.add(new JoinTeamCommand(server)); commands.add(new AllowGameMasterCommand(server, this)); commands.add(new GameMasterCommand(server)); + commands.add(new ChangeTeamCommand(server, this)); + commands.add(new EndGameCommand(server, this)); return commands; } @@ -341,11 +345,31 @@ public void setSeeAll(Player player, boolean seeAll) { sendServerChat(player.getName() + " set SeeAll: " + player.getSeeAll()); } + private record TeamChangeRequest(int teamID, Player player){}; + + /** + * request the change of a player from a team to another + * @param teamID + * @param player + * @deprecated Planned to be removed. Use {@link #requestTeamChangeForPlayer(int, Player)} instead. + */ @Override - public void requestTeamChange(int team, Player player) { - requestedTeam = team; + @Deprecated + public void requestTeamChange(int teamID, Player player) { + requestedTeam = teamID; playerChangingTeam = player; changePlayersTeam = false; + playersChangingTeam.add(new TeamChangeRequest(teamID, player)); + } + + /** + * request the change of a player from a team to another + * @param teamID + * @param player + */ + @Override + public void requestTeamChangeForPlayer(int teamID, Player player) { + playersChangingTeam.add(new TeamChangeRequest(teamID, player)); } public void allowTeamChange() { @@ -364,8 +388,25 @@ public int getRequestedTeam() { return requestedTeam; } + + /** + * Changes the team of the player specified in the team change request and updates the game state. + */ void processTeamChangeRequest() { - if (playerChangingTeam != null) { + // Change requested by a GM must execute. + playersChangingTeam.forEach(this::changePlayerTeams); + playersChangingTeam.clear(); + + // Changes requested by players follow the default behavior + legacyProcessTeamChangeRequest(); + } + + /** + * Changes the team of the player specified in the team change request and updates the game state. + * @deprecated Planned to be removed at a later date + */ + private void legacyProcessTeamChangeRequest() { + if (playerChangingTeam != null && changePlayersTeam) { playerChangingTeam.setTeam(requestedTeam); getGame().setupTeams(); transmitPlayerUpdate(playerChangingTeam); @@ -377,19 +418,36 @@ void processTeamChangeRequest() { } sendServerChat(playerChangingTeam.getName() + " has changed teams to " + teamString); playerChangingTeam = null; + changePlayersTeam = false; } - changePlayersTeam = false; + } + + /** + * Changes the team of the player specified in the team change request and updates the game state. + * + * @param teamChangeRequest the request containing the player and the new team ID + */ + void changePlayerTeams(TeamChangeRequest teamChangeRequest) { + teamChangeRequest.player().setTeam(teamChangeRequest.teamID()); + getGame().setupTeams(); + transmitPlayerUpdate(teamChangeRequest.player()); + String teamString = "Team " + teamChangeRequest.teamID() + "!"; + if (teamChangeRequest.teamID() == Player.TEAM_UNASSIGNED) { + teamString = " unassigned!"; + } else if (teamChangeRequest.teamID() == Player.TEAM_NONE) { + teamString = " lone wolf!"; + } + sendServerChat(teamChangeRequest.player().getName() + " has changed teams to " + teamString); } @Override public void disconnect(Player player) { // in the lounge, just remove all entities for that player if (getGame().getPhase().isLounge()) { - List gms = game.getPlayersList().stream().filter(p -> p.isGameMaster()) - .collect(Collectors.toList()); + var gm = game.getPlayersList().stream().filter(Player::isGameMaster).findAny(); - if (gms.size() > 0) { - transferAllEnititiesOwnedBy(player, gms.get(0)); + if (gm.isPresent()) { + transferAllEnititiesOwnedBy(player, gm.get()); } else { removeAllEntitiesOwnedBy(player); } @@ -1274,10 +1332,12 @@ private String getDetailedVictoryReport() { } /** - * Forces victory for the specified player, or his/her team at the end of the + * Forces victory for a specified player or their team at the end of the * round. */ - public void forceVictory(Player victor) { + public void forceVictory(Player victor, boolean endImmediately, boolean ignorePlayerVotes) { + game.setEndImmediately(endImmediately); + game.setIgnorePlayerDefeatVotes(endImmediately); game.setForceVictory(true); if (victor.getTeam() == Player.TEAM_NONE) { game.setVictoryPlayerId(victor.getId()); @@ -2176,9 +2236,13 @@ public boolean victory() { return vr.isVictory(); }// end victory + private boolean isPlayerForcedVictory() { // check game options - if (!game.getOptions().booleanOption(OptionsConstants.VICTORY_SKIP_FORCED_VICTORY)) { + var dontSkipForcedVictory = !game.getOptions().booleanOption(OptionsConstants.VICTORY_SKIP_FORCED_VICTORY); + var dontEndImmediately = !game.isEndImmediately(); + + if (dontSkipForcedVictory && dontEndImmediately) { return false; } @@ -2186,17 +2250,18 @@ private boolean isPlayerForcedVictory() { return false; } - for (Player player : game.getPlayersList()) { - if ((player.getId() == game.getVictoryPlayerId()) || ((player.getTeam() == game.getVictoryTeam()) - && (game.getVictoryTeam() != Player.TEAM_NONE))) { - continue; - } + if (!game.isIgnorePlayerDefeatVotes()) { + for (Player player : game.getPlayersList()) { + if ((player.getId() == game.getVictoryPlayerId()) || ((player.getTeam() == game.getVictoryTeam()) + && (game.getVictoryTeam() != Player.TEAM_NONE))) { + continue; + } - if (!player.admitsDefeat()) { - return false; + if (!player.admitsDefeat()) { + return false; + } } } - return true; } diff --git a/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java b/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java index 4edf0209754..8bbc8108b3c 100644 --- a/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWPhaseEndManager.java @@ -272,9 +272,7 @@ void managePhase() { break; case END_REPORT: - if (gameManager.changePlayersTeam()) { - gameManager.processTeamChangeRequest(); - } + gameManager.processTeamChangeRequest(); if (gameManager.victory()) { gameManager.changePhase(GamePhase.VICTORY); } else {