/*
* MegaMek -
* Copyright (C) 2000,2001,2002,2003,2004,2005 Ben Mazur (bmazur@sev.org)
*
* 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;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.TimerTask;
import java.util.Vector;
import javax.swing.SwingUtilities;
import megamek.client.bot.BotClient;
import megamek.client.commands.AddBotCommand;
import megamek.client.commands.ClientCommand;
import megamek.client.commands.DeployCommand;
import megamek.client.commands.FireCommand;
import megamek.client.commands.HelpCommand;
import megamek.client.commands.MoveCommand;
import megamek.client.commands.RulerCommand;
import megamek.client.commands.ShowEntityCommand;
import megamek.client.commands.ShowTileCommand;
import megamek.client.ui.IClientCommandHandler;
import megamek.common.Board;
import megamek.common.Building;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.EntitySelector;
import megamek.common.Flare;
import megamek.common.Game;
import megamek.common.GameLog;
import megamek.common.GameTurn;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.MapSettings;
import megamek.common.MechFileParser;
import megamek.common.MechSummaryCache;
import megamek.common.Minefield;
import megamek.common.Mounted;
import megamek.common.MovePath;
import megamek.common.PlanetaryConditions;
import megamek.common.Player;
import megamek.common.Report;
import megamek.common.SpecialHexDisplay;
import megamek.common.UnitLocation;
import megamek.common.actions.ArtilleryAttackAction;
import megamek.common.actions.AttackAction;
import megamek.common.actions.ClubAttackAction;
import megamek.common.actions.DodgeAction;
import megamek.common.actions.EntityAction;
import megamek.common.actions.FlipArmsAction;
import megamek.common.actions.TorsoTwistAction;
import megamek.common.event.GameEntityChangeEvent;
import megamek.common.event.GameMapQueryEvent;
import megamek.common.event.GamePlayerChatEvent;
import megamek.common.event.GamePlayerDisconnectedEvent;
import megamek.common.event.GameReportEvent;
import megamek.common.event.GameSettingsChangeEvent;
import megamek.common.net.ConnectionFactory;
import megamek.common.net.ConnectionListenerAdapter;
import megamek.common.net.DisconnectedEvent;
import megamek.common.net.IConnection;
import megamek.common.net.Packet;
import megamek.common.net.PacketReceivedEvent;
import megamek.common.options.GameOptions;
import megamek.common.options.IBasicOption;
import megamek.common.preference.PreferenceManager;
import megamek.common.util.StringUtil;
/**
* This class is instanciated for each client and for each bot running on that
* client. non-local clients are not also instantiated on the
* local server.
*/
public class Client implements IClientCommandHandler {
public static final String CLIENT_COMMAND = "#";
// we need these to communicate with the server
private String name;
private IConnection connection;
// the hash table of client commands
private Hashtable<String, ClientCommand> commandsHash = new Hashtable<String, ClientCommand>();
// some info about us and the server
private boolean connected = false;
public int local_pn = -1;
private String host;
private int port;
// the game state object
public IGame game = new Game();
// here's some game phase stuff
private MapSettings mapSettings;
public String phaseReport;
public String roundReport;
// And close client events!
private Vector<CloseClientListener> closeClientListeners = new Vector<CloseClientListener>();
// we might want to keep a game log...
private GameLog log;
private Vector<Coords> artilleryAutoHitHexes = null;
private boolean disconnectFlag = false;
private Hashtable<String, Integer> duplicateNameHash = new Hashtable<String, Integer>();
private ConnectionListenerAdapter connectionListener = new ConnectionListenerAdapter() {
/**
* Called when it is sensed that a connection has terminated.
*/
@Override
public void disconnected(DisconnectedEvent e) {
Client.this.disconnected();
}
@Override
public void packetReceived(PacketReceivedEvent e) {
handlePacket(e.getPacket());
}
};
/**
* Construct a client which will try to connect. If the connection fails, it
* will alert the player, free resources and hide the frame.
*
* @param name the player name for this client
* @param host the hostname
* @param port the host port
*/
public Client(String name, String host, int port) {
// construct new client
this.name = name;
this.host = host;
this.port = port;
registerCommand(new HelpCommand(this));
registerCommand(new MoveCommand(this));
registerCommand(new RulerCommand(this));
registerCommand(new ShowEntityCommand(this));
registerCommand(new FireCommand(this));
registerCommand(new DeployCommand(this));
registerCommand(new ShowTileCommand(this));
registerCommand(new AddBotCommand(this));
TimerSingleton ts = TimerSingleton.getInstance();
/*
* this should be moved to UI implementations so that they are
* responsible for figuring out who should call update for connection..
* so if somebody does a text-only implementation which doesnt support
* AWT event queue, we dont depend on it
*/
final Runnable packetUpdate = new Runnable() {
public void run() {
updateConnection();
}
};
final TimerTask packetUpdate2 = new TimerTask() {
@Override
public void run() {
try {
SwingUtilities.invokeAndWait(packetUpdate);
} catch (Exception ie) {
}
}
};
ts.schedule(packetUpdate2, 500, 100);
}
/**
* call this once to update the connection
*/
protected void updateConnection() {
if (connection != null) {
connection.update();
}
}
/**
* Attempt to connect to the specified host
*/
public boolean connect() {
connection = ConnectionFactory.getInstance().createClientConnection(
host, port, 1);
boolean result = connection.open();
if (result) {
connection.addConnectionListener(connectionListener);
}
return result;
}
/**
* Shuts down threads and sockets
*/
public void die() {
// If we're still connected, tell the server that we're going down.
if (connected) {
send(new Packet(Packet.COMMAND_CLOSE_CONNECTION));
flushConn();
}
connected = false;
if (connection != null) {
connection.close();
connection = null;
}
for (int i = 0; i < closeClientListeners.size(); i++) {
closeClientListeners.elementAt(i).clientClosed();
}
if (log != null) {
try {
log.close();
} catch (IOException e) {
System.err.print("Exception closing logfile: "); //$NON-NLS-1$
e.printStackTrace();
}
}
System.out.println("client: died"); //$NON-NLS-1$
System.out.flush();
}
/**
* The client has become disconnected from the server
*/
protected void disconnected() {
if (!disconnectFlag) {
disconnectFlag = true;
if (connected) {
connected = false;
die();
}
if (!host.equals("localhost")) { //$NON-NLS-1$
game.processGameEvent(new GamePlayerDisconnectedEvent(this,
getLocalPlayer()));
}
}
}
/**
* Get hexes designated for automatic artillery hits.
*/
public Vector<Coords> getArtilleryAutoHit() {
return artilleryAutoHitHexes;
}
private void initGameLog() {
// log = new GameLog(
// PreferenceManager.getClientPreferences().getGameLogFilename(),
// false,
// (new
// Integer(PreferenceManager.getClientPreferences().getGameLogMaxSize()).longValue()
// * 1024 * 1024) );
log = new GameLog(PreferenceManager.getClientPreferences()
.getGameLogFilename());
}
private boolean keepGameLog() {
return PreferenceManager.getClientPreferences().keepGameLog()
&& !(this instanceof BotClient);
}
/**
* Return an enumeration of the players in the game
*/
public Enumeration<Player> getPlayers() {
return game.getPlayers();
}
public Entity getEntity(int id) {
return game.getEntity(id);
}
/**
* Returns the individual player assigned the index parameter.
*/
public Player getPlayer(int idx) {
return game.getPlayer(idx);
}
/**
* Return the local player
*/
public Player getLocalPlayer() {
return getPlayer(local_pn);
}
/**
* Returns an <code>Enumeration</code> of the entities that match the
* selection criteria.
*/
public Enumeration<Entity> getSelectedEntities(EntitySelector selector) {
return game.getSelectedEntities(selector);
}
/**
* Returns the number of first selectable entity
*/
public int getFirstEntityNum() {
return game.getFirstEntityNum(getMyTurn());
}
/**
* Returns the number of the next selectable entity after the one given
*/
public int getNextEntityNum(int entityId) {
return game.getNextEntityNum(getMyTurn(), entityId);
}
/**
* Returns the number of the first deployable entity
*/
public int getFirstDeployableEntityNum() {
return game.getFirstDeployableEntityNum();
}
/**
* Returns the number of the next deployable entity
*/
public int getNextDeployableEntityNum(int entityId) {
return game.getNextDeployableEntityNum(entityId);
}
/**
* Shortcut to game.board
*/
public IBoard getBoard() {
return game.getBoard();
}
/**
* Returns an enumeration of the entities in game.entities
*/
public Enumeration<Entity> getEntities() {
return game.getEntities();
}
public MapSettings getMapSettings() {
return mapSettings;
}
/**
* Changes the game phase, and the displays that go along with it.
*/
public void changePhase(IGame.Phase phase) {
game.setPhase(phase);
// Handle phase-specific items.
switch (phase) {
case PHASE_STARTING_SCENARIO:
sendDone(true);
break;
case PHASE_EXCHANGE:
sendDone(true);
break;
case PHASE_DEPLOYMENT:
// free some memory thats only needed in lounge
MechSummaryCache.dispose();
MechFileParser.dispose();
memDump("entering deployment phase"); //$NON-NLS-1$
break;
case PHASE_TARGETING:
memDump("entering targeting phase"); //$NON-NLS-1$
break;
case PHASE_MOVEMENT:
memDump("entering movement phase"); //$NON-NLS-1$
break;
case PHASE_OFFBOARD:
memDump("entering offboard phase"); //$NON-NLS-1$
break;
case PHASE_FIRING:
memDump("entering firing phase"); //$NON-NLS-1$
break;
case PHASE_PHYSICAL:
memDump("entering physical phase"); //$NON-NLS-1$
break;
case PHASE_LOUNGE:
MechSummaryCache.getInstance();
duplicateNameHash.clear(); // reset this
break;
}
}
/**
* Adds the specified close client listener to receive close client events.
* This is used by external programs running megamek
*
* @param l the game listener.
*/
public void addCloseClientListener(CloseClientListener l) {
closeClientListeners.addElement(l);
}
/**
* wtf is this? waits for 5 seconds just for nothing?? - itmo fixed this to
* be a bit more sensible..
*/
public void retrieveServerInfo() {
updateConnection();
int retry = 50;
while ((retry-- > 0) && !connected) {
synchronized (this) {
flushConn();
updateConnection();
try {
wait(100);
} catch (InterruptedException ex) {
}
}
}
}
/**
* is it my turn?
*/
public boolean isMyTurn() {
if(game.isPhaseSimultaneous()) {
return game.getTurnForPlayer(local_pn) != null;
}
return (game.getTurn() != null) && game.getTurn().isValid(local_pn, game);
}
public GameTurn getMyTurn() {
if(game.isPhaseSimultaneous()) {
return game.getTurnForPlayer(local_pn);
}
return game.getTurn();
}
/**
* Can I unload entities stranded on immobile transports?
*/
public boolean canUnloadStranded() {
return (game.getTurn() instanceof GameTurn.UnloadStrandedTurn)
&& game.getTurn().isValid(local_pn, game);
}
/**
* Send command to unload stranded entities to the server
*/
public void sendUnloadStranded(int[] entityIds) {
Object[] data = new Object[1];
data[0] = entityIds;
send(new Packet(Packet.COMMAND_UNLOAD_STRANDED, data));
}
/**
* Change whose turn it is.
*/
protected void changeTurnIndex(int index) {
game.setTurnIndex(index);
}
/**
* Send mode-change data to the server
*/
public void sendModeChange(int nEntity, int nEquip, int nMode) {
Object[] data = { new Integer(nEntity), new Integer(nEquip),
new Integer(nMode) };
send(new Packet(Packet.COMMAND_ENTITY_MODECHANGE, data));
}
/**
* Send system mode-change data to the server
*/
public void sendSystemModeChange(int nEntity, int nSystem, int nMode) {
Object[] data = { new Integer(nEntity), new Integer(nSystem),
new Integer(nMode) };
send(new Packet(Packet.COMMAND_ENTITY_SYSTEMMODECHANGE, data));
}
/**
* Send mode-change data to the server
*/
public void sendAmmoChange(int nEntity, int nWeapon, int nAmmo) {
Object[] data = { new Integer(nEntity), new Integer(nWeapon),
new Integer(nAmmo) };
send(new Packet(Packet.COMMAND_ENTITY_AMMOCHANGE, data));
}
/**
* Send movement data for the given entity to the server.
*/
public void moveEntity(int id, MovePath md) {
Object[] data = new Object[2];
data[0] = new Integer(id);
data[1] = md;
send(new Packet(Packet.COMMAND_ENTITY_MOVE, data));
}
/**
* Maintain backwards compatability.
*
* @param id - the <code>int</code> ID of the deployed entity
* @param c - the <code>Coords</code> where the entity should be deployed
* @param nFacing - the <code>int</code> direction the entity should face
*/
public void deploy(int id, Coords c, int nFacing) {
this.deploy(id, c, nFacing, new Vector<Entity>(), false);
}
/**
* Deploy an entity at the given coordinates, with the given facing, and
* starting with the given units already loaded.
*
* @param id - the <code>int</code> ID of the deployed entity
* @param c - the <code>Coords</code> where the entity should be deployed
* @param nFacing - the <code>int</code> direction the entity should face
* @param loadedUnits - a <code>List</code> of units that start the game
* being transported byt the deployed entity.
* @param assaultDrop - true if deployment is an assault drop
*/
public void deploy(int id, Coords c, int nFacing,
Vector<Entity> loadedUnits, boolean assaultDrop) {
int packetCount = 5 + loadedUnits.size();
int index = 0;
Object[] data = new Object[packetCount];
data[index++] = new Integer(id);
data[index++] = c;
data[index++] = new Integer(nFacing);
data[index++] = new Integer(loadedUnits.size());
data[index++] = new Boolean(assaultDrop);
Enumeration<Entity> iter = loadedUnits.elements();
while (iter.hasMoreElements()) {
data[index++] = new Integer((iter.nextElement()).getId());
}
send(new Packet(Packet.COMMAND_ENTITY_DEPLOY, data));
flushConn();
}
/**
* Send a weapon fire command to the server.
*/
public void sendAttackData(int aen, Vector<EntityAction> attacks) {
Object[] data = new Object[2];
data[0] = new Integer(aen);
data[1] = attacks;
send(new Packet(Packet.COMMAND_ENTITY_ATTACK, data));
flushConn();
}
/**
* Send the game options to the server
*/
public void sendGameOptions(String password, Vector<IBasicOption> options) {
final Object[] data = new Object[2];
data[0] = password;
data[1] = options;
send(new Packet(Packet.COMMAND_SENDING_GAME_SETTINGS, data));
}
/**
* Send the game settings to the server
*/
public void sendMapSettings(MapSettings settings) {
send(new Packet(Packet.COMMAND_SENDING_MAP_SETTINGS, settings));
}
/**
* Send the planetary Conditions to the server
*/
public void sendPlanetaryConditions(PlanetaryConditions conditions) {
send(new Packet(Packet.COMMAND_SENDING_PLANETARY_CONDITIONS, conditions));
}
/**
* Send the game settings to the server
*/
public void sendMapQuery(MapSettings query) {
send(new Packet(Packet.COMMAND_QUERY_MAP_SETTINGS, query));
}
/**
* Broadcast a general chat message from the local player
*/
public void sendChat(String message) {
send(new Packet(Packet.COMMAND_CHAT, message));
flushConn();
}
/**
* Sends a "player done" message to the server.
*/
public synchronized void sendDone(boolean done) {
send(new Packet(Packet.COMMAND_PLAYER_READY, new Boolean(done)));
flushConn();
}
/**
* Sends a "reroll initiative" message to the server.
*/
public void sendRerollInitiativeRequest() {
send(new Packet(Packet.COMMAND_REROLL_INITIATIVE));
}
/**
* Sends the info associated with the local player.
*/
public void sendPlayerInfo() {
Player player = game.getPlayer(local_pn);
PreferenceManager.getClientPreferences().setLastPlayerColor(
player.getColorIndex());
PreferenceManager.getClientPreferences().setLastPlayerCategory(
player.getCamoCategory());
PreferenceManager.getClientPreferences().setLastPlayerCamoName(
player.getCamoFileName());
send(new Packet(Packet.COMMAND_PLAYER_UPDATE, player));
}
/**
* Sends an "add entity" packet
*/
public void sendAddEntity(Entity entity) {
checkDuplicateNamesDuringAdd(entity);
send(new Packet(Packet.COMMAND_ENTITY_ADD, entity));
}
/**
* Sends an "add squadron" packet
* This is not working, don't use it
*/
public void sendAddSquadron(Vector<Entity> fighters) {
//checkDuplicateNamesDuringAdd(fs);
send(new Packet(Packet.COMMAND_SQUADRON_ADD, fighters));
}
/**
* Sends an "deploy minefields" packet
*/
public void sendDeployMinefields(Vector<Minefield> minefields) {
send(new Packet(Packet.COMMAND_DEPLOY_MINEFIELDS, minefields));
}
/**
* Sends a "set Artillery Autohit Hexes" packet
*/
public void sendArtyAutoHitHexes(Vector<Coords> hexes) {
artilleryAutoHitHexes = hexes; // save for minimap use
send(new Packet(Packet.COMMAND_SET_ARTYAUTOHITHEXES, hexes));
}
/**
* Sends an "update entity" packet
*/
public void sendUpdateEntity(Entity entity) {
send(new Packet(Packet.COMMAND_ENTITY_UPDATE, entity));
}
/**
* Sends an "update custom initiative" packet
*/
public void sendCustomInit(Player player) {
send(new Packet(Packet.COMMAND_CUSTOM_INITIATIVE, player));
}
/**
* Sends a "delete entity" packet
*/
public void sendDeleteEntity(int id) {
checkDuplicateNamesDuringDelete(id);
send(new Packet(Packet.COMMAND_ENTITY_REMOVE, new Integer(id)));
}
/***
* sends a load game file to the server
*/
public void sendLoadGame(File f) {
ObjectInputStream ois;
try {
ois = new ObjectInputStream(new FileInputStream(f));
send(new Packet(Packet.COMMAND_LOAD_GAME, new Object[] { f, ois.readObject() }));
} catch (Exception e) {
System.out.println("Can't find local savegame "+f);
}
}
/**
* Receives player information from the message packet.
*/
protected void receivePlayerInfo(Packet c) {
int pindex = c.getIntValue(0);
Player newPlayer = (Player) c.getObject(1);
if (getPlayer(newPlayer.getId()) == null) {
game.addPlayer(pindex, newPlayer);
} else {
game.setPlayer(pindex, newPlayer);
}
PreferenceManager.getClientPreferences().setLastPlayerColor(
newPlayer.getColorIndex());
PreferenceManager.getClientPreferences().setLastPlayerCategory(
newPlayer.getCamoCategory());
PreferenceManager.getClientPreferences().setLastPlayerCamoName(
newPlayer.getCamoFileName());
}
/**
* Loads the turn list from the data in the packet
*/
@SuppressWarnings("unchecked")
protected void receiveTurns(Packet packet) {
game.setTurnVector((Vector<GameTurn>) packet.getObject(0));
}
/**
* Loads the board from the data in the net command.
*/
protected void receiveBoard(Packet c) {
Board newBoard = (Board) c.getObject(0);
game.setBoard(newBoard);
}
/**
* Loads the entities from the data in the net command.
*/
@SuppressWarnings("unchecked")
protected void receiveEntities(Packet c) {
Vector<Entity> newEntities = (Vector<Entity>) c.getObject(0);
Vector<Entity> newOutOfGame = (Vector<Entity>) c.getObject(1);
// Replace the entities in the game.
game.setEntitiesVector(newEntities);
if (newOutOfGame != null) {
game.setOutOfGameEntitiesVector(newOutOfGame);
}
}
/**
* Loads entity update data from the data in the net command.
*/
@SuppressWarnings("unchecked")
protected void receiveEntityUpdate(Packet c) {
int eindex = c.getIntValue(0);
Entity entity = (Entity) c.getObject(1);
Vector<UnitLocation> movePath = (Vector<UnitLocation>) c.getObject(2);
// Replace this entity in the game.
game.setEntity(eindex, entity, movePath);
}
protected void receiveEntityAdd(Packet packet) {
int entityId = packet.getIntValue(0);
Entity entity = (Entity) packet.getObject(1);
// Add the entity to the game.
game.addEntity(entityId, entity);
}
protected void receiveEntityRemove(Packet packet) {
int entityId = packet.getIntValue(0);
int condition = packet.getIntValue(1);
// Move the unit to its final resting place.
game.removeEntity(entityId, condition);
}
protected void receiveEntityVisibilityIndicator(Packet packet) {
Entity e = game.getEntity(packet.getIntValue(0));
if (e != null) { // we may not have this entity due to double blind
e.setSeenByEnemy(packet.getBooleanValue(1));
e.setVisibleToEnemy(packet.getBooleanValue(2));
// this next call is only needed sometimes, but we'll just
// call it everytime
game.processGameEvent(new GameEntityChangeEvent(this, e));
}
}
@SuppressWarnings("unchecked")
protected void receiveDeployMinefields(Packet packet) {
game.addMinefields((Vector<Minefield>) packet.getObject(0));
}
@SuppressWarnings("unchecked")
protected void receiveSendingMinefields(Packet packet) {
game.setMinefields((Vector<Minefield>) packet.getObject(0));
}
protected void receiveRevealMinefield(Packet packet) {
game.addMinefield((Minefield) packet.getObject(0));
}
protected void receiveRemoveMinefield(Packet packet) {
game.removeMinefield((Minefield) packet.getObject(0));
}
@SuppressWarnings("unchecked")
protected void receiveUpdateMinefields(Packet packet) {
//only update information if you know about the minefield
Vector<Minefield> newMines = new Vector<Minefield>();
for(Minefield mf : (Vector<Minefield>)packet.getObject(0)) {
if(getLocalPlayer().containsMinefield(mf)) {
newMines.add(mf);
}
}
if(newMines.size() > 0) {
game.resetMinefieldDensity(newMines);
}
}
@SuppressWarnings("unchecked")
protected void receiveBuildingUpdateCF(Packet packet) {
game.getBoard()
.updateBuildingCF((Vector<Building>) packet.getObject(0));
}
@SuppressWarnings("unchecked")
protected void receiveBuildingCollapse(Packet packet) {
game.getBoard()
.collapseBuilding((Vector<Coords>) packet.getObject(0));
}
/**
* Loads entity firing data from the data in the net command
*/
@SuppressWarnings("unchecked")
protected void receiveAttack(Packet c) {
Vector<EntityAction> vector = (Vector<EntityAction>) c.getObject(0);
int charge = c.getIntValue(1);
boolean addAction = true;
for (EntityAction ea : vector) {
int entityId = ea.getEntityId();
if ((ea instanceof TorsoTwistAction) && game.hasEntity(entityId)) {
TorsoTwistAction tta = (TorsoTwistAction) ea;
Entity entity = game.getEntity(entityId);
entity.setSecondaryFacing(tta.getFacing());
} else if ((ea instanceof FlipArmsAction) && game.hasEntity(entityId)) {
FlipArmsAction faa = (FlipArmsAction) ea;
Entity entity = game.getEntity(entityId);
entity.setArmsFlipped(faa.getIsFlipped());
} else if ((ea instanceof DodgeAction) && game.hasEntity(entityId)) {
Entity entity = game.getEntity(entityId);
entity.dodging = true;
addAction = false;
} else if (ea instanceof AttackAction) {
// The equipment type of a club needs to be restored.
if (ea instanceof ClubAttackAction) {
ClubAttackAction caa = (ClubAttackAction) ea;
Mounted club = caa.getClub();
club.restore();
}
}
if (addAction) {
// track in the appropriate list
if (charge == 0) {
game.addAction(ea);
} else if (charge == 1) {
game.addCharge((AttackAction) ea);
}
}
}
}
// Should be private?
public String receiveReport(Vector<Report> v) {
boolean doubleBlind = false;
if (v == null) {
return "[null report vector]";
}
StringBuffer report = new StringBuffer();
for (int i = 0; i < v.size(); i++) {
report.append((v.elementAt(i)).getText());
}
/*
* This make Double blind fully blind. Its best to do this here as at
* the server level you can have two lines merged, i.e. no new line, and
* the second line doesn't have any obsuring data. This way once the
* line gets to the client we filter it. --Torren
*/
while (game.getOptions().booleanOption(
"supress_all_double_blind_messages")
&& (report.indexOf(Report.OBSCURED_STRING) != -1)) {
doubleBlind = true;
int startPos = report.indexOf(Report.OBSCURED_STRING);
int endPos = report.indexOf("\n", startPos);
if (report.lastIndexOf("\n", startPos) != -1) {
startPos = report.lastIndexOf("\n", startPos);
}
// In case we get obscured reports but not final \n
if (endPos <= 0) {
endPos = report.length();
}
if (startPos < 0) {
startPos = 0;
}
report.delete(startPos, endPos);
}
String endReport = report.toString();
// Get rid of some extra double spaces that the pasing can sometimes
// cause.
while ((endReport.indexOf("\n\n") != -1) && doubleBlind) {
// Looks silly but it slows the proccess down enough to keep an
// Inf Loop from happening. -- Torren
endReport = endReport.replaceAll("\n\n", "\n");
}
return endReport;
}
/**
* Saves server entity status data to a local file
*/
private void saveEntityStatus(String sStatus) {
try {
String sLogDir = PreferenceManager.getClientPreferences()
.getLogDirectory();
File logDir = new File(sLogDir);
if (!logDir.exists()) {
logDir.mkdir();
}
String fileName = "entitystatus.txt";
if (PreferenceManager.getClientPreferences().stampFilenames()) {
fileName = StringUtil.addDateTimeStamp(fileName);
}
FileWriter fw = new FileWriter(sLogDir + File.separator + fileName);
fw.write(sStatus);
fw.flush();
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* send the message to the server
*/
protected void send(Packet packet) {
connection.send(packet);
}
/**
* send all buffered packets on their way this should be called after
* everything which causes us to wait for a reply. For example "done" button
* presses etc. to make stuff more efficient, this should only be called
* after a batch of packets is sent,not separately for each packet
*/
protected void flushConn() {
connection.flush();
}
@SuppressWarnings("unchecked")
protected void handlePacket(Packet c) {
if (c == null) {
System.out.println("client: got null packet"); //$NON-NLS-1$
return;
}
switch (c.getCommand()) {
case Packet.COMMAND_CLOSE_CONNECTION:
disconnected();
break;
case Packet.COMMAND_RESET_CONNECTION:
disconnected();
connect();
break;
case Packet.COMMAND_SERVER_GREETING:
connected = true;
send(new Packet(Packet.COMMAND_CLIENT_NAME, name));
break;
case Packet.COMMAND_SERVER_CORRECT_NAME:
correctName(c);
break;
case Packet.COMMAND_LOCAL_PN:
local_pn = c.getIntValue(0);
break;
case Packet.COMMAND_PLAYER_UPDATE:
receivePlayerInfo(c);
break;
case Packet.COMMAND_PLAYER_READY:
getPlayer(c.getIntValue(0)).setDone(c.getBooleanValue(1));
break;
case Packet.COMMAND_PLAYER_ADD:
receivePlayerInfo(c);
break;
case Packet.COMMAND_PLAYER_REMOVE:
game.removePlayer(c.getIntValue(0));
break;
case Packet.COMMAND_CHAT:
if (log == null) {
initGameLog();
}
if ((log != null) && keepGameLog()) {
log.append((String) c.getObject(0));
}
game.processGameEvent(new GamePlayerChatEvent(this, null,
(String) c.getObject(0)));
break;
case Packet.COMMAND_ENTITY_ADD:
receiveEntityAdd(c);
break;
case Packet.COMMAND_ENTITY_UPDATE:
receiveEntityUpdate(c);
break;
case Packet.COMMAND_ENTITY_REMOVE:
receiveEntityRemove(c);
break;
case Packet.COMMAND_ENTITY_VISIBILITY_INDICATOR:
receiveEntityVisibilityIndicator(c);
break;
case Packet.COMMAND_SENDING_MINEFIELDS:
receiveSendingMinefields(c);
break;
case Packet.COMMAND_UPDATE_MINEFIELDS:
receiveUpdateMinefields(c);
break;
case Packet.COMMAND_DEPLOY_MINEFIELDS:
receiveDeployMinefields(c);
break;
case Packet.COMMAND_REVEAL_MINEFIELD:
receiveRevealMinefield(c);
break;
case Packet.COMMAND_REMOVE_MINEFIELD:
receiveRemoveMinefield(c);
break;
case Packet.COMMAND_CHANGE_HEX:
game.getBoard().setHex((Coords) c.getObject(0),
(IHex) c.getObject(1));
break;
case Packet.COMMAND_BLDG_UPDATE_CF:
receiveBuildingUpdateCF(c);
break;
case Packet.COMMAND_BLDG_COLLAPSE:
receiveBuildingCollapse(c);
break;
case Packet.COMMAND_PHASE_CHANGE:
changePhase((IGame.Phase) c.getObject(0));
break;
case Packet.COMMAND_TURN:
changeTurnIndex(c.getIntValue(0));
break;
case Packet.COMMAND_ROUND_UPDATE:
game.setRoundCount(c.getIntValue(0));
break;
case Packet.COMMAND_SENDING_TURNS:
receiveTurns(c);
break;
case Packet.COMMAND_SENDING_BOARD:
receiveBoard(c);
break;
case Packet.COMMAND_SENDING_ENTITIES:
receiveEntities(c);
break;
case Packet.COMMAND_SENDING_REPORTS:
case Packet.COMMAND_SENDING_REPORTS_TACTICAL_GENIUS:
phaseReport = receiveReport((Vector) c.getObject(0));
if (keepGameLog()) {
if ((log == null) && (game.getRoundCount() == 1)) {
initGameLog();
}
if (log != null) {
log.append(phaseReport);
}
}
game.addReports((Vector<Report>) c.getObject(0));
roundReport = receiveReport(game.getReports(game
.getRoundCount()));
if (c.getCommand() == Packet.COMMAND_SENDING_REPORTS_TACTICAL_GENIUS) {
game.processGameEvent(new GameReportEvent(this, null));
}
break;
case Packet.COMMAND_SENDING_REPORTS_SPECIAL:
game.processGameEvent(new GameReportEvent(this,
receiveReport((Vector) c.getObject(0))));
break;
case Packet.COMMAND_SENDING_REPORTS_ALL:
Vector<Vector<Report>> allReports = (Vector<Vector<Report>>) c
.getObject(0);
game.setAllReports(allReports);
if (keepGameLog()) {
// Re-write gamelog.txt from scratch
initGameLog();
if (log != null) {
for (int i = 0; i < allReports.size(); i++) {
log.append(receiveReport(allReports.elementAt(i)));
}
}
}
roundReport = receiveReport(game.getReports(game
.getRoundCount()));
// We don't really have a copy of the phase report at
// this point, so I guess we'll just use the round report
// until the next phase actually completes.
phaseReport = roundReport;
break;
case Packet.COMMAND_ENTITY_ATTACK:
receiveAttack(c);
break;
case Packet.COMMAND_SENDING_GAME_SETTINGS:
game.setOptions((GameOptions) c.getObject(0));
break;
case Packet.COMMAND_SENDING_MAP_SETTINGS:
mapSettings = (MapSettings) c.getObject(0);
game.processGameEvent(new GameSettingsChangeEvent(this));
break;
case Packet.COMMAND_SENDING_PLANETARY_CONDITIONS:
game.setPlanetaryConditions((PlanetaryConditions) c.getObject(0));
game.processGameEvent(new GameSettingsChangeEvent(this));
break;
case Packet.COMMAND_QUERY_MAP_SETTINGS:
game.processGameEvent(new GameMapQueryEvent(this,
(MapSettings) c.getObject(0)));
break;
case Packet.COMMAND_END_OF_GAME:
String sEntityStatus = (String) c.getObject(0);
game.end(c.getIntValue(1), c.getIntValue(2));
// save victory report
saveEntityStatus(sEntityStatus);
break;
case Packet.COMMAND_SENDING_ARTILLERYATTACKS:
Vector<ArtilleryAttackAction> v = (Vector<ArtilleryAttackAction>) c
.getObject(0);
game.setArtilleryVector(v);
break;
case Packet.COMMAND_SENDING_FLARES:
Vector<Flare> v2 = (Vector<Flare>) c.getObject(0);
game.setFlares(v2);
break;
case Packet.COMMAND_SEND_SAVEGAME:
String sFinalFile = (String) c.getObject(0);
String localFile = "savegames" + File.separator + sFinalFile;
try {
File sDir = new File("savegames");
if (!sDir.exists()) {
sDir.mkdir();
}
} catch (Exception e) {
System.err.println("Unable to create savegames directory");
}
try {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(localFile));
oos.writeObject(c.getObject(1));
oos.flush();
oos.close();
} catch (Exception e) {
System.err.println("Unable to save file: " + sFinalFile);
e.printStackTrace();
}
break;
case Packet.COMMAND_LOAD_SAVEGAME:
String loadFile = (String) c.getObject(0);
try {
File f = new File("savegames", loadFile);
sendLoadGame(f);
} catch (Exception e) {
System.err.println("Unable to find the file: " + loadFile);
}
break;
case Packet.COMMAND_SENDING_SPECIAL_HEX_DISPLAY:
game.getBoard().setSpecialHexDisplayTable(
(Hashtable<Coords, Collection<SpecialHexDisplay>>) c
.getObject(0));
//System.err.println("Specials updated");
break;
}
}
/**
* Perform a dump of the current memory usage. <p/> This method is useful in
* tracking performance issues on various player's systems. You can activate
* it by changing the "memorydumpon" setting to "true" in the MegaMek.cfg
* file.
*
* @param where - a <code>String</code> indicating which part of the game
* is making this call.
* @see megamek.common.Settings#memoryDumpOn
* @see megamek.client.Client#changePhase(int)
*/
private void memDump(String where) {
if (PreferenceManager.getClientPreferences().memoryDumpOn()) {
StringBuffer buf = new StringBuffer();
final long total = Runtime.getRuntime().totalMemory();
final long free = Runtime.getRuntime().freeMemory();
final long used = total - free;
buf.append("Memory dump ").append(where); //$NON-NLS-1$
for (int loop = where.length(); loop < 25; loop++) {
buf.append(' ');
}
buf
.append(": used (").append(used).append(") + free (").append(free).append(") = ").append(total); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
System.out.println(buf.toString());
}
}
public String getName() {
return name;
}
public int getPort() {
return port;
}
public String getHost() {
return host;
}
private void correctName(Packet inP) {
setName((String) (inP.getObject(0)));
}
public void setName(String newN) {
name = newN;
}
/**
* Before we officially "add" this unit to the game, check and see if this
* client (player) already has a unit in the game with the same name. If so,
* add an identifier to the units name.
*/
private void checkDuplicateNamesDuringAdd(Entity entity) {
if (duplicateNameHash.get(entity.getShortName()) == null) {
duplicateNameHash.put(entity.getShortName(), new Integer(1));
} else {
int count = duplicateNameHash.get(entity.getShortName()).intValue();
count++;
duplicateNameHash.put(entity.getShortName(), new Integer(count));
entity.duplicateMarker = count;
entity.generateShortName();
entity.generateDisplayName();
}
}
/**
* If we remove an entity, we may need to update the duplicate identifier.
* TODO: This function is super slow :(
* @param id
*/
private void checkDuplicateNamesDuringDelete(int id) {
Entity entity = game.getEntity(id);
Object o = duplicateNameHash.get(entity.getShortNameRaw());
if (o != null) {
int count = ((Integer) o).intValue();
if (count > 1) {
ArrayList<Entity> myEntities = game.getPlayerEntities(game
.getPlayer(local_pn), false);
for (int i = 0; i < myEntities.size(); i++) {
Entity e = myEntities.get(i);
if (e.getShortNameRaw().equals(entity.getShortNameRaw())
&& (e.duplicateMarker > entity.duplicateMarker)) {
e.duplicateMarker--;
e.generateShortName();
e.generateDisplayName();
sendUpdateEntity(e);
}
}
duplicateNameHash.put(entity.getShortNameRaw(), new Integer(
count - 1));
} else {
duplicateNameHash.remove(entity.getShortNameRaw());
}
}
}
/**
* @param text a client command with CLIENT_COMMAND prepended.
*/
public String runCommand(String cmd) {
cmd = cmd.substring(CLIENT_COMMAND.length());
return runCommand(cmd.split("\\s+"));
}
/**
* Runs the command
*
* @param args the command and it's arguments with the CLIENT_COMMAND
* already removed, and the string tokenized.
*/
public String runCommand(String[] args) {
if ((args != null) && (args.length > 0)
&& commandsHash.containsKey(args[0])) {
return commandsHash.get(args[0]).run(args);
}
return "Unknown Client Command.";
}
/**
* Registers a new command in the client command table
*/
public void registerCommand(ClientCommand command) {
commandsHash.put(command.getName(), command);
}
/**
* Returns the command associated with the specified name
*/
public ClientCommand getCommand(String commandName) {
return commandsHash.get(commandName);
}
/*
* (non-Javadoc)
*
* @see megamek.client.ui.IClientCommandHandler#getAllCommandNames()
*/
public Enumeration<String> getAllCommandNames() {
return commandsHash.keys();
}
}