diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 948ae58efe7..a0905aaeb6a 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4675,14 +4675,18 @@ Gamemaster.KillUnit=Kill Unit Gamemaster.KillUnit.text=Kill Unit {0} Gamemaster.KillUnit.confirmation=Are you sure you want to kill {0}? Gamemaster.SpecialCommands=Special Actions + #Gamemaster Chat Commands Gamemaster.cmd.missingUnit=Specified unit is not on the board. Gamemaster.cmd.error.integerparse=must be between the min and max values: +Gamemaster.cmd.error.outofbounds=Specified hex is not on the board. Gamemaster.cmd.help=Usage: Gamemaster.cmd.params.required=Required. Gamemaster.cmd.params.optional=Optional. Gamemaster.cmd.x=The x coordinate of the hex. Gamemaster.cmd.y=The y coordinate of the hex. +Gamemaster.cmd.playerID=The ID of the player that is going to be held as responsible. + # Rescue cmd Gamemaster.cmd.rescue.longName=Rescue Unit Gamemaster.cmd.rescue.unitID=ID of the unit to rescue. @@ -4721,7 +4725,7 @@ Gamemaster.cmd.changeweather.blowsand=Blowing Sand Gamemaster.cmd.changeweather.emi=EMI Gamemaster.cmd.changeweather.weather=Weather # Disaster cmd -Gamemaster.cmd.disaster.longName=Disaster +Gamemaster.cmd.disaster.longName=Disaster! Gamemaster.cmd.disaster.help=Causes a disaster on the board. Gamemaster.cmd.disaster.type=Type of disaster. Beware, some disasters are very destructive! Gamemaster.cmd.changeweather.fog.success=The fog has changed. @@ -4740,7 +4744,7 @@ Gamemaster.cmd.firestarter.longName=Start a Fire Gamemaster.cmd.fire.type=Type of fire. They are 1=Normal, 2=Inferno, 3=Inferno Bomb or 4=Inferno IV. Gamemaster.cmd.fire.help=Starts a fire on the board. # Firestorm cmd -Gamemaster.cmd.firestorm.longName=Firestorm +Gamemaster.cmd.firestorm.longName=Start a Firestorm Gamemaster.cmd.firestorm.help=Starts fire in the entire board. Gamemaster.cmd.fire.failed=Failed to ignite fire. Gamemaster.cmd.fire.percent=Percentage of the board hexes to ignite. @@ -4751,14 +4755,50 @@ Gamemaster.cmd.orbitalbombardment.dmg=Total damage at target hex. Gamemaster.cmd.orbitalbombardment.radius=Radius of the bombardment. Gamemaster.cmd.orbitalbombardment.error.outofbounds=Specified hex is not on the board. Gamemaster.cmd.orbitalbombardment.success=Orbital bombardment incoming! + +#Nuclear strike +Gamemaster.cmd.nuke.longName=Nuclear Strike +Gamemaster.cmd.nuke.help=Calls a Nuclear Strike onto the board, to be exploded at the end of the next weapons attack phase. +Gamemaster.cmd.nuke.type=The type of nuke to drop. +Gamemaster.cmd.nuke.error.disabled=Command-line nukes are not enabled in this game. + +Gamemaster.cmd.nuke.success=A nuke is incoming! Take cover! + +#Nuclear strike custom +Gamemaster.cmd.nukec.longName=Nuclear Strike (Custom) +Gamemaster.cmd.nukec.help=Calls a Nuclear Strike onto the board with a custom bomb, to be exploded at the end of the next weapons attack phase. +Gamemaster.cmd.nukec.dmg=Total damage at target hex. +Gamemaster.cmd.nukec.radius=Radius of the secondary explosion of the nuclear strike. +Gamemaster.cmd.nukec.deg=Degradation of the nuclear strike. +Gamemaster.cmd.nukec.depth=Crater depth of the nuclear strike. + # Firefight -Gamemaster.cmd.firefight.longName=Firefight +Gamemaster.cmd.firefight.longName=Extinguish a Fire 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.longName=Extinguish All 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..366106147d1 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; @@ -693,7 +693,7 @@ private JMenu createGMSpecialCommandsMenu() { new RescueCommand(null, null) ).forEach(cmd -> { JMenuItem item = new JMenuItem(cmd.getLongName()); - item.addActionListener(evt -> new GamemasterCommandPanel(gui.getFrame(), gui, cmd, coords).setVisible(true)); + item.addActionListener(evt -> new ClientCommandPanel(gui.getFrame(), gui, cmd, coords).setVisible(true)); menu.add(item); }); diff --git a/megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java b/megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java new file mode 100644 index 00000000000..ac4150f70ea --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/commands/ClientCommandPanel.java @@ -0,0 +1,428 @@ +/* + * 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.client.ui.swing.commands; + +import megamek.client.ui.swing.ClientGUI; +import megamek.common.Coords; +import megamek.common.annotations.Nullable; +import megamek.server.commands.ClientServerCommand; +import megamek.server.commands.arguments.*; + +import javax.swing.*; +import java.awt.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Dialog for executing a client command. + * @author Luana Coppio + */ +public class ClientCommandPanel extends JDialog { + private final ClientServerCommand command; + private final ClientGUI client; + private final Coords coords; + private int yPosition = 0; + + /** + * 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 ClientCommandPanel(JFrame parent, ClientGUI client, ClientServerCommand command, @Nullable Coords coords) { + super(parent, command.getLongName() + " /" + command.getName(), true); + this.command = command; + this.client = client; + this.coords = coords; + initializeUI(parent); + } + + private void initializeUI(JFrame parent) { + setLayout(new GridBagLayout()); + addTitleAndDescription(); + Map argumentComponents = addArgumentComponents(); + addExecuteButton(argumentComponents); + pack(); + setLocationRelativeTo(parent); + } + + private void addTitleAndDescription() { + + var title = new JLabel(command.getLongName()); + title.setFont(new Font("Arial", Font.BOLD, 18)); + var gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = yPosition++; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.gridheight = 1; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + add(title, gridBagConstraints); + + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = yPosition++; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.gridheight = 1; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + + add(new JSeparator(), gridBagConstraints); + + var helpLabel = new JLabel(command.getHelpHtml()); + helpLabel.setFont(new Font("Arial", Font.PLAIN, 14)); + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = yPosition++; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.gridheight = 3; + yPosition += 2; + + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = yPosition++; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.gridheight = 1; + add(new JSeparator(), gridBagConstraints); + + add(helpLabel, gridBagConstraints); + } + + private Map addArgumentComponents() { + List> arguments = command.defineArguments(); + Map argumentComponents = new HashMap<>(); + + for (Argument argument : arguments) { + argumentComponents.put(argument.getName(), getArgumentComponent(argument)); + } + return argumentComponents; + } + + private JComponent getArgumentComponent(Argument argument) { + if (argument instanceof CoordXArgument intArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JSpinner spinner = createSpinner(intArg); + spinner.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition); + add(spinner, gridBagConstraints); + return spinner; + } else if (argument instanceof CoordYArgument intArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(2, yPosition); + add(label, labelConstraintBag); + JSpinner spinner = createSpinner(intArg); + spinner.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(3, yPosition++); + add(spinner, gridBagConstraints); + return spinner; + } else if (argument instanceof PlayerArgument playerArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JComboBox comboBox = createPlayerComboBox(playerArg); + comboBox.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(comboBox, gridBagConstraints); + return comboBox; + } else if (argument instanceof UnitArgument unitArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JComboBox comboBox = createUnitComboBox(unitArg); + comboBox.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(comboBox, gridBagConstraints); + return comboBox; + } else if (argument instanceof TeamArgument teamArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JComboBox comboBox = createTeamsComboBox(teamArg); + comboBox.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(comboBox, gridBagConstraints); + return comboBox; + } else if (argument instanceof IntegerArgument intArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JSpinner spinner = createSpinner(intArg); + spinner.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(spinner, gridBagConstraints); + return spinner; + } else if (argument instanceof OptionalIntegerArgument intArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JSpinner spinner = createSpinner(intArg); + spinner.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(spinner, gridBagConstraints); + return spinner; + } else if (argument instanceof OptionalEnumArgument enumArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JComboBox comboBox = createOptionalEnumComboBox(enumArg); + comboBox.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(comboBox, gridBagConstraints); + return comboBox; + } else if (argument instanceof EnumArgument enumArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JComboBox comboBox = createEnumComboBox(enumArg); + comboBox.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(comboBox, gridBagConstraints); + return comboBox; + } else if (argument instanceof OptionalPasswordArgument) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JPasswordField passwordField = new JPasswordField(); + passwordField.setToolTipText(argument.getHelp()); + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(passwordField, gridBagConstraints); + return passwordField; + } else if (argument instanceof StringArgument stringArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JTextField textField = new JTextField(); + textField.setToolTipText(argument.getHelp()); + if (stringArg.hasDefaultValue()) { + textField.setText(stringArg.getValue()); + } + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(textField, gridBagConstraints); + return textField; + } else if (argument instanceof OptionalStringArgument stringArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JTextField textField = new JTextField(); + textField.setToolTipText(argument.getHelp()); + if (stringArg.getValue().isPresent()) { + textField.setText(stringArg.getValue().get()); + } + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(textField, gridBagConstraints); + return textField; + } else if (argument instanceof BooleanArgument boolArg) { + JLabel label = new JLabel(argument.getName() + ":"); + var labelConstraintBag = getGridBagConstraints(0, yPosition); + add(label, labelConstraintBag); + JCheckBox checkBox = new JCheckBox(); + checkBox.setToolTipText(argument.getHelp()); + if (boolArg.hasDefaultValue()) { + checkBox.setSelected(boolArg.getValue()); + } + var gridBagConstraints = getGridBagConstraints(1, yPosition++); + add(checkBox, gridBagConstraints); + return checkBox; + } + return null; + } + + private GridBagConstraints getGridBagConstraints(int x, int y) { + var gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = x; + gridBagConstraints.gridy = y; + gridBagConstraints.gridwidth = 1; + gridBagConstraints.gridheight = 1; + gridBagConstraints.fill = GridBagConstraints.NONE; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + return gridBagConstraints; + } + + private JSpinner createSpinner(OptionalIntegerArgument intArg) { + return new JSpinner(new SpinnerNumberModel( + Math.max(intArg.getMinValue(), 0), + intArg.getMinValue(), + intArg.getMaxValue(), + 1)); + } + + private JSpinner createSpinner(CoordXArgument coordX) { + return new JSpinner(new SpinnerNumberModel( + coords.getX()+1, + 0, + 1_000_000, + 1)); + } + + private JSpinner createSpinner(CoordYArgument coordY) { + return new JSpinner(new SpinnerNumberModel( + coords.getY()+1, + 0, + 1_000_000, + 1)); + } + + private JSpinner createSpinner(IntegerArgument intArg) { + return new JSpinner(new SpinnerNumberModel( + intArg.hasDefaultValue() ? intArg.getValue() : 0, + intArg.getMinValue(), + intArg.getMaxValue(), + 1)); + } + + private JComboBox createPlayerComboBox(PlayerArgument playerArgument) { + JComboBox comboBox = new JComboBox<>(); + var players = client.getClient().getGame().getPlayersList(); + for (var player : players) { + comboBox.addItem(player.getId() + ":" + player.getName()); + } + + return comboBox; + } + + private JComboBox createUnitComboBox(UnitArgument unitArgument) { + JComboBox comboBox = new JComboBox<>(); + var entities = client.getClient().getGame().getEntitiesVector(); + for (var entity : entities) { + comboBox.addItem(entity.getId() + ":" + entity.getDisplayName()); + } + + var entitiesAtSpot = client.getClient().getGame().getEntities(coords); + if (entitiesAtSpot.hasNext()) { + var selectedEntity = entitiesAtSpot.next(); + comboBox.setSelectedItem(selectedEntity.getId() + ":" + selectedEntity.getDisplayName()); + } + + return comboBox; + } + + private JComboBox createTeamsComboBox(TeamArgument teamArgument) { + JComboBox comboBox = new JComboBox<>(); + var teams = client.getClient().getGame().getTeams(); + for (var team : teams) { + comboBox.addItem(team.getId() + ""); + } + + return comboBox; + } + + private JComboBox createOptionalEnumComboBox(OptionalEnumArgument enumArg) { + JComboBox comboBox = new JComboBox<>(); + if (enumArg.getValue() == null) { + comboBox.addItem("-"); + comboBox.setSelectedItem("-"); + } + for (var arg : enumArg.getEnumType().getEnumConstants()) { + comboBox.addItem(arg.ordinal() + ": " + arg); + } + if (enumArg.getValue() != null) { + comboBox.setSelectedItem(enumArg.getValue().ordinal() + ": " + enumArg.getValue().toString()); + } + return comboBox; + } + + private JComboBox createEnumComboBox(EnumArgument enumArg) { + JComboBox comboBox = new JComboBox<>(); + for (Enum constant : enumArg.getEnumType().getEnumConstants()) { + comboBox.addItem(constant.name()); + } + if (enumArg.getValue() != null) { + comboBox.setSelectedItem(enumArg.getValue().name()); + } + return comboBox; + } + + private void addExecuteButton(Map argumentComponents) { + var gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = yPosition; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.gridheight = 1; + add(getExecuteButton(argumentComponents), gridBagConstraints); + } + + private JButton getExecuteButton(Map argumentComponents) { + JButton executeButton = new JButton("Execute"); + executeButton.addActionListener(e -> { + int response = JOptionPane.showConfirmDialog( + this, + "Are you sure you want to execute this command?", + "Execute?", + JOptionPane.YES_NO_OPTION + ); + if (response == JOptionPane.YES_OPTION) { + executeCommand(argumentComponents); + } + }); + return executeButton; + } + + /** + * Execute the command with the given arguments. + * It runs the command using the client chat, this way the command is sent to the server. + * All arguments are loaded as named variables in the form of "argumentName=argumentValue". + * + * @param argumentComponents The components that hold the arguments selected. + */ + private void executeCommand(Map argumentComponents) { + List> arguments = command.defineArguments(); + String[] args = new String[arguments.size()]; + + for (int i = 0; i < arguments.size(); i++) { + Argument argument = arguments.get(i); + JComponent component = argumentComponents.get(argument.getName()); + + 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(); + if (selectedItem == null || selectedItem.equals("-")) { + // If it is null we just set it to an empty string and move on + args[i] = ""; + continue; + } + var selectedItemValue = selectedItem.split(":")[0].trim(); + args[i] = argument.getName() + "=" + selectedItemValue; + } else if ( + (argument instanceof PlayerArgument) || + (argument instanceof UnitArgument) || + (argument instanceof TeamArgument)) { + + String selectedItem = (String) ((JComboBox) component).getSelectedItem(); + if (selectedItem == null || selectedItem.equals("-")) { + // If it is null we just set it to an empty string and move on + args[i] = ""; + continue; + } + var selectedItemValue = selectedItem.split(":")[0].trim(); + args[i] = argument.getName() + "=" + selectedItemValue; + } else { + args[i] = argument.getName() + "=" + Objects.requireNonNull(((JComboBox) component).getSelectedItem()); + } + } + } + + client.getClient().sendChat("/" + command.getName() + " " + String.join(" ", args)); + } +} diff --git a/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java b/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java deleted file mode 100644 index d2e4d7fa70a..00000000000 --- a/megamek/src/megamek/client/ui/swing/gmCommands/GamemasterCommandPanel.java +++ /dev/null @@ -1,205 +0,0 @@ -package megamek.client.ui.swing.gmCommands; - -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 javax.swing.*; -import java.awt.*; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Dialog for executing a gamemaster command. - */ -public class GamemasterCommandPanel extends JDialog { - private final GamemasterServerCommand command; - private final ClientGUI client; - private final Coords coords; - - /** - * Constructor for the dialog for executing a gamemaster 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) { - super(parent, command.getName(), true); - this.command = command; - this.client = client; - this.coords = coords; - initializeUI(parent); - } - - private void initializeUI(JFrame parent) { - setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); - addTitleAndDescription(); - Map argumentComponents = addArgumentComponents(); - addExecuteButton(argumentComponents); - pack(); - setLocationRelativeTo(parent); - } - - private void addTitleAndDescription() { - JPanel titlePanel = new JPanel(); - titlePanel.setLayout(new BoxLayout(titlePanel, BoxLayout.Y_AXIS)); - - JLabel titleLabel = new JLabel(command.getLongName()); - titleLabel.setFont(new Font("Arial", Font.BOLD, 16)); - titlePanel.add(titleLabel); - - JLabel helpLabel = new JLabel(command.getHelpHtml()); - helpLabel.setFont(new Font("Arial", Font.PLAIN, 12)); - titlePanel.add(helpLabel); - - add(titlePanel); - } - - private Map addArgumentComponents() { - List> arguments = command.defineArguments(); - Map argumentComponents = new HashMap<>(); - - for (Argument argument : arguments) { - JPanel argumentPanel = createArgumentPanel(argument); - add(argumentPanel); - argumentComponents.put(argument.getName(), getArgumentComponent(argument, argumentPanel)); - } - return argumentComponents; - } - - private JPanel createArgumentPanel(Argument argument) { - JPanel argumentPanel = new JPanel(); - argumentPanel.setLayout(new FlowLayout()); - JLabel label = new JLabel(argument.getName() + ":"); - argumentPanel.add(label); - return argumentPanel; - } - - private JComponent getArgumentComponent(Argument argument, JPanel argumentPanel) { - if (argument instanceof IntegerArgument intArg) { - JSpinner spinner = createSpinner(intArg); - argumentPanel.add(spinner); - return spinner; - } else if (argument instanceof OptionalEnumArgument enumArg) { - JComboBox comboBox = createOptionalEnumComboBox(enumArg); - argumentPanel.add(comboBox); - return comboBox; - } else if (argument instanceof EnumArgument enumArg) { - JComboBox comboBox = createEnumComboBox(enumArg); - argumentPanel.add(comboBox); - return comboBox; - } - return null; - } - - private boolean isArgumentX(Argument argument) { - return argument.getName().equals("x"); - } - - private boolean isArgumentY(Argument argument) { - return argument.getName().equals("y"); - } - - private int getIntArgumentDefaultValue(IntegerArgument intArg) { - return intArg.hasDefaultValue() ? intArg.getValue() : isArgumentX(intArg) ? coords.getX()+1 : - isArgumentY(intArg) ? coords.getY()+1 : 0; - } - - private JSpinner createSpinner(IntegerArgument intArg) { - return new JSpinner(new SpinnerNumberModel( - getIntArgumentDefaultValue(intArg), - intArg.getMinValue(), - intArg.getMaxValue(), - 1)); - } - - private JComboBox createOptionalEnumComboBox(OptionalEnumArgument enumArg) { - JComboBox comboBox = new JComboBox<>(); - if (enumArg.getValue() == null) { - comboBox.addItem("-"); - comboBox.setSelectedItem("-"); - } - for (var arg : enumArg.getEnumType().getEnumConstants()) { - comboBox.addItem(arg.ordinal() + ": " + arg); - } - if (enumArg.getValue() != null) { - comboBox.setSelectedItem(enumArg.getValue().ordinal() + ": " + enumArg.getValue().toString()); - } - return comboBox; - } - - private JComboBox createEnumComboBox(EnumArgument enumArg) { - JComboBox comboBox = new JComboBox<>(); - for (Enum constant : enumArg.getEnumType().getEnumConstants()) { - comboBox.addItem(constant.name()); - } - if (enumArg.getValue() != null) { - comboBox.setSelectedItem(enumArg.getValue().name()); - } - return comboBox; - } - - private void addExecuteButton(Map argumentComponents) { - add(getExecuteButton(argumentComponents)); - } - - private JButton getExecuteButton(Map argumentComponents) { - JButton executeButton = new JButton("Execute Command"); - executeButton.addActionListener(e -> { - int response = JOptionPane.showConfirmDialog( - this, - "Are you sure you want to execute this command?", - "Execute Command", - JOptionPane.YES_NO_OPTION - ); - if (response == JOptionPane.YES_OPTION) { - executeCommand(argumentComponents); - } - }); - return executeButton; - } - - /** - * Execute the command with the given arguments. - * It runs the command using the client chat, this way the command is sent to the server. - * All arguments are loaded as named variables in the form of "argumentName=argumentValue". - * - * @param argumentComponents The components that hold the arguments selected. - */ - private void executeCommand(Map argumentComponents) { - List> arguments = command.defineArguments(); - String[] args = new String[arguments.size()]; - - for (int i = 0; i < arguments.size(); i++) { - Argument argument = arguments.get(i); - JComponent component = argumentComponents.get(argument.getName()); - - if (component instanceof JSpinner) { - args[i] = argument.getName() + "=" + ((JSpinner) component).getValue().toString(); - } else if (component instanceof JComboBox) { - if (argument instanceof OptionalEnumArgument) { - String selectedItem = (String) ((JComboBox) component).getSelectedItem(); - if (selectedItem == null || selectedItem.equals("-")) { - // If it is null we just set it to an empty string and move on - args[i] = ""; - continue; - } - var selectedItemValue = selectedItem.split(":")[0].trim(); - args[i] = argument.getName() + "=" + selectedItemValue; - } else { - args[i] = argument.getName() + "=" + Objects.requireNonNull(((JComboBox) component).getSelectedItem()); - } - } - } - - client.getClient().sendChat("/" + command.getName() + " " + String.join(" ", args)); - } -} diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java index 80b553b9c9f..d0326b98d2a 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; @@ -321,7 +323,7 @@ public void setOptions(final @Nullable GameOptions options) { public void setupTeams() { Vector initTeams = new Vector<>(); boolean useTeamInit = getOptions().getOption(OptionsConstants.BASE_TEAM_INITIATIVE) - .booleanValue(); + .booleanValue(); // Get all NO_TEAM players. If team_initiative is false, all // players are on their own teams for initiative purposes. @@ -428,25 +430,25 @@ private boolean isOffboardPlayable() { // per errata, TAG will spot for LRMs and such if ((ammoType.getAmmoType() == AmmoType.T_LRM) - || (ammoType.getAmmoType() == AmmoType.T_LRM_IMP) - || (ammoType.getAmmoType() == AmmoType.T_MML) - || (ammoType.getAmmoType() == AmmoType.T_NLRM) - || (ammoType.getAmmoType() == AmmoType.T_MEK_MORTAR)) { + || (ammoType.getAmmoType() == AmmoType.T_LRM_IMP) + || (ammoType.getAmmoType() == AmmoType.T_MML) + || (ammoType.getAmmoType() == AmmoType.T_NLRM) + || (ammoType.getAmmoType() == AmmoType.T_MEK_MORTAR)) { return true; } if (((ammoType.getAmmoType() == AmmoType.T_ARROW_IV) - || (ammoType.getAmmoType() == AmmoType.T_LONG_TOM) - || (ammoType.getAmmoType() == AmmoType.T_SNIPER) - || (ammoType.getAmmoType() == AmmoType.T_THUMPER)) - && (ammoType.getMunitionType().contains(AmmoType.Munitions.M_HOMING))) { + || (ammoType.getAmmoType() == AmmoType.T_LONG_TOM) + || (ammoType.getAmmoType() == AmmoType.T_SNIPER) + || (ammoType.getAmmoType() == AmmoType.T_THUMPER)) + && (ammoType.getMunitionType().contains(AmmoType.Munitions.M_HOMING))) { return true; } } if (entity.getBombs().stream().anyMatch(bomb -> !bomb.isDestroyed() - && (bomb.getUsableShotsLeft() > 0) - && (bomb.getType().getBombType() == BombType.B_LG))) { + && (bomb.getUsableShotsLeft() > 0) + && (bomb.getType().getBombType() == BombType.B_LG))) { return true; } } @@ -457,9 +459,9 @@ private boolean isOffboardPlayable() { // aerospace // unit having left the field already, for example return getAttacksVector().stream() - .map(AttackHandler::getWaa) - .filter(Objects::nonNull) - .anyMatch(waa -> waa.getAmmoMunitionType().contains(AmmoType.Munitions.M_HOMING)); + .map(AttackHandler::getWaa) + .filter(Objects::nonNull) + .anyMatch(waa -> waa.getAmmoMunitionType().contains(AmmoType.Munitions.M_HOMING)); } @Override @@ -508,7 +510,7 @@ public int getLiveEntitiesOwnedBy(Player player) { int count = 0; for (Entity entity : inGameTWEntities()) { if (entity.getOwner().equals(player) && !entity.isDestroyed() - && !entity.isCarcass()) { + && !entity.isCarcass()) { count++; } } @@ -524,8 +526,8 @@ public int getLiveDeployedEntitiesOwnedBy(Player player) { int count = 0; for (Entity entity : inGameTWEntities()) { if (entity.getOwner().equals(player) && !entity.isDestroyed() - && !entity.isCarcass() - && !entity.isOffBoard() && !entity.isCaptured()) { + && !entity.isCarcass() + && !entity.isOffBoard() && !entity.isCaptured()) { count++; } } @@ -541,9 +543,9 @@ public int getLiveCommandersOwnedBy(Player player) { int count = 0; for (Entity entity : inGameTWEntities()) { if (entity.getOwner().equals(player) && !entity.isDestroyed() - && !entity.isCarcass() - && entity.isCommander() && !entity.isOffBoard() - && !entity.isCaptured()) { + && !entity.isCarcass() + && entity.isCommander() && !entity.isOffBoard() + && !entity.isCaptured()) { count++; } } @@ -557,8 +559,8 @@ public int getLiveCommandersOwnedBy(Player player) { public boolean hasTacticalGenius(Player player) { for (Entity entity : inGameTWEntities()) { if (entity.hasAbility(OptionsConstants.MISC_TACTICAL_GENIUS) - && entity.getOwner().equals(player) && !entity.isDestroyed() && entity.isDeployed() - && !entity.isCarcass() && !entity.getCrew().isUnconscious()) { + && entity.getOwner().equals(player) && !entity.isDestroyed() && entity.isDeployed() + && !entity.isCarcass() && !entity.getCrew().isUnconscious()) { return true; } } @@ -578,17 +580,17 @@ public List getValidTargets(Entity entity) { // Even if friendly fire is acceptable, do not shoot yourself // Enemy units not on the board can not be shot. if ((otherEntity.getPosition() != null) - && !otherEntity.isOffBoard() - && otherEntity.isTargetable() - && !otherEntity.isHidden() - && !otherEntity.isSensorReturn(entity.getOwner()) - && otherEntity.hasSeenEntity(entity.getOwner()) - && (entity.isEnemyOf(otherEntity) || (friendlyFire && (entity - .getId() != otherEntity.getId())))) { + && !otherEntity.isOffBoard() + && otherEntity.isTargetable() + && !otherEntity.isHidden() + && !otherEntity.isSensorReturn(entity.getOwner()) + && otherEntity.hasSeenEntity(entity.getOwner()) + && (entity.isEnemyOf(otherEntity) || (friendlyFire && (entity + .getId() != otherEntity.getId())))) { // Air to Ground - target must be on flight path if (Compute.isAirToGround(entity, otherEntity)) { if (entity.getPassedThrough().contains( - otherEntity.getPosition())) { + otherEntity.getPosition())) { ents.add(otherEntity); } } else { @@ -925,7 +927,7 @@ public Vector getC3SubNetworkMembers(Entity entity) { // WOR // Handle null, C3i, NC3, and company commander units. if ((entity == null) || entity.hasC3i() || entity.hasNavalC3() || entity.hasActiveNovaCEWS() - || entity.C3MasterIs(entity)) { + || entity.C3MasterIs(entity)) { return getC3NetworkMembers(entity); } @@ -991,7 +993,7 @@ public Enumeration getGraveyardEntities() { for (Entity entity : vOutOfGame) { if ((entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_SALVAGEABLE) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_EJECTED)) { + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_EJECTED)) { graveyard.addElement(entity); } } @@ -1006,8 +1008,8 @@ public Enumeration getWreckedEntities() { Vector wrecks = new Vector<>(); for (Entity entity : vOutOfGame) { if ((entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_SALVAGEABLE) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_EJECTED) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED)) { + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_EJECTED) + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_DEVASTATED)) { wrecks.addElement(entity); } } @@ -1024,8 +1026,8 @@ public Enumeration getRetreatedEntities() { for (Entity entity : vOutOfGame) { if ((entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_IN_RETREAT) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_CAPTURED) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_PUSHED)) { + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_CAPTURED) + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_PUSHED)) { sanctuary.addElement(entity); } } @@ -1119,6 +1121,22 @@ public int getNoOfEntities() { return (possibleEntity instanceof Entity) ? (Entity) possibleEntity : null; } + /** + * When it has to exist, the entity HAS to exists. If it doesn't, throw a no such element exception. + * This is to be used in place of the previous getEntity method when the entity is expected to exist and the following actions + * will cause a null pointer exception if the entity does not exist. + * @param id The id number of the entity to get. + * @return The entity with the given id number or throw a no such element exception. + */ + public Entity getEntityOrThrow(final int id) { + InGameObject possibleEntity = inGameObjects.get(id); + var entity = (possibleEntity instanceof Entity) ? (Entity) possibleEntity : null; + if (entity == null) { + throw new NoSuchElementException("No value present"); + } + return entity; + } + /** * looks for an entity by id number even if out of the game */ @@ -1195,7 +1213,7 @@ public synchronized void addEntity(Entity entity, boolean genEvent) { if ((entity instanceof Mek) && !entity.isOmni() && !entity.hasBattleArmorHandles()) { entity.addTransporter(new ClampMountMek()); } else if ((entity instanceof Tank) && !entity.isOmni() - && !entity.hasBattleArmorHandles()) { + && !entity.hasBattleArmorHandles()) { entity.addTransporter(new ClampMountTank()); } @@ -1309,7 +1327,7 @@ public synchronized void removeEntity(int id, int condition) { // do not keep never-joined entities if ((vOutOfGame != null) - && (condition != IEntityRemovalConditions.REMOVE_NEVER_JOINED)) { + && (condition != IEntityRemovalConditions.REMOVE_NEVER_JOINED)) { vOutOfGame.addElement(toRemove); } @@ -1410,7 +1428,7 @@ public Entity getFirstEntity(Coords c) { public Entity getFirstEnemyEntity(Coords c, Entity currentEntity) { for (Entity entity : inGameTWEntities()) { if (c.equals(entity.getPosition()) && entity.isTargetable() - && entity.isEnemyOf(currentEntity)) { + && entity.isEnemyOf(currentEntity)) { return entity; } } @@ -1560,10 +1578,10 @@ public boolean hasRooftopGunEmplacement(Coords c) { Hex hex = getBoard().getHex(c); for (Entity entity : getEntitiesVector(c)) { if (entity.isTargetable() - && ((entity.getElevation() == 0) // Standing on hex surface - || (entity.getElevation() == -hex.depth())) // Standing on hex floor - && (entity.getAltitude() == 0) - && !(entity instanceof Infantry) && (entity != ignore)) { + && ((entity.getElevation() == 0) // Standing on hex surface + || (entity.getElevation() == -hex.depth())) // Standing on hex floor + && (entity.getAltitude() == 0) + && !(entity instanceof Infantry) && (entity != ignore)) { vector.addElement(entity); } } @@ -1589,7 +1607,7 @@ public boolean hasRooftopGunEmplacement(Coords c) { */ public Iterator getEnemyEntities(final Coords coords, final Entity currentEntity) { return getSelectedEntities(entity -> coords.equals(entity.getPosition()) - && entity.isTargetable() && entity.isEnemyOf(currentEntity)); + && entity.isTargetable() && entity.isEnemyOf(currentEntity)); } /** @@ -1620,7 +1638,7 @@ public Iterator getTeamEntities(final Team team) { */ public Iterator getFriendlyEntities(final Coords coords, final Entity currentEntity) { return getSelectedEntities(entity -> coords.equals(entity.getPosition()) - && entity.isTargetable() && !entity.isEnemyOf(currentEntity)); + && entity.isTargetable() && !entity.isEnemyOf(currentEntity)); } /** @@ -1911,9 +1929,9 @@ public ArrayList getPlayerRetreatedEntities(Player player) { ArrayList output = new ArrayList<>(); for (Entity entity : vOutOfGame) { if (player.equals(entity.getOwner()) && - ((entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_IN_RETREAT) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_CAPTURED) - || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_PUSHED))) { + ((entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_IN_RETREAT) + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_CAPTURED) + || (entity.getRemovalCondition() == IEntityRemovalConditions.REMOVE_PUSHED))) { output.add(entity); } } @@ -1966,7 +1984,7 @@ public int getInfantryLeft(int playerId) { for (Entity entity : inGameTWEntities()) { if (player.equals(entity.getOwner()) && entity.isSelectableThisTurn() - && (entity instanceof Infantry)) { + && (entity instanceof Infantry)) { remaining++; } } @@ -1985,7 +2003,7 @@ public int getProtoMeksLeft(int playerId) { for (Entity entity : inGameTWEntities()) { if (player.equals(entity.getOwner()) && entity.isSelectableThisTurn() - && (entity instanceof ProtoMek)) { + && (entity instanceof ProtoMek)) { remaining++; } } @@ -2004,7 +2022,7 @@ public int getVehiclesLeft(int playerId) { for (Entity entity : inGameTWEntities()) { if (player.equals(entity.getOwner()) && entity.isSelectableThisTurn() - && (entity instanceof Tank)) { + && (entity instanceof Tank)) { remaining++; } } @@ -2022,7 +2040,7 @@ public int getMeksLeft(int playerId) { for (Entity entity : inGameTWEntities()) { if (player.equals(entity.getOwner()) && entity.isSelectableThisTurn() - && (entity instanceof Mek)) { + && (entity instanceof Mek)) { remaining++; } } @@ -2064,9 +2082,9 @@ public void removeTurnFor(Entity entity) { // A turn only needs to be removed when going from 4 inf (2 turns) to // 3 inf (1 turn) if (getOptions().booleanOption(OptionsConstants.INIT_INF_MOVE_MULTI) - && (entity instanceof Infantry) && getPhase().isMovement()) { + && (entity instanceof Infantry) && getPhase().isMovement()) { if ((getInfantryLeft(entity.getOwnerId()) % getOptions().intOption( - OptionsConstants.INIT_INF_PROTO_MOVE_MULTI)) != 1) { + OptionsConstants.INIT_INF_PROTO_MOVE_MULTI)) != 1) { // exception, if the _next_ turn is an infantry turn, remove that // contrived, but may come up e.g. one inf accidentally kills another synchronized (turnVector) { @@ -2075,7 +2093,7 @@ public void removeTurnFor(Entity entity) { if (nextTurn instanceof EntityClassTurn) { EntityClassTurn ect = (EntityClassTurn) nextTurn; if (ect.isValidClass(EntityClassTurn.CLASS_INFANTRY) - && !ect.isValidClass(~EntityClassTurn.CLASS_INFANTRY)) { + && !ect.isValidClass(~EntityClassTurn.CLASS_INFANTRY)) { turnVector.removeElementAt(turnIndex + 1); } } @@ -2086,9 +2104,9 @@ public void removeTurnFor(Entity entity) { } // Same thing but for ProtoMeks if (getOptions().booleanOption(OptionsConstants.INIT_PROTOS_MOVE_MULTI) - && (entity instanceof ProtoMek) && getPhase().isMovement()) { + && (entity instanceof ProtoMek) && getPhase().isMovement()) { if ((getProtoMeksLeft(entity.getOwnerId()) % getOptions() - .intOption(OptionsConstants.INIT_INF_PROTO_MOVE_MULTI)) != 1) { + .intOption(OptionsConstants.INIT_INF_PROTO_MOVE_MULTI)) != 1) { // exception, if the _next_ turn is an ProtoMek turn, remove that // contrived, but may come up e.g. one inf accidentally kills another synchronized (turnVector) { @@ -2097,7 +2115,7 @@ public void removeTurnFor(Entity entity) { if (nextTurn instanceof EntityClassTurn) { EntityClassTurn ect = (EntityClassTurn) nextTurn; if (ect.isValidClass(EntityClassTurn.CLASS_PROTOMEK) - && !ect.isValidClass(~EntityClassTurn.CLASS_PROTOMEK)) { + && !ect.isValidClass(~EntityClassTurn.CLASS_PROTOMEK)) { turnVector.removeElementAt(turnIndex + 1); } } @@ -2109,9 +2127,9 @@ public void removeTurnFor(Entity entity) { // Same thing but for vehicles if (getOptions().booleanOption(OptionsConstants.ADVGRNDMOV_VEHICLE_LANCE_MOVEMENT) - && (entity instanceof Tank) && getPhase().isMovement()) { + && (entity instanceof Tank) && getPhase().isMovement()) { if ((getVehiclesLeft(entity.getOwnerId()) % getOptions() - .intOption(OptionsConstants.ADVGRNDMOV_VEHICLE_LANCE_MOVEMENT_NUMBER)) != 1) { + .intOption(OptionsConstants.ADVGRNDMOV_VEHICLE_LANCE_MOVEMENT_NUMBER)) != 1) { // exception, if the _next_ turn is a tank turn, remove that // contrived, but may come up e.g. one tank accidentally kills another synchronized (turnVector) { @@ -2120,7 +2138,7 @@ public void removeTurnFor(Entity entity) { if (nextTurn instanceof EntityClassTurn) { EntityClassTurn ect = (EntityClassTurn) nextTurn; if (ect.isValidClass(EntityClassTurn.CLASS_TANK) - && !ect.isValidClass(~EntityClassTurn.CLASS_TANK)) { + && !ect.isValidClass(~EntityClassTurn.CLASS_TANK)) { turnVector.removeElementAt(turnIndex + 1); } } @@ -2132,9 +2150,9 @@ public void removeTurnFor(Entity entity) { // Same thing but for meks if (getOptions().booleanOption(OptionsConstants.ADVGRNDMOV_MEK_LANCE_MOVEMENT) - && (entity instanceof Mek) && getPhase().isMovement()) { + && (entity instanceof Mek) && getPhase().isMovement()) { if ((getMeksLeft(entity.getOwnerId()) % getOptions() - .intOption(OptionsConstants.ADVGRNDMOV_MEK_LANCE_MOVEMENT_NUMBER)) != 1) { + .intOption(OptionsConstants.ADVGRNDMOV_MEK_LANCE_MOVEMENT_NUMBER)) != 1) { // exception, if the _next_ turn is a mek turn, remove that // contrived, but may come up e.g. one mek accidentally kills another synchronized (turnVector) { @@ -2143,7 +2161,7 @@ public void removeTurnFor(Entity entity) { if (nextTurn instanceof EntityClassTurn) { EntityClassTurn ect = (EntityClassTurn) nextTurn; if (ect.isValidClass(EntityClassTurn.CLASS_MEK) - && !ect.isValidClass(~EntityClassTurn.CLASS_MEK)) { + && !ect.isValidClass(~EntityClassTurn.CLASS_MEK)) { turnVector.removeElementAt(turnIndex + 1); } } @@ -2159,9 +2177,9 @@ public void removeTurnFor(Entity entity) { // considered invalid unless we don't consider the extra validity // checks. if ((getOptions().booleanOption(OptionsConstants.INIT_INF_MOVE_LATER) && - (entity instanceof Infantry)) || - (getOptions().booleanOption(OptionsConstants.INIT_PROTOS_MOVE_LATER) && - (entity instanceof ProtoMek))) { + (entity instanceof Infantry)) || + (getOptions().booleanOption(OptionsConstants.INIT_PROTOS_MOVE_LATER) && + (entity instanceof ProtoMek))) { useInfantryMoveLaterCheck = false; } @@ -2266,7 +2284,7 @@ public void rollInitAndResolveTies() { TurnOrdered.rollInitAndResolveTies(getEntitiesVector(), vRerolls, false); } else { TurnOrdered.rollInitAndResolveTies(teams, initiativeRerollRequests, - getOptions().booleanOption(OptionsConstants.INIT_INITIATIVE_STREAK_COMPENSATION)); + getOptions().booleanOption(OptionsConstants.INIT_INITIATIVE_STREAK_COMPENSATION)); } initiativeRerollRequests.removeAllElements(); @@ -2275,7 +2293,7 @@ public void rollInitAndResolveTies() { public void handleInitiativeCompensation() { if (getOptions().booleanOption(OptionsConstants.INIT_INITIATIVE_STREAK_COMPENSATION)) { TurnOrdered.resetInitiativeCompensation(teams, - getOptions().booleanOption(OptionsConstants.INIT_INITIATIVE_STREAK_COMPENSATION)); + getOptions().booleanOption(OptionsConstants.INIT_INITIATIVE_STREAK_COMPENSATION)); } } @@ -2528,13 +2546,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 +2557,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 +2584,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. * @@ -2790,7 +2836,7 @@ public int getSelectedEntityCount(EntitySelector selector) { * empty. */ public Enumeration getSelectedOutOfGameEntities( - EntitySelector selector) { + EntitySelector selector) { Enumeration retVal; // If no selector was supplied, return all entities. @@ -2890,15 +2936,15 @@ public int getSelectedOutOfGameEntityCount(EntitySelector selector) { */ public boolean checkForValidNonInfantryAndOrProtoMeks(int playerId) { Iterator iter = getPlayerEntities(getPlayer(playerId), false) - .iterator(); + .iterator(); while (iter.hasNext()) { Entity entity = iter.next(); boolean excluded = false; if ((entity instanceof Infantry) - && getOptions().booleanOption(OptionsConstants.INIT_INF_MOVE_LATER)) { + && getOptions().booleanOption(OptionsConstants.INIT_INF_MOVE_LATER)) { excluded = true; } else if ((entity instanceof ProtoMek) - && getOptions().booleanOption(OptionsConstants.INIT_PROTOS_MOVE_LATER)) { + && getOptions().booleanOption(OptionsConstants.INIT_PROTOS_MOVE_LATER)) { excluded = true; } @@ -2927,7 +2973,7 @@ public Enumeration getNemesisTargets(Entity attacker, Coords target) { for (Coords c : in) { for (Entity entity : getEntitiesVector(c)) { if (entity.isINarcedWith(INarcPod.NEMESIS) - && !entity.isEnemyOf(attacker)) { + && !entity.isEnemyOf(attacker)) { nemesisTargets.addElement(entity); } } @@ -3047,7 +3093,7 @@ public Vector ageFlares() { if (!planetaryConditions.getWind().isCalm()) { WindDirection dir = planetaryConditions.getWindDirection(); flare.position = flare.position.translated(dir.ordinal(), - (wind.ordinal() > 1) ? (wind.ordinal() - 1) : wind.ordinal()); + (wind.ordinal() > 1) ? (wind.ordinal() - 1) : wind.ordinal()); if (getBoard().contains(flare.position)) { r = new Report(5236); r.add(flare.position.getBoardNum()); @@ -3084,7 +3130,7 @@ public Vector ageFlares() { public boolean gameTimerIsExpired() { return getOptions().booleanOption(OptionsConstants.VICTORY_USE_GAME_TURN_LIMIT) - && (getRoundCount() == getOptions().intOption(OptionsConstants.VICTORY_GAME_TURN_LIMIT)); + && (getRoundCount() == getOptions().intOption(OptionsConstants.VICTORY_GAME_TURN_LIMIT)); } /** @@ -3111,7 +3157,7 @@ public VictoryResult getVictoryResult() { // applicable public boolean useVectorMove() { return getOptions().booleanOption(OptionsConstants.ADVAERORULES_ADVANCED_MOVEMENT) - && getBoard().inSpace(); + && getBoard().inSpace(); } /** @@ -3163,11 +3209,11 @@ public void resetControlRolls() { */ public boolean checkForValidSpaceStations(int playerId) { Iterator iter = getPlayerEntities(getPlayer(playerId), false) - .iterator(); + .iterator(); while (iter.hasNext()) { Entity entity = iter.next(); if ((entity instanceof SpaceStation) - && getTurn().isValidEntity(entity, this)) { + && getTurn().isValidEntity(entity, this)) { return true; } } @@ -3179,7 +3225,7 @@ public boolean checkForValidDropShips(int playerId) { while (iter.hasNext()) { Entity entity = iter.next(); if ((entity instanceof Dropship) - && getTurn().isValidEntity(entity, this)) { + && getTurn().isValidEntity(entity, this)) { return true; } } @@ -3188,7 +3234,7 @@ && getTurn().isValidEntity(entity, this)) { public boolean checkForValidSmallCraft(int playerId) { return getPlayerEntities(getPlayer(playerId), false).stream() - .anyMatch(e -> (e instanceof SmallCraft) && getTurn().isValidEntity(e, this)); + .anyMatch(e -> (e instanceof SmallCraft) && getTurn().isValidEntity(e, this)); } @Override @@ -3235,7 +3281,7 @@ public void removeCompletelyDissipatedSmokeClouds() { * @param e */ public synchronized void updateEntityPositionLookup(Entity e, - HashSet oldPositions) { + HashSet oldPositions) { HashSet newPositions = e.getOccupiedCoords(); // Check to see that the position has actually changed if (newPositions.equals(oldPositions)) { @@ -3313,10 +3359,10 @@ private void checkPositionCacheConsistency() { Collections.sort(entitiesInCache); Collections.sort(entitiesInVector); if ((entitiesInCacheCount != entityVectorSize) && !getPhase().isDeployment() - && !getPhase().isExchange() && !getPhase().isLounge() - && !getPhase().isInitiativeReport() && !getPhase().isInitiative()) { + && !getPhase().isExchange() && !getPhase().isLounge() + && !getPhase().isInitiativeReport() && !getPhase().isInitiative()) { logger.warn("Entities vector has " + inGameTWEntities().size() - + " but pos lookup cache has " + entitiesInCache.size() + "entities!"); + + " but pos lookup cache has " + entitiesInCache.size() + "entities!"); List missingIds = new ArrayList<>(); for (Integer id : entitiesInVector) { if (!entitiesInCache.contains(id)) { @@ -3331,8 +3377,8 @@ private void checkPositionCacheConsistency() { HashSet ents = entityPosLookup.get(c); if ((ents != null) && !ents.contains(e.getId())) { logger.warn("Entity " + e.getId() + " is in " - + e.getPosition() + " however the position cache " - + "does not have it in that position!"); + + e.getPosition() + " however the position cache " + + "does not have it in that position!"); } } } @@ -3345,7 +3391,7 @@ private void checkPositionCacheConsistency() { HashSet positions = e.getOccupiedCoords(); if (!positions.contains(c)) { logger.warn("Entity Position Cache thinks Entity " + eId - + "is in " + c + " but the Entity thinks it's in " + e.getPosition()); + + "is in " + c + " but the Entity thinks it's in " + e.getPosition()); } } } diff --git a/megamek/src/megamek/common/actions/NukeDetonatedAction.java b/megamek/src/megamek/common/actions/NukeDetonatedAction.java new file mode 100644 index 00000000000..e22eea8d565 --- /dev/null +++ b/megamek/src/megamek/common/actions/NukeDetonatedAction.java @@ -0,0 +1,41 @@ +/* + * MegaMek - Copyright (C) 2024 - 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 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.common.actions; + +import megamek.common.AmmoType; + +import java.io.Serial; + +public class NukeDetonatedAction extends AbstractEntityAction { + + @Serial + private static final long serialVersionUID = 918785269096319255L; + + private final AmmoType.Munitions nukeType; + private final int playerID; + + public NukeDetonatedAction(int entityId, int playerID, AmmoType.Munitions nukeType) { + super(entityId); + this.nukeType = nukeType; + this.playerID = playerID; + } + + public AmmoType.Munitions typeOfNuke() { + return nukeType; + } + + public int getPlayerID() { + return playerID; + } +} diff --git a/megamek/src/megamek/common/event/GameListener.java b/megamek/src/megamek/common/event/GameListener.java index cc33f7ba769..00c2058f8ab 100644 --- a/megamek/src/megamek/common/event/GameListener.java +++ b/megamek/src/megamek/common/event/GameListener.java @@ -73,4 +73,6 @@ default void gameScriptedEvent(GameScriptedEvent event) { } * @param event */ default void gameUnitChange(GameEvent event) { } + + default void gamePlayerStrategicAction(GamePlayerStrategicActionEvent e) { } } diff --git a/megamek/src/megamek/common/event/GamePlayerStrategicActionEvent.java b/megamek/src/megamek/common/event/GamePlayerStrategicActionEvent.java new file mode 100644 index 00000000000..0857bab5514 --- /dev/null +++ b/megamek/src/megamek/common/event/GamePlayerStrategicActionEvent.java @@ -0,0 +1,58 @@ +/* + * MegaMek - Copyright (C) 2024 - 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 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.common.event; + +import megamek.common.actions.EntityAction; + +import java.io.Serial; + +/** + * Instances of this class are sent when an strategic action is created in the game + * + * @see GameListener + */ +public class GamePlayerStrategicActionEvent extends GameEvent { + + @Serial + private static final long serialVersionUID = 928848699583079097L; + protected EntityAction action; + + /** + * Construct new GameNewActionEvent + * + * @param source sender + * @param action + */ + public GamePlayerStrategicActionEvent(Object source, EntityAction action) { + super(source); + this.action = action; + } + + /** + * @return the action. + */ + public EntityAction getAction() { + return action; + } + + @Override + public void fireEvent(GameListener gl) { + gl.gamePlayerStrategicAction(this); + } + + @Override + public String getEventName() { + return "Game New Action"; + } +} diff --git a/megamek/src/megamek/common/weapons/ArtilleryBayWeaponIndirectFireHandler.java b/megamek/src/megamek/common/weapons/ArtilleryBayWeaponIndirectFireHandler.java index d90e6b4558f..4ee253433f6 100644 --- a/megamek/src/megamek/common/weapons/ArtilleryBayWeaponIndirectFireHandler.java +++ b/megamek/src/megamek/common/weapons/ArtilleryBayWeaponIndirectFireHandler.java @@ -21,10 +21,12 @@ import megamek.common.*; import megamek.common.actions.ArtilleryAttackAction; +import megamek.common.actions.NukeDetonatedAction; import megamek.common.actions.WeaponAttackAction; import megamek.common.enums.GamePhase; import megamek.common.equipment.AmmoMounted; import megamek.common.equipment.WeaponMounted; +import megamek.common.event.GamePlayerStrategicActionEvent; import megamek.common.options.OptionsConstants; import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; @@ -417,12 +419,18 @@ public boolean accept(Entity entity) { if (!bMissed) { // Keep blasting the target hex with each weapon in the bay that fired while (nweaponsHit > 0) { + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(ae.getId(), ae.getOwnerId(), AmmoType.Munitions.M_DAVY_CROCKETT_M))); gameManager.doNuclearExplosion(targetPos, 1, vPhaseReport); nweaponsHit--; } } else { // Deliver a round to each target hex for (Coords c : targets) { + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(ae.getId(), ae.getOwnerId(), AmmoType.Munitions.M_DAVY_CROCKETT_M))); gameManager.doNuclearExplosion(c, 1, vPhaseReport); } } diff --git a/megamek/src/megamek/common/weapons/ArtilleryCannonWeaponHandler.java b/megamek/src/megamek/common/weapons/ArtilleryCannonWeaponHandler.java index f020e3811d6..3f9581307c2 100644 --- a/megamek/src/megamek/common/weapons/ArtilleryCannonWeaponHandler.java +++ b/megamek/src/megamek/common/weapons/ArtilleryCannonWeaponHandler.java @@ -17,8 +17,10 @@ import java.util.Vector; import megamek.common.*; +import megamek.common.actions.NukeDetonatedAction; import megamek.common.actions.WeaponAttackAction; import megamek.common.enums.GamePhase; +import megamek.common.event.GamePlayerStrategicActionEvent; import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; @@ -169,6 +171,10 @@ public boolean handle(GamePhase phase, Vector vPhaseReport) { return false; } else if (ammoType.getMunitionType().contains(AmmoType.Munitions.M_DAVY_CROCKETT_M)) { // The appropriate term here is "Bwahahahahaha..." + gameManager.drawNukeHitOnBoard(targetPos); + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(ae.getId(), ae.getOwnerId(), AmmoType.Munitions.M_DAVY_CROCKETT_M))); gameManager.doNuclearExplosion(targetPos, 1, vPhaseReport); return false; } else if (ammoType.getMunitionType().contains(AmmoType.Munitions.M_FASCAM)) { diff --git a/megamek/src/megamek/common/weapons/ArtilleryWeaponIndirectFireHandler.java b/megamek/src/megamek/common/weapons/ArtilleryWeaponIndirectFireHandler.java index e8e2c29d5f1..808a80e177d 100644 --- a/megamek/src/megamek/common/weapons/ArtilleryWeaponIndirectFireHandler.java +++ b/megamek/src/megamek/common/weapons/ArtilleryWeaponIndirectFireHandler.java @@ -20,8 +20,10 @@ import megamek.common.AmmoType.Munitions; import megamek.common.SpecialHexDisplay.Type; import megamek.common.actions.ArtilleryAttackAction; +import megamek.common.actions.NukeDetonatedAction; import megamek.common.actions.WeaponAttackAction; import megamek.common.enums.GamePhase; +import megamek.common.event.GamePlayerStrategicActionEvent; import megamek.common.options.OptionsConstants; import megamek.common.weapons.AreaEffectHelper.DamageFalloff; import megamek.common.weapons.capitalweapons.CapitalMissileWeapon; @@ -336,9 +338,16 @@ else if ((null != bestSpotter) && !(this instanceof ArtilleryWeaponDirectFireHan if (atype.getMunitionType().contains(Munitions.M_DAVY_CROCKETT_M)) { // The appropriate term here is "Bwahahahahaha..." if (target.isOffBoard()) { + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(ae.getId(), ae.getOwnerId(), AmmoType.Munitions.M_DAVY_CROCKETT_M))); AreaEffectHelper.doNuclearExplosion((Entity) aaa.getTarget(game), finalPos, 1, vPhaseReport, gameManager); } else { + gameManager.drawNukeHitOnBoard(targetPos); + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(ae.getId(), ae.getOwnerId(), AmmoType.Munitions.M_DAVY_CROCKETT_M))); gameManager.doNuclearExplosion(finalPos, 1, vPhaseReport); } return false; diff --git a/megamek/src/megamek/server/IGameManager.java b/megamek/src/megamek/server/IGameManager.java index b55801f5bd9..a300468a628 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); + + List getCommandList(Server server); void addReport(ReportEntry r); @@ -147,4 +149,10 @@ default void saveGame(String fileName) { */ void calculatePlayerInitialCounts(); + /** + * Requests a team change for a player. + * @param teamID ID of the team the player will be passed to + * @param player player + */ + void requestTeamChangeForPlayer(int teamID, Player player); } diff --git a/megamek/src/megamek/server/Server.java b/megamek/src/megamek/server/Server.java index cc51549c8c2..603410b1786 100644 --- a/megamek/src/megamek/server/Server.java +++ b/megamek/src/megamek/server/Server.java @@ -1183,10 +1183,26 @@ 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. + */ + @Deprecated 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..9ce651baf8e 100644 --- a/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java +++ b/megamek/src/megamek/server/commands/ChangeOwnershipCommand.java @@ -17,8 +17,7 @@ import megamek.common.Entity; import megamek.common.Player; import megamek.server.Server; -import megamek.server.commands.arguments.Argument; -import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.commands.arguments.*; import megamek.server.totalwarfare.TWGameManager; import java.util.List; @@ -45,14 +44,14 @@ public ChangeOwnershipCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { return List.of( - new IntegerArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.changeownership.unitID")), - new IntegerArgument(PLAYER_ID, Messages.getString("Gamemaster.cmd.changeownership.playerID"))); + new UnitArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.changeownership.unitID")), + new PlayerArgument(PLAYER_ID, Messages.getString("Gamemaster.cmd.changeownership.playerID"))); } @Override - protected void runAsGM(int connId, Map> args) { - IntegerArgument unitID = (IntegerArgument) args.get(UNIT_ID); - IntegerArgument playerID = (IntegerArgument) args.get(PLAYER_ID); + protected void runCommand(int connId, Arguments args) { + var unitID = (UnitArgument) args.get(UNIT_ID); + var playerID = (PlayerArgument) args.get(PLAYER_ID); Entity ent = gameManager.getGame().getEntity(unitID.getValue()); Player player = server.getGame().getPlayer(playerID.getValue()); diff --git a/megamek/src/megamek/server/commands/ChangeTeamCommand.java b/megamek/src/megamek/server/commands/ChangeTeamCommand.java new file mode 100644 index 00000000000..dea078fb0ed --- /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.Player; +import megamek.server.Server; +import megamek.server.commands.arguments.Argument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.commands.arguments.PlayerArgument; +import megamek.server.commands.arguments.TeamArgument; +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 PlayerArgument(PLAYER_ID, Messages.getString("Gamemaster.cmd.changeteam.playerID")), + new TeamArgument(TEAM_ID, Messages.getString("Gamemaster.cmd.changeteam.teamID"))); + } + + @Override + protected void runCommand(int connId, Arguments args) { + int playerID = ((PlayerArgument) args.get(PLAYER_ID)).getValue(); + int teamID = ((TeamArgument) args.get(TEAM_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..6e278ef2ccd --- /dev/null +++ b/megamek/src/megamek/server/commands/ClientServerCommand.java @@ -0,0 +1,228 @@ +/* + * 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; + } + + protected boolean isGM(int connId) { + return server.getGameManager().getGame().getPlayer(connId).getGameMaster(); + } + + 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(); + } + + protected boolean isOutsideOfBoard(int connId, Arguments args) { + if (!(args.hasArg("x") && args.hasArg("y"))) { + // There is nothing to check, s out of excess of caution we return false. + return true; + } + + // is the hex on the board? + if (!gameManager.getGame().getBoard().contains(((int) args.get("x").getValue()) - 1 , ((int) args.get("y").getValue()) - 1)) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.error.outofbounds")); + return true; + } + return false; + } + + + // 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..e3c2479c327 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; @@ -130,7 +131,6 @@ private void runDisasterCommand(int connId, Disaster disaster) { server.sendServerChat("The star is going supernova!"); server.sendServerChat("Everything is on fire! We are doomed!"); break; - case SANDSTORM: new ChangeWeatherCommand(server, gameManager).run(connId, new String[]{"weather", "blowsand=1", "wind=4", "winddir=6"}); server.sendServerChat("A sandstorm is approaching!"); @@ -188,7 +188,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..d4dd307c61b --- /dev/null +++ b/megamek/src/megamek/server/commands/EndGameCommand.java @@ -0,0 +1,62 @@ +/* + * 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.server.Server; +import megamek.server.commands.arguments.*; +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 PlayerArgument(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 = ((PlayerArgument) 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..50c830ab629 100644 --- a/megamek/src/megamek/server/commands/FirefightCommand.java +++ b/megamek/src/megamek/server/commands/FirefightCommand.java @@ -18,11 +18,12 @@ import megamek.common.Hex; import megamek.server.Server; import megamek.server.commands.arguments.Argument; -import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.commands.arguments.Arguments; +import megamek.server.commands.arguments.CoordXArgument; +import megamek.server.commands.arguments.CoordYArgument; import megamek.server.totalwarfare.TWGameManager; import java.util.List; -import java.util.Map; import java.util.Objects; /** @@ -35,7 +36,6 @@ public class FirefightCommand extends GamemasterServerCommand { private static final String FIRESTARTER = "firefight"; private static final String X = "x"; private static final String Y = "y"; - private static final String TYPE = "type"; public FirefightCommand(Server server, TWGameManager gameManager) { super(server, @@ -48,8 +48,8 @@ public FirefightCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { return List.of( - new IntegerArgument(X, Messages.getString("Gamemaster.cmd.x")), - new IntegerArgument(Y, Messages.getString("Gamemaster.cmd.y")) + new CoordXArgument(X, Messages.getString("Gamemaster.cmd.x")), + new CoordYArgument(Y, Messages.getString("Gamemaster.cmd.y")) ); } @@ -59,7 +59,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..8d860cdfceb 100644 --- a/megamek/src/megamek/server/commands/FirestarterCommand.java +++ b/megamek/src/megamek/server/commands/FirestarterCommand.java @@ -17,8 +17,7 @@ import megamek.common.Coords; import megamek.common.Hex; import megamek.server.Server; -import megamek.server.commands.arguments.Argument; -import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.commands.arguments.*; import megamek.server.totalwarfare.TWGameManager; import java.util.List; @@ -48,8 +47,8 @@ public FirestarterCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { return List.of( - new IntegerArgument(X, Messages.getString("Gamemaster.cmd.x")), - new IntegerArgument(Y, Messages.getString("Gamemaster.cmd.y")), + new CoordXArgument(X, Messages.getString("Gamemaster.cmd.x")), + new CoordYArgument(Y, Messages.getString("Gamemaster.cmd.y")), new IntegerArgument(TYPE, Messages.getString("Gamemaster.cmd.fire.type"), 1, 4, 1)); } @@ -59,7 +58,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..2bdfc58f1e9 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,20 @@ 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; - } - - 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, "
")+ - ""; + super(server, gameManager, name, helpText, longName); } @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..9c9850420b9 100644 --- a/megamek/src/megamek/server/commands/JoinTeamCommand.java +++ b/megamek/src/megamek/server/commands/JoinTeamCommand.java @@ -27,18 +27,22 @@ * 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 " - + "must allow this change. Use /allowTeamChange " - + "to allow this change."; + + "must allow this change. Use /allowTeamChange " + + "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."); } /** @@ -54,8 +58,8 @@ public void run(int connId, String[] args) { if (args.length != 2) { server.sendServerChat(connId, "Incorrect number of arguments " - + "for joinTeam command! Expected 1, received, " - + (args.length - 1) + "."); + + "for joinTeam command! Expected 1, received, " + + (args.length - 1) + "."); server.sendServerChat(connId, getHelp()); return; } @@ -64,7 +68,7 @@ public void run(int connId, String[] args) { if ((Player.TEAM_UNASSIGNED == teamId) && (numEntities != 0)) { server.sendServerChat(connId, "Player must have no more " + - "units to join the unassigned team!"); + "units to join the unassigned team!"); return; } String teamString = "join Team " + teamId + ". "; @@ -77,8 +81,8 @@ public void run(int connId, String[] args) { for (Player p : server.getGame().getPlayersList()) { if (p.getId() != player.getId()) { server.sendServerChat(p.getId(), player.getName() - + " wants to " + teamString - + SERVER_VOTE_PROMPT_MSG); + + " wants to " + teamString + + SERVER_VOTE_PROMPT_MSG); } } diff --git a/megamek/src/megamek/server/commands/KickCommand.java b/megamek/src/megamek/server/commands/KickCommand.java index 8838955d55a..66ed671d283 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..af84148780d 100644 --- a/megamek/src/megamek/server/commands/KillCommand.java +++ b/megamek/src/megamek/server/commands/KillCommand.java @@ -16,7 +16,9 @@ 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.commands.arguments.UnitArgument; import megamek.server.totalwarfare.TWGameManager; import java.util.List; @@ -37,14 +39,14 @@ public KillCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { - return List.of(new IntegerArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.kill.unitID"))); + return List.of(new UnitArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.kill.unitID"))); } /** * 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/NuclearStrikeCommand.java b/megamek/src/megamek/server/commands/NuclearStrikeCommand.java new file mode 100644 index 00000000000..e572df5dc48 --- /dev/null +++ b/megamek/src/megamek/server/commands/NuclearStrikeCommand.java @@ -0,0 +1,94 @@ +/* + * 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.AmmoType; +import megamek.common.Entity; +import megamek.common.Player; +import megamek.common.actions.NukeDetonatedAction; +import megamek.common.event.GamePlayerStrategicActionEvent; +import megamek.common.options.OptionsConstants; +import megamek.server.Server; +import megamek.server.commands.arguments.*; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; +import java.util.Optional; + +/** + * @author Luana Coppio + */ +public class NuclearStrikeCommand extends ClientServerCommand { + + /** Creates new NukeCommand */ + public NuclearStrikeCommand(Server server, TWGameManager gameManager) { + super(server, gameManager, "ns", Messages.getString("Gamemaster.cmd.nuke.help"), + Messages.getString("Gamemaster.cmd.nuke.longName")); + } + + public enum NukeType { + DAVY_CROCKETT_I, + DAVY_CROCKETT_M, + ALAMO, + SANTA_ANA, + PEACEMAKER + } + + @Override + public List> defineArguments() { + return List.of( + new CoordXArgument("x", Messages.getString("Gamemaster.cmd.x")), + new CoordYArgument("y", Messages.getString("Gamemaster.cmd.y")), + new EnumArgument<>("type", Messages.getString("Gamemaster.cmd.nuke.type"), NukeType.class, NukeType.DAVY_CROCKETT_M), + new OptionalIntegerArgument("playerID", Messages.getString("Gamemaster.cmd.playerID")) + ); + } + + @Override + @SuppressWarnings("unchecked") + protected void runCommand(int connId, Arguments args) { + // Check to make sure nuking is allowed by game options! + if (!isGM(connId)) { + if (!(server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_REALLY_ALLOW_NUKES) + && server.getGame().getOptions().booleanOption(OptionsConstants.ALLOWED_ALLOW_NUKES))) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.nuke.error.disabled")); + return; + } + } + + if (isOutsideOfBoard(connId, args)) { + return; + } + + var nukeType = (NukeType) args.get("type").getValue(); + int[] nuke = { + ((IntegerArgument) args.get("x")).getValue() - 1, + ((IntegerArgument) args.get("y")).getValue() - 1, + nukeType.ordinal() + }; + + var playerID = (Optional) args.get("playerID").getValue(); + var player = getGameManager().getGame().getPlayer(playerID.orElse(Player.PLAYER_NONE)); + if (player != null) { + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(Entity.NONE, Player.PLAYER_NONE, AmmoType.Munitions.M_DAVY_CROCKETT_M))); + } + + gameManager.addScheduledNuke(nuke); + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.nuke.success")); + + } +} diff --git a/megamek/src/megamek/server/commands/NuclearStrikeCustomCommand.java b/megamek/src/megamek/server/commands/NuclearStrikeCustomCommand.java new file mode 100644 index 00000000000..a46a87d859e --- /dev/null +++ b/megamek/src/megamek/server/commands/NuclearStrikeCustomCommand.java @@ -0,0 +1,93 @@ +/* + * 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.AmmoType; +import megamek.common.Entity; +import megamek.common.Player; +import megamek.common.actions.NukeDetonatedAction; +import megamek.common.event.GamePlayerStrategicActionEvent; +import megamek.common.options.OptionsConstants; +import megamek.server.Server; +import megamek.server.commands.arguments.*; +import megamek.server.totalwarfare.TWGameManager; + +import java.util.List; +import java.util.Optional; + +/** + * @author Luana Coppio + */ +public class NuclearStrikeCustomCommand extends ClientServerCommand { + + /** Creates new NukeCommand */ + public NuclearStrikeCustomCommand(Server server, TWGameManager gameManager) { + super(server, gameManager, "nsc", Messages.getString("Gamemaster.cmd.nukec.help"), + Messages.getString("Gamemaster.cmd.nukec.longName")); + } + + @Override + public List> defineArguments() { + return List.of( + new CoordXArgument("x", Messages.getString("Gamemaster.cmd.x")), + new CoordYArgument("y", Messages.getString("Gamemaster.cmd.y")), + new IntegerArgument("dmg", Messages.getString("Gamemaster.cmd.nukec.dmg"), 0, 1_000_000), + new IntegerArgument("deg", Messages.getString("Gamemaster.cmd.nukec.deg"), 0, 1_000_000), + new IntegerArgument("radius", Messages.getString("Gamemaster.cmd.nukec.radius"), 1, 1000), + new IntegerArgument("depth", Messages.getString("Gamemaster.cmd.nukec.depth"), 0, 9), + new OptionalIntegerArgument("playerID", Messages.getString("Gamemaster.cmd.playerID")) + ); + } + + @Override + @SuppressWarnings("unchecked") + protected void runCommand(int connId, Arguments args) { + + // The GM can ignore the rules and nuke at will + if (!isGM(connId)) { + // 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))) { + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.nuke.error.disabled")); + return; + } + } + + // is the hex on the board? + if (isOutsideOfBoard(connId, args)) { + return; + } + + int[] nuke = { + (int) args.get("x").getValue() - 1, + (int) args.get("y").getValue() - 1, + (int) args.get("dmg").getValue(), + (int) args.get("deg").getValue(), + (int) args.get("radius").getValue(), + (int) args.get("depth").getValue() + }; + + var playerID = (Optional) args.get("playerID").getValue(); + var player = getGameManager().getGame().getPlayer(playerID.orElse(Player.PLAYER_NONE)); + if (player != null) { + gameManager.getGame().processGameEvent( + new GamePlayerStrategicActionEvent(gameManager, + new NukeDetonatedAction(Entity.NONE, Player.PLAYER_NONE, AmmoType.Munitions.M_DAVY_CROCKETT_M))); + } + + gameManager.addScheduledNuke(nuke); + server.sendServerChat(connId, Messages.getString("Gamemaster.cmd.nuke.success")); + } +} diff --git a/megamek/src/megamek/server/commands/NukeCommand.java b/megamek/src/megamek/server/commands/NukeCommand.java index 322b5893c06..baa79aab479 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,94 @@ package megamek.server.commands; import megamek.common.options.OptionsConstants; +import megamek.server.commands.arguments.*; 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" + - "the end of the next weapons attack phase." + - "Allowed formats:"+ - "/nuke and" + - "/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)"); + 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 " + + "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)", "Nuclear Strike (old)"); 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 CoordXArgument("x", "The x-coordinate of the hex to nuke."), + new CoordYArgument("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..47ab7e377ba 100644 --- a/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java +++ b/megamek/src/megamek/server/commands/OrbitalBombardmentCommand.java @@ -15,8 +15,7 @@ import megamek.client.ui.Messages; import megamek.server.Server; -import megamek.server.commands.arguments.Argument; -import megamek.server.commands.arguments.IntegerArgument; +import megamek.server.commands.arguments.*; import megamek.server.props.OrbitalBombardment; import megamek.server.totalwarfare.TWGameManager; @@ -41,9 +40,9 @@ public OrbitalBombardmentCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { return List.of( - new IntegerArgument(X, Messages.getString("Gamemaster.cmd.x")), - new IntegerArgument(Y, Messages.getString("Gamemaster.cmd.y")), - new IntegerArgument(DMG, Messages.getString("Gamemaster.cmd.orbitalbombardment.dmg"), 10, Integer.MAX_VALUE, 100), + new CoordXArgument(X, Messages.getString("Gamemaster.cmd.x")), + new CoordYArgument(Y, Messages.getString("Gamemaster.cmd.y")), + new IntegerArgument(DMG, Messages.getString("Gamemaster.cmd.orbitalbombardment.dmg"), 10, 1_000_000, 100), new IntegerArgument(RADIUS, Messages.getString("Gamemaster.cmd.orbitalbombardment.radius"), 1, 10, 4)); } @@ -51,7 +50,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..2d97769c6e2 100644 --- a/megamek/src/megamek/server/commands/RescueCommand.java +++ b/megamek/src/megamek/server/commands/RescueCommand.java @@ -17,7 +17,9 @@ 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.commands.arguments.UnitArgument; import megamek.server.totalwarfare.TWGameManager; import java.util.List; @@ -38,14 +40,14 @@ public RescueCommand(Server server, TWGameManager gameManager) { @Override public List> defineArguments() { - return List.of(new IntegerArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.rescue.unitID"))); + return List.of(new UnitArgument(UNIT_ID, Messages.getString("Gamemaster.cmd.rescue.unitID"))); } /** * 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..61654da02de 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 */ @@ -27,16 +27,16 @@ public class VictoryCommand extends ServerCommand { public static final String commandName = "victory"; public static final String helpText = "Causes automatic victory for the issuing player or his/her team at the " + - "end of this turn. Must be acknowledged by all opponents using the " + - "/defeat command. Usage: /victory "; + "end of this turn. Must be acknowledged by all opponents using the " + + "/defeat command. Usage: /victory "; public static final String restrictedUse = "Observers are restricted from declaring victory."; public static final String badPassword = "The password is incorrect. Usage: /victory "; private static final String declareIndividual = " declares individual victory at the end of the turn. This must " + - "be acknowledged by all opponents using the /defeat command or " + - "no victory will occur."; + "be acknowledged by all opponents using the /defeat command or " + + "no victory will occur."; private static final String declareTeam = " declares team victory at the end of the turn. This must be " + - "acknowledged by all opponents using the /defeat command or no " + - "victory will occur."; + "acknowledged by all opponents using the /defeat command or no " + + "victory will occur."; private final TWGameManager gameManager; @@ -59,7 +59,7 @@ public void run(int connId, String[] args) { } if (!server.isPassworded() - || (args.length > 1 && server.isPassword(args[1]))) { + || (args.length > 1 && server.isPassword(args[1]))) { reset(connId); } else { server.sendServerChat(connId, badPassword); } @@ -75,17 +75,13 @@ public static String getDeclareTeam(String playerName) { private void reset(int connId) { Player player = server.getPlayer(connId); - /* - * // are we cancelling victory? if (server.getGame().isForceVictory()) { - * server.sendServerChat(player.getName() + " cancels the force - * victory."); server.cancelVictory(); return; } - */// okay, declare force victory + if (player.getTeam() == Player.TEAM_NONE) { server.sendServerChat(getDeclareIndividual(player.getName())); } 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..ad2fa831aa7 100644 --- a/megamek/src/megamek/server/commands/arguments/Argument.java +++ b/megamek/src/megamek/server/commands/arguments/Argument.java @@ -1,3 +1,16 @@ +/* + * 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; /** @@ -42,4 +55,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..695ddd9133f --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/Arguments.java @@ -0,0 +1,40 @@ +/* + * 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 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..b719f38a53a --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/BooleanArgument.java @@ -0,0 +1,69 @@ +/* + * 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.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/CoordXArgument.java b/megamek/src/megamek/server/commands/arguments/CoordXArgument.java new file mode 100644 index 00000000000..1fca01aff56 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/CoordXArgument.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Argument for an Integer type. + * @author Luana Coppio + */ +public class CoordXArgument extends Argument { + + public CoordXArgument(String name, String description) { + super(name, description); + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < 0) { + throw new IllegalArgumentException(getName() + " must be an integer of an X hex coordinate."); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer of an X hex coordinate."); + } + } + + @Override + public String getHelp() { + return getDescription() + " " + Messages.getString("Gamemaster.cmd.params.required"); + } +} diff --git a/megamek/src/megamek/server/commands/arguments/CoordYArgument.java b/megamek/src/megamek/server/commands/arguments/CoordYArgument.java new file mode 100644 index 00000000000..ab0b6a725f4 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/CoordYArgument.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Argument for an Integer type. + * @author Luana Coppio + */ +public class CoordYArgument extends Argument { + + public CoordYArgument(String name, String description) { + super(name, description); + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < 0) { + throw new IllegalArgumentException(getName() + " must be an integer of an Y hex coordinate."); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer of an Y hex coordinate."); + } + } + + @Override + public String getHelp() { + return getDescription() + " " + Messages.getString("Gamemaster.cmd.params.required"); + } +} diff --git a/megamek/src/megamek/server/commands/arguments/EnumArgument.java b/megamek/src/megamek/server/commands/arguments/EnumArgument.java index cdaae3e7b0d..ff17fc8a086 100644 --- a/megamek/src/megamek/server/commands/arguments/EnumArgument.java +++ b/megamek/src/megamek/server/commands/arguments/EnumArgument.java @@ -1,8 +1,24 @@ +/* + * 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 org.apache.commons.lang3.math.NumberUtils; import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Argument for an Enum type. @@ -19,6 +35,10 @@ public EnumArgument(String name, String description, Class enumType, E defaul this.defaultValue = defaultValue; } + public EnumArgument(String name, String description, Class enumType) { + this(name, description, enumType, null); + } + public Class getEnumType() { return enumType; } @@ -34,7 +54,11 @@ public void parse(String input) throws IllegalArgumentException { } } try { - value = Enum.valueOf(enumType, input.toUpperCase()); + if (NumberUtils.isCreatable(input)) { + value = enumType.getEnumConstants()[Integer.parseInt(input)]; + } else { + value = Enum.valueOf(enumType, input.toUpperCase()); + } } catch (IllegalArgumentException e) { throw new IllegalArgumentException(getName() + " must be one of: " + String.join(", ", Arrays.toString(enumType.getEnumConstants()))); @@ -49,10 +73,16 @@ public E getValue() { return value; } + private String getEnumConstantsString() { + return IntStream.range(0, enumType.getEnumConstants().length) + .mapToObj(i -> i + ": " + enumType.getEnumConstants()[i]) + .collect(Collectors.joining(", ")); + } + @Override public String getHelp() { return getDescription() + - " (" + String.join(", ", Arrays.toString(enumType.getEnumConstants())) + ")" + + " [" + getEnumConstantsString() + "] " + (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/IntegerArgument.java b/megamek/src/megamek/server/commands/arguments/IntegerArgument.java index 2a942bf9e86..2651a0c2a17 100644 --- a/megamek/src/megamek/server/commands/arguments/IntegerArgument.java +++ b/megamek/src/megamek/server/commands/arguments/IntegerArgument.java @@ -1,3 +1,16 @@ +/* + * 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; diff --git a/megamek/src/megamek/server/commands/arguments/OptionalEnumArgument.java b/megamek/src/megamek/server/commands/arguments/OptionalEnumArgument.java index 5e7fcd78630..d079883590f 100644 --- a/megamek/src/megamek/server/commands/arguments/OptionalEnumArgument.java +++ b/megamek/src/megamek/server/commands/arguments/OptionalEnumArgument.java @@ -1,8 +1,23 @@ +/* + * 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 org.apache.commons.lang3.math.NumberUtils; -import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Nullable Argument for an Enum type. @@ -21,7 +36,11 @@ public void parse(String input) throws IllegalArgumentException { return; } try { - value = enumType.getEnumConstants()[Integer.parseInt(input)]; + if (NumberUtils.isCreatable(input)) { + value = enumType.getEnumConstants()[Integer.parseInt(input)]; + } else { + value = Enum.valueOf(enumType, input.toUpperCase()); + } } catch (IllegalArgumentException e) { throw new IllegalArgumentException(getName() + " must be one of: " + getEnumConstantsString()); } @@ -36,14 +55,9 @@ public boolean isEmpty() { } private String getEnumConstantsString() { - var sb = new StringBuilder(); - for (int i = 0; i < enumType.getEnumConstants().length; i++) { - sb.append(i).append(": ").append(enumType.getEnumConstants()[i]); - if (i < enumType.getEnumConstants().length - 1) { - sb.append(", "); - } - } - return sb.toString(); + return IntStream.range(0, enumType.getEnumConstants().length) + .mapToObj(i -> i + ": " + enumType.getEnumConstants()[i]) + .collect(Collectors.joining(", ")); } @Override 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/PlayerArgument.java b/megamek/src/megamek/server/commands/arguments/PlayerArgument.java new file mode 100644 index 00000000000..4060c6d642a --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/PlayerArgument.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Argument for an Integer type for player ID. + * @author Luana Coppio + */ +public class PlayerArgument extends Argument { + + public PlayerArgument(String name, String description) { + super(name, description); + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < 0) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a player."); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a player."); + } + } + + @Override + public String getHelp() { + return getDescription() + " " + Messages.getString("Gamemaster.cmd.params.required"); + } +} 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..c43c4d2c178 --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/StringArgument.java @@ -0,0 +1,67 @@ +/* + * 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; + +/** + * 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/commands/arguments/TeamArgument.java b/megamek/src/megamek/server/commands/arguments/TeamArgument.java new file mode 100644 index 00000000000..2e6cf088cce --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/TeamArgument.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Argument for an Integer type. + * @author Luana Coppio + */ +public class TeamArgument extends Argument { + + public TeamArgument(String name, String description) { + super(name, description); + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < 0 || parsedValue > 5) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a team."); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a team."); + } + } + + @Override + public String getHelp() { + return getDescription() + " " + Messages.getString("Gamemaster.cmd.params.required"); + } +} diff --git a/megamek/src/megamek/server/commands/arguments/UnitArgument.java b/megamek/src/megamek/server/commands/arguments/UnitArgument.java new file mode 100644 index 00000000000..2cd071e5bde --- /dev/null +++ b/megamek/src/megamek/server/commands/arguments/UnitArgument.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Argument for an Integer type. + * @author Luana Coppio + */ +public class UnitArgument extends Argument { + + public UnitArgument(String name, String description) { + super(name, description); + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void parse(String input) throws IllegalArgumentException { + if (input == null) { + throw new IllegalArgumentException(getName() + " is required."); + } + try { + int parsedValue = Integer.parseInt(input); + if (parsedValue < 0) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a unit."); + } + value = parsedValue; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(getName() + " must be an integer ID of a unit."); + } + } + + @Override + public String getHelp() { + return getDescription() + " " + 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..f34a871ea7c 100644 --- a/megamek/src/megamek/server/sbf/SBFGameManager.java +++ b/megamek/src/megamek/server/sbf/SBFGameManager.java @@ -177,6 +177,11 @@ public void addReport(ReportEntry r) { public void calculatePlayerInitialCounts() { } + @Override + public void requestTeamChangeForPlayer(int teamID, Player player) { + + } + /** * Creates a packet containing all entities, including wrecks, visible to * the player in a blind game diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 9cad65ee4da..84f0c484a92 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -122,6 +122,18 @@ public Vector getvPhaseReport() { */ private Player playerChangingTeam = null; + /** + * Keeps track of which players have to change teams. + */ + private Set playersChangingTeam = new HashSet<>(); + + /** + * The immutable request for team change + * @param teamID + * @param player + */ + private record TeamChangeRequest(int teamID, Player player){}; + /** * Flag that is set to true when all players have voted to allow another * player to change teams. @@ -168,7 +180,7 @@ public List getCommandList(Server server) { commands.add(new FixElevationCommand(server, this)); commands.add(new HelpCommand(server)); commands.add(new BotHelpCommand(server)); - commands.add(new KickCommand(server)); + commands.add(new KickCommand(server, this)); commands.add(new ListSavesCommand(server)); commands.add(new LocalSaveGameCommand(server)); commands.add(new LocalLoadGameCommand(server)); @@ -208,6 +220,10 @@ 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)); + commands.add(new NuclearStrikeCommand(server, this)); + commands.add(new NuclearStrikeCustomCommand(server, this)); return commands; } @@ -341,13 +357,31 @@ public void setSeeAll(Player player, boolean seeAll) { sendServerChat(player.getName() + " set SeeAll: " + player.getSeeAll()); } + /** + * request the change of a player from a team to another + * @param team + * @param player + * @deprecated Planned to be removed. Use {@link #requestTeamChangeForPlayer(int, Player)} instead. + */ @Override public void requestTeamChange(int team, Player player) { requestedTeam = team; playerChangingTeam = player; changePlayersTeam = false; + playersChangingTeam.add(new TeamChangeRequest(team, 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() { changePlayersTeam = true; } @@ -364,8 +398,23 @@ 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); @@ -381,15 +430,32 @@ void processTeamChangeRequest() { 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); } @@ -1277,7 +1343,9 @@ private String getDetailedVictoryReport() { * Forces victory for the specified player, or his/her 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(ignorePlayerVotes); game.setForceVictory(true); if (victor.getTeam() == Player.TEAM_NONE) { game.setVictoryPlayerId(victor.getId()); @@ -1287,9 +1355,7 @@ public void forceVictory(Player victor) { game.setVictoryTeam(victor.getTeam()); } - List playersVector = game.getPlayersList(); - for (int i = 0; i < playersVector.size(); i++) { - Player player = playersVector.get(i); + for (Player player : game.getPlayersList()) { player.setAdmitsDefeat(false); } } @@ -2178,7 +2244,11 @@ public boolean 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,14 +2256,16 @@ private boolean isPlayerForcedVictory() { return false; } - for (Player player : game.getPlayersList()) { - if ((player.getId() == game.getVictoryPlayerId()) || ((player.getTeam() == game.getVictoryTeam()) + if (!game.isIgnorePlayerDefeatVotes()) { + for (Player player : game.getPlayersList()) { + if ((player.getId() == game.getVictoryPlayerId()) || ((player.getTeam() == game.getVictoryTeam()) && (game.getVictoryTeam() != Player.TEAM_NONE))) { - continue; - } + continue; + } - if (!player.admitsDefeat()) { - return false; + if (!player.admitsDefeat()) { + return false; + } } } @@ -20537,6 +20609,11 @@ private void drawOrbitalBombardmentIncomingOnBoard(OrbitalBombardment orbitalBom } } + public void drawNukeHitOnBoard(Coords coord) { + int[] position = {coord.getX(), coord.getY()}; + drawNukeHitOnBoard(position); + } + /** * explode any scheduled nukes */ @@ -20646,6 +20723,7 @@ public void doNuclearExplosion(Coords position, int nukeType, Vector vDe if (nukeStats == null) { logger.error("Illegal nuke not listed in HS:3070"); + return; } doNuclearExplosion(position, nukeStats.baseDamage, nukeStats.degradation, nukeStats.secondaryRadius, 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 {