/* * File: ServerLink.java * Author: Catherine * * Created on April 10, 2009, 2:55 AM * * This file is a part of Shoddy Battle. * Copyright (C) 2009 Catherine Fitzpatrick and Benjamin Gwin * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation; either version 3 * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, visit the Free Software Foundation, Inc. * online at http://gnu.org. */ package shoddybattleclient.network; import java.net.*; import java.io.*; import java.util.*; import java.nio.ByteBuffer; import java.util.concurrent.*; import java.security.*; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.swing.JOptionPane; import shoddybattleclient.BattleWindow; import shoddybattleclient.ChatPane; import shoddybattleclient.GameVisualisation.VisualPokemon; import shoddybattleclient.LobbyWindow; import shoddybattleclient.Preference; import shoddybattleclient.ServerConnect; import shoddybattleclient.WelcomeWindow; import shoddybattleclient.forms.BattlePanel; import shoddybattleclient.shoddybattle.Generation; import shoddybattleclient.shoddybattle.Generation.Metagame; import shoddybattleclient.shoddybattle.Generation.RuleSet; import shoddybattleclient.shoddybattle.Pokemon; import shoddybattleclient.shoddybattle.PokemonMove; import shoddybattleclient.shoddybattle.PokemonNature; import shoddybattleclient.shoddybattle.PokemonSpecies; import shoddybattleclient.utils.ClauseList.Clause; import shoddybattleclient.utils.Text; /** * An instance of this class acts as the client's link to the Shoddy Battle 2 * server. * * @author Catherine */ public class ServerLink extends Thread { public static class TimerOptions { public int pool; public int periods; public int periodLength; public TimerOptions(int pool, int pers, int periodLen) { this.pool = pool; this.periods = pers; this.periodLength = periodLen; } } public interface ChallengeMediator { /** * Get the team being used for this challenge. */ public Pokemon[] getTeam(); /** * Called when the challenge has been resolved, either through it * being accepted or rejected. If it was accepted, the method should * send the client's team to the server. */ public void informResolved(boolean accepted); /** * Get the name of the user who has been challenged. */ String getOpponent(); /** * Get the generation being played. */ public int getGeneration(); /** * Get the active party size ("n"). */ public int getActivePartySize(); /** * Get the maximum team length */ public int getMaxTeamLength(); /** * Get the applied clauses */ public int[] getClauses(); /** * Gets the timer options */ public TimerOptions getTimerOptions(); /** * Gets the metagame, or -1 if custom rules are being used */ public int getMetagame(); } //Provies callbacks for elements trying to receive a user's personal message public static interface MessageListener { public void informMessageRecevied(String user, String msg); } /** * Messages sent by the client to the server. */ public static class OutMessage extends ByteArrayOutputStream { protected final DataOutputStream m_stream = new DataOutputStream(this); public OutMessage(int type) { try { m_stream.write(type); m_stream.writeInt(0); // insert in 0 for size for now } catch (IOException e) { } } @Override public byte[] toByteArray() { byte[] bytes = super.toByteArray(); ByteBuffer buffer = ByteBuffer.wrap(bytes); buffer.putInt(1, bytes.length - 5); return bytes; } } public static class RequestChallengeMessage extends OutMessage { public RequestChallengeMessage(String user) { super(0); // see network.cpp try { m_stream.writeUTF(user); } catch (Exception e) { } } } public static class ChallengeResponseMessage extends OutMessage { public ChallengeResponseMessage(byte[] response) { super(1); try { m_stream.write(response, 0, 16); } catch (Exception e) { } } } public static class RegisterAccountMessage extends OutMessage { public RegisterAccountMessage(String user, String password) { super (2); try { m_stream.writeUTF(user); m_stream.writeUTF(password); } catch (Exception e) { } } } public static class JoinChannel extends OutMessage { public JoinChannel(String channel) { super(3); try { m_stream.writeUTF(channel); } catch (Exception e) { } } } public static class ChannelMessage extends OutMessage { public ChannelMessage(int channel, String message) { super(4); try { m_stream.writeInt(channel); m_stream.writeUTF(message); } catch (Exception e) { } } } public static class ModeMessage extends OutMessage { public ModeMessage(int channel, String user, int mode, boolean enable) { super(5); try { m_stream.writeInt(channel); m_stream.writeUTF(user); m_stream.write(mode); m_stream.write(enable ? 1 : 0); } catch (Exception e) { } } } public static class OutgoingChallenge extends OutMessage { public OutgoingChallenge(ChallengeMediator mediator) { super(6); try { m_stream.writeUTF(mediator.getOpponent()); m_stream.writeByte(mediator.getGeneration()); m_stream.writeInt(mediator.getActivePartySize()); m_stream.writeInt(mediator.getMaxTeamLength()); m_stream.writeInt(mediator.getMetagame()); if (mediator.getMetagame() != -1) { return; } int[] clauses = mediator.getClauses(); m_stream.write(clauses.length); for (int i = 0; i < clauses.length; i++) { m_stream.write(clauses[i]); } TimerOptions ops = mediator.getTimerOptions(); if (ops == null) { m_stream.write(0); } else { m_stream.write(1); m_stream.writeInt(ops.pool); m_stream.write(ops.periods); m_stream.writeInt(ops.periodLength); } } catch (Exception e) { } } } public void writePokemon(Pokemon pokemon, DataOutputStream stream) throws IOException { stream.writeInt(PokemonSpecies.getIdFromName( m_generation, pokemon.species)); stream.writeUTF(pokemon.nickname); stream.write(pokemon.shiny ? 1 : 0); stream.write(pokemon.gender.getValue()); stream.write(pokemon.happiness); stream.writeInt(pokemon.level); stream.writeUTF((pokemon.item == null) ? "" : pokemon.item); stream.writeUTF(pokemon.ability); PokemonNature nature = PokemonNature.getNature(pokemon.nature); stream.writeInt(nature.getInternalValue()); stream.writeInt(pokemon.moves.length); for (int i = 0; i < pokemon.moves.length; ++i) { stream.writeInt(PokemonMove.getIdFromName( getMoveList(), pokemon.moves[i])); stream.writeInt(pokemon.ppUps[i]); } for (int i = 0; i < Pokemon.STAT_COUNT; ++i) { stream.writeInt(pokemon.ivs[i]); stream.writeInt(pokemon.evs[i]); } } public void writeTeam(Pokemon[] team, DataOutputStream stream) throws IOException { stream.writeInt(team.length); for (Pokemon i : team) { writePokemon(i, stream); } } public static class ResolveChallenge extends OutMessage { public ResolveChallenge(ServerLink link, String opponent, boolean accepted, Pokemon[] team) { super(7); try { m_stream.writeUTF(opponent); m_stream.write(accepted ? 1 : 0); if (accepted) { link.writeTeam(team, m_stream); } } catch (Exception e) { e.printStackTrace(); } } } public static class ChallengeTeam extends OutMessage { public ChallengeTeam(ServerLink link, String opponent, Pokemon[] team) { super(8); try { m_stream.writeUTF(opponent); link.writeTeam(team, m_stream); } catch (Exception e) { } } } public static class WithdrawChallenge extends OutMessage { public WithdrawChallenge(String opponent) { super(9); try { m_stream.writeUTF(opponent); } catch (Exception e) { } } } public static class BattleAction extends OutMessage { public BattleAction(int fid, int turnType, int index, int target) { super(10); try { m_stream.writeInt(fid); m_stream.write(turnType); m_stream.write(index); m_stream.write(target); } catch (Exception e) { } } } public static class PartChannelMessage extends OutMessage { public PartChannelMessage(int channel) { super(11); try { m_stream.writeInt(channel); } catch (Exception e) { } } } public static class RequestChannelListMessage extends OutMessage { public RequestChannelListMessage() { super(12); } } public static class MetagameQueueMessage extends OutMessage { public MetagameQueueMessage(ServerLink link, int generation, int metagame, boolean rated, Pokemon[] team) { super(13); try { m_stream.write(generation); m_stream.write(metagame); m_stream.write(rated ? 1 : 0); link.writeTeam(team, m_stream); } catch (Exception e) { } } } public static class BanMessage extends OutMessage { public BanMessage(int channel, String user, int date, boolean ipBan) { super(14); try { m_stream.writeInt(channel); m_stream.writeUTF(user); m_stream.writeInt(date); m_stream.write(ipBan ? 1 : 0); } catch (Exception e) { } } } public static class UserDetailMessage extends OutMessage { public UserDetailMessage(String user) { super(15); try { m_stream.writeUTF(user); } catch (Exception e) { } } } public static class PersonalUserMessage extends OutMessage { public PersonalUserMessage(String message) { super(16); try { m_stream.writeUTF(message); } catch (Exception e) { } } } public static class RequestUserMessage extends OutMessage { public RequestUserMessage(String user) { super(17); try { m_stream.writeUTF(user); } catch (Exception e) { } } } public static class MetagameQueueCancelMessage extends OutMessage { public MetagameQueueCancelMessage(int generation, int metagame, boolean rated) { super(19); try { m_stream.write(generation); m_stream.write(metagame); m_stream.write(rated ? 1 : 0); } catch (Exception e) { } } } public static class CancelBattleAction extends OutMessage { public CancelBattleAction(int fid) { super(20); try { m_stream.writeInt(fid); } catch (Exception e) { } } } public static class PrivateMessage extends OutMessage { public PrivateMessage(String target, String message) { super(21); try { m_stream.writeUTF(target); m_stream.writeUTF(message); } catch (Exception e) { } } } public static class ImportantMessage extends OutMessage { public ImportantMessage(int channel, String message) { super(22); try { m_stream.writeInt(channel); m_stream.writeUTF(message); } catch (Exception e) { } } } public static abstract class MessageHandler { /** * Handle a message from the server by reading values from the * DataInputStream. The underlying InputStream is a byte array, not * a socket, so the method is unable to ruin the connection with * the server. */ public abstract void handle(ServerLink link, DataInputStream is) throws IOException; } /** * Messages _received_ from the server that need to be handled by the * client. * * Note that the codes from this enum MUST match the codes from the * OutMessage::TYPE enum in the server. */ public static class ServerMessage { static { m_map = new HashMap<Integer, ServerMessage>(); // WELCOME_MESSAGE new ServerMessage(0, new MessageHandler() { // int32 : server version // string : server name // string : welcome message // byte : are registrations permitted? // string : explanation of how to login // string : explanation of how to register public void handle(ServerLink link, DataInputStream is) throws IOException { int version = is.readInt(); String name = is.readUTF(); String welcome = is.readUTF(); boolean canRegister = (is.readUnsignedByte() != 0); String loginMessage = is.readUTF(); String registerMessage = is.readUTF(); if (ServerLink.CLIENT_VERSION == version) { link.m_serverConnect = new ServerConnect(link, name, welcome, canRegister, loginMessage, registerMessage); link.m_serverConnect.setVisible(true); return; } if (ServerLink.CLIENT_VERSION < version) { JOptionPane.showMessageDialog(null, "Your client is older than the server, " + "please update", "Error", JOptionPane.ERROR_MESSAGE); } else { JOptionPane.showMessageDialog(null, "This server is outdated", "Error", JOptionPane.ERROR_MESSAGE); } link.close(); new WelcomeWindow().setVisible(true); } }); // PASSWORD_CHALLENGE new ServerMessage(1, new MessageHandler() { // byte[16] : the challenge // byte : style of secret: // 0 - secret := password // 1 - secret := md5(password) // 2 - secret := md5(md5(password) + salt) // string : salt, if relevant public void handle(ServerLink link, DataInputStream is) throws IOException { byte[] challenge = new byte[16]; is.readFully(challenge); int style = is.readUnsignedByte(); String salt = is.readUTF(); link.createKeySpec(style, salt); link.m_password = null; // decrypt the challenge try { Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); // pass 1 cipher.init(Cipher.DECRYPT_MODE, link.m_key[1]); challenge = cipher.doFinal(challenge, 0, 16); // pass 2 cipher.init(Cipher.DECRYPT_MODE, link.m_key[0]); challenge = cipher.doFinal(challenge, 0, 16); ByteBuffer buffer = ByteBuffer.wrap(challenge); int r = buffer.getInt(0) + 1; buffer.putInt(0, r); // pass 1 cipher.init(Cipher.ENCRYPT_MODE, link.m_key[0]); challenge = cipher.doFinal(challenge, 0, 16); // pass 2 cipher.init(Cipher.ENCRYPT_MODE, link.m_key[1]); challenge = cipher.doFinal(challenge, 0, 16); link.sendMessage( new ChallengeResponseMessage(challenge)); // don't keep the keys in memory indefinitely link.m_key[0] = null; link.m_key[1] = null; } catch (Exception e) { e.printStackTrace(); } } }); // REGISTRY_RESPONSE new ServerMessage(2, new MessageHandler() { // byte : type // string : details public void handle(ServerLink link, DataInputStream is) throws IOException { int type = is.readUnsignedByte(); String details = is.readUTF(); ServerConnect conn = link.m_serverConnect; // see network.cpp for these values switch (type) { case 0: conn.informNameUnavailable(); break; case 1: conn.informRegisterSuccess(); break; case 2: conn.informInvalidName(); break; case 3: conn.informNameTooLong(); break; case 4: conn.informNonexistentAccount(); break; case 5: conn.informFailedChallenge(); break; case 6: ServerConnect.informUserBanned(details); break; case 7: conn.informSuccessfulLogin(); break; case 8: conn.informAlreadyLoggedIn(); break; case 9: JOptionPane.showMessageDialog(null, "This server is full", "Error", JOptionPane.ERROR_MESSAGE); break; } } }); // CHANNEL_INFO new ServerMessage(4, new MessageHandler() { // int32 : channel id // byte : channel info // string : channel name // string : channel topic // int32 : channel flags // int32 : number of users // for each user: // string : name // int32 : flags public void handle(ServerLink link, DataInputStream is) throws IOException { int id = is.readInt(); int type = is.read(); String channelName = is.readUTF(); String topic = is.readUTF(); int channelFlags = is.readInt(); int count = is.readInt(); LobbyWindow.Channel channel = new LobbyWindow.Channel(id, type, channelName, topic, channelFlags); for (int i = 0; i < count; ++i) { String name = is.readUTF(); int flags = is.readInt(); channel.addUser(name, flags); } link.m_lobby.addChannel(channel); } }); // CHANNEL_JOIN_PART new ServerMessage(5, new MessageHandler() { // int32 : channel id // string : user // byte : joining? public void handle(ServerLink link, DataInputStream is) throws IOException { int id = is.readInt(); String user = is.readUTF(); boolean join = (is.readByte() != 0); link.m_lobby.handleJoinPart(id, user, join); } }); // CHANNEL_STATUS new ServerMessage(6, new MessageHandler() { // int32 : channel id // string : the person who set the mode // string : user // int32 : flags public void handle(ServerLink link, DataInputStream is) throws IOException { int id = is.readInt(); String setter = is.readUTF(); String user = is.readUTF(); int flags = is.readInt(); link.m_lobby.handleUpdateStatus(id, setter, user, flags); } }); // CHANNEL_LIST new ServerMessage(7, new MessageHandler() { // int32 : number of channels // for each channel: // string : name // byte : type // string : topic // int32 : population public void handle(ServerLink link, DataInputStream is) throws IOException { List<BattlePanel.Battle> battles = new ArrayList<BattlePanel.Battle>(); List<String> chatChannel = new ArrayList<String>(); int count = is.readInt(); for (int i = 0; i < count; ++i) { String name = is.readUTF(); int type = is.read(); String topic = is.readUTF(); int population = is.readInt(); if (type == 1) { // It's a battle. BattlePanel.Battle battle = new BattlePanel.Battle(); battle.id = Integer.valueOf(name).intValue(); battle.population = population; String[] parts = topic.split(","); battle.players = new String[] { parts[0], parts[1] }; battle.generation = Integer.valueOf(parts[2]).intValue(); battle.n = Integer.valueOf(parts[3]).intValue(); battle.ladder = Integer.valueOf( parts[5]).intValue(); battle.rated = (Integer.valueOf( parts[6]).intValue() != 0); battles.add(battle); } else { // It's a chat channel. chatChannel.add(name); } } BattlePanel.Battle[] arr = (BattlePanel.Battle[])battles.toArray( new BattlePanel.Battle[battles.size()]); link.m_lobby.getBattlePanel().setBattles(arr); String[] chats = (String[])chatChannel.toArray( new String[chatChannel.size()]); link.m_lobby.setChatChannels(chats); } }); // CHANNEL_MESSAGE new ServerMessage(8, new MessageHandler() { // int32 : channel id // string : user // string : message public void handle(ServerLink link, DataInputStream is) throws IOException { int id = is.readInt(); String user = is.readUTF(); String message = is.readUTF(); link.m_lobby.handleChannelMessage(id, user, message); } }); // INCOMING_CHALLENGE new ServerMessage(9, new MessageHandler() { // string : user // byte : generation // int32 : active party size // int32 : max team length // int32 : metagame // if metagame == -1: // byte : number of clauses // for each: // byte : clause index // byte : if timing is enabled // if timing is enabled: // int16 : starting time bank // byte : number of periods // int16 : period length public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); int generation = is.read(); int partySize = is.readInt(); int teamLength = is.readInt(); int metagame = is.readInt(); int pool = 0; int periods = 0; int periodLength = 0; if ((partySize > 6) || (teamLength > 6)) { // The client only supports a max team/party size of 6 // Block out all challenges that go beyond this limit link.resolveChallenge(user, false, null); return; } if (metagame != -1) { Generation gen = link.m_generations[generation]; Metagame mg = gen.getMetagame(metagame); link.m_lobby.addChallenge(user, true, generation, partySize, teamLength, metagame, mg); } else { int size = is.read(); final int[] clauses = new int[size]; for (int i = 0; i < size; i++) { clauses[i] = is.read(); } final TimerOptions ops; if (is.read() != 0) { pool = is.readInt(); periods = is.read(); periodLength = is.readInt(); ops = new TimerOptions(pool, periods, periodLength); } else { ops = null; } RuleSet rules = new RuleSet() { @Override public TimerOptions getTimerOptions() { return ops; } @Override public int[] getClauses(List<Clause> cl) { return clauses; } }; link.m_lobby.addChallenge(user, true, generation, partySize, teamLength, metagame, rules); } } }); // FINALISE_CHALLENGE new ServerMessage(10, new MessageHandler() { // string : user // byte : whether the challenge was accepted public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); boolean accepted = (is.read() != 0); ChallengeMediator mediator = link.m_challenges.get(user); if (mediator != null) { mediator.informResolved(accepted); } if (!accepted) { link.m_challenges.remove(user); } } }); // CHALLENGE_WITHDRAWN new ServerMessage(11, new MessageHandler() { // string : opponent public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); if (link.m_lobby.isUserPanelSelected(user)) { JOptionPane.showMessageDialog(link.m_lobby, user + " withdrew his or her challenge"); } link.m_lobby.removeUserPanel(user); } }); // BATTLE_BEGIN new ServerMessage(12, new MessageHandler() { // int32 : field id // string : opponent // byte : party // byte : generation // int16 : metagame (-1 for a direct challenge) // byte : rated // string : unique battle ID public void handle(ServerLink link, DataInputStream is) throws IOException { int id = is.readInt(); String user = is.readUTF(); int party = is.readUnsignedByte(); int generationId = is.readUnsignedByte(); int metagameId = is.readShort(); boolean rated = (is.read() != 0); String battleUid = is.readUTF(); String[] users = null; ChallengeMediator mediator = null; if (party == 0) { users = new String[] { link.m_name, user }; } else { users = new String[] { user, link.m_name }; } int partySize; int maxTeamLength; Pokemon[] team; TimerOptions opts; if (metagameId != -1) { Generation gen = link.m_generations[generationId]; Metagame metagame = gen.getMetagame(metagameId); partySize = metagame.getPartySize(); maxTeamLength = metagame.getMaxTeamLength(); team = metagame.getTeam(); opts = metagame.getTimerOptions(); link.getLobby().getFindPanel().informMatchStarted(); } else { if (party == 0) { // we made the original challenge mediator = link.m_challenges.get(user); link.m_challenges.remove(user); } else { // we were challenged mediator = link.m_lobby.getChallengeMediator(user); link.m_lobby.cancelChallenge(user); } partySize = mediator.getActivePartySize(); team = mediator.getTeam(); maxTeamLength = mediator.getMaxTeamLength(); opts = mediator.getTimerOptions(); link.m_lobby.removeUserPanel(user); } int periods = (opts == null) ? -1 : opts.periods; int periodLength = (opts == null) ? -1 : opts.periodLength; BattleWindow wnd = new BattleWindow(link, id, partySize, maxTeamLength, party, users, team, periods, periodLength, battleUid); link.m_battles.put(id, wnd); wnd.setVisible(true); } }); // REQUEST_ACTION new ServerMessage(13, new MessageHandler() { // int32 : field id // byte : slot of relevant pokemon // byte : position of relevant pokemon // byte : whether this is a replacement // byte : index of the request sequence // byte : number of sequential requests // int32 : number of pokemon // for each pokemon: // byte : whether it is legal to switch to this pokemon // if not replacement: // byte : whether switching is legal // byte : whether there is a forced move // if not forced: // int32 : total number of moves // for each move: // byte : whether the move is legal public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int slot = is.readUnsignedByte(); int pos = is.readUnsignedByte(); boolean replacement = (is.read() != 0); int requestIdx = is.readUnsignedByte(); int requestCount = is.readUnsignedByte(); int count = is.readInt(); boolean[] switches = new boolean[count]; for (int i = 0; i < count; ++i) { switches[i] = (is.read() != 0); } if (replacement) { wnd.requestReplacement(requestIdx, requestCount); wnd.setValidSwitches(switches); } else { wnd.requestAction(pos, slot, requestIdx, requestCount); boolean canSwitch = (is.read() != 0); if (!canSwitch) { Arrays.fill(switches, false); } wnd.setValidSwitches(switches); boolean forced = (is.read() != 0); wnd.setForced(forced); if (!forced) { count = is.readInt(); boolean[] legal = new boolean[count]; for (int i = 0; i < count; ++i) { legal[i] = (is.read() != 0); } wnd.setValidMoves(legal); } } } }); // BATTLE_POKEMON new ServerMessage(14, new MessageHandler() { // int32 : field id // for 0...1: // for 0...n-1: // int16 : species id // if id != -1: // byte : gender // byte : level // byte : whether the pokemon is shiny public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int size = wnd.getPartySize(); for (int i = 0; i < 2; ++i) { for (int j = 0; j < size; ++j) { short id = is.readShort(); if (id == -1) { wnd.updateSprite(i, j); continue; } String species = PokemonSpecies.getNameFromId( link.m_generation, id); wnd.setSpecies(i, j, species); int gender = is.readUnsignedByte(); int level = is.readUnsignedByte(); boolean shiny = (is.read() != 0); VisualPokemon p = wnd.getPokemonForSlot(i, j); if (p == null) continue; p.setSpeciesId(id); p.setGender(gender); p.setLevel(level); p.setShiny(shiny); wnd.updateSprite(i, j); } } wnd.updateSpectatorForm(); } }); // BATTLE_PRINT new ServerMessage(15, new MessageHandler() { // int32 : field id // byte : category // int16 : message id // byte : number of arguments // for each argument: // string : value of the argument public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int category = is.readUnsignedByte(); int msg = is.readShort(); int count = is.readUnsignedByte(); String[] args = new String[count]; for (int i = 0; i < count; ++i) { args[i] = is.readUTF(); } String message = Text.getText(category, msg, args, wnd); wnd.addMessage(null, message, false); } }); // BATTLE_VICTORY new ServerMessage(16, new MessageHandler() { // int32 : field id // int16 : party id public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.readShort(); wnd.informVictory(party); } }); // BATTLE_USE_MOVE new ServerMessage(17, new MessageHandler() { // int32 : field id // byte : party // byte : slot // string : user [nick]name // int16 : move id public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.read(); int slot = is.read(); String name = is.readUTF(); int idx = is.readShort(); String move = PokemonMove.getNameFromId(link.getMoveList(), idx); name = Text.formatName(name, (party == wnd.getParty())); move = "<font class='move'>" + move + "</font>"; String message = Text.getText(4, 10, new String[] { name, move }); wnd.addMessage(null, message, false); } }); // BATTLE_WITHDRAW new ServerMessage(18, new MessageHandler() { // int32 : field id // byte : party // byte : slot // string : user [nick]name public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.read(); int slot = is.read(); String name = is.readUTF(); boolean us = (party == wnd.getParty()); String trainer = Text.formatTrainer(wnd.getTrainer(party), wnd.getParty(), party); name = Text.formatName(name, us); String message = Text.getText(4, 11, new String[] { trainer, name }); wnd.addMessage(null, message, false); } }); // BATTLE_SEND_OUT new ServerMessage(19, new MessageHandler() { // int32 : field id // byte : party // byte : slot // byte : index // string : user [nick]name // int16 : species id // byte : gender // byte : level public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.read(); int slot = is.read(); int index = is.read(); String name = is.readUTF(); int speciesId = is.readShort(); int gender = is.readUnsignedByte(); int level = is.readUnsignedByte(); String species = PokemonSpecies.getNameFromId( link.m_generation, speciesId); wnd.sendOut(party, slot, index, speciesId, species, name, gender, level); if (gender != 0) { species += " "; boolean male = (gender == Pokemon.Gender.GENDER_MALE.getValue()); species += male ? '\u2642' : '\u2640'; } String trainer = Text.formatTrainer(wnd.getTrainer(party), wnd.getParty(), party); name = Text.formatName(name, wnd.getParty() == party); String message = Text.getText(4, 12, new String[] { trainer, name, String.valueOf(level), species }); wnd.addMessage(null, message, false); wnd.updateSpectatorForm(); } }); // BATTLE_HEALTH_CHANGE new ServerMessage(20, new MessageHandler() { // int32 : field id // byte : party // byte : slot // int16 : delta health in [0, 48] // int16 : new total health [0, 48] // int16 : denominator public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.read(); int slot = is.read(); int delta = is.readShort(); int total = is.readShort(); int denominator = is.readShort(); // Update the health bars. wnd.updateHealth(party, slot, total, denominator); wnd.updateSpectatorForm(); boolean ally = wnd.getParty() == party; String name = Text.formatName( wnd.getNameForSlot(party, slot), ally); String number; int id = (delta >= 0) ? 13 : 14; delta = Math.abs(delta); number = Text.formatHealthChange(delta, denominator, Preference.getHealthDisplay(ally)); String message = Text.getText(4, id, new String[] { name, number }); wnd.addMessage(null, message, false); } }); // BATTLE_SET_PP new ServerMessage(21, new MessageHandler() { // int32 : field id // byte : pokemon // byte : move // byte : pp public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int i = is.readUnsignedByte(); int j = is.readUnsignedByte(); int pp = is.readUnsignedByte(); wnd.setPp(i, j, pp); } }); // BATTLE_FAINTED new ServerMessage(22, new MessageHandler() { // int32 : field id // byte : party // byte : slot // string : user [nick]name public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int party = is.read(); int slot = is.read(); String name = is.readUTF(); name = Text.formatName(name, wnd.getParty() == party); String message = Text.getText(4, 15, new String[] { name }); wnd.addMessage(null, message, false); wnd.faint(party, slot); wnd.updateSpectatorForm(); } }); // BATTLE_BEGIN_TURN new ServerMessage(23, new MessageHandler() { // int32 : field id // int16 : turn count // bool : if timing is enabled // if timing is enabled: // for each player: // int16 : remaining time in the pool/period // byte : number of periods remaining public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int count = is.readShort(); wnd.informTurnStart(count); boolean timing = (is.read() != 0); if (timing) { for (int i = 0; i < 2; i++) { int remaining = is.readShort(); int periods = is.read(); wnd.synchroniseClock(i, remaining, periods); } } } }); // SPECTATOR BEGIN new ServerMessage(24, new MessageHandler() { // int32 : field id // string : first player // string : second player // byte : active party size // byte : maximum party size // byte : maximum timer periods or -1 if no timing // // for 0...1: // byte : party size // for 0...party size: // byte : has the pokemon been revealed // if revealed: // int16 : slot the pokemon is in or -1 if no slot // string : the nickname of the pokemon // int16 : species id // byte : gender // byte : level // byte : whether the pokemon is shiny // byte : whether the pokemon is fainted // if not fainted: // byte : present hp in [0, 48] // byte : number of status effects // for each status: // string : id // string : message // byte : effect radius public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); String[] player = new String[2]; player[0] = is.readUTF(); player[1] = is.readUTF(); int n = is.read(); int max = is.read(); int maxPeriods = is.readByte(); BattleWindow battle = new BattleWindow( link, fid, n, max, player, maxPeriods); link.m_battles.put(fid, battle); for (int i = 0; i < 2; ++i) { int size = is.readUnsignedByte(); for (int j = 0; j < size; ++j) { VisualPokemon p = battle.getPokemon(i, j); int revealed = is.readUnsignedByte(); if (revealed != 0) { int slot = is.readShort(); String name = is.readUTF(); int id = is.readShort(); int gender = is.read(); int level = is.readUnsignedByte(); boolean shiny = (is.read() != 0); String species = PokemonSpecies.getNameFromId( link.m_generation, id); p.setSpeciesId(id); p.setSpecies(species); p.setName(name); p.setLevel(level); p.setGender(gender); p.setShiny(shiny); if (slot != -1) { battle.sendOut(i, slot, j, id, species, name, gender, level); battle.setSpecies(i, slot, species); } boolean fainted = (is.read() != 0); int hp = (fainted) ? 0 : is.read(); p.setHealth(hp, 48); if (p.getSlot() != -1) { battle.updateHealth(i, p.getSlot(), hp, 48); } if (fainted) { p.faint(); } else { int nStatus = is.readUnsignedByte(); for (int k = 0; k < nStatus; k++) { String statusId = is.readUTF(); String msg = is.readUTF(); int radius = is.readUnsignedByte(); msg = Text.parse(msg, battle); battle.updateStatus(i, j, radius, statusId, msg, true); } } if (slot != -1) { battle.updateSprite(i, slot); } } } } battle.updateSpectatorForm(); battle.setVisible(true); } }); // BATTLE_SET_MOVE new ServerMessage(25, new MessageHandler() { // int32 : field id // byte : pokemon // byte : move slot // int16 : new move // byte : pp // byte : max pp public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); BattleWindow wnd = link.m_battles.get(fid); if (wnd == null) return; int i = is.readUnsignedByte(); int j = is.readUnsignedByte(); int move = is.readShort(); int pp = is.readUnsignedByte(); int maxPp = is.readUnsignedByte(); wnd.setPokemonMove(i, j, move, pp, maxPp); } }); // METAGAME_LIST new ServerMessage(26, new MessageHandler() { // byte : generation count // for each generation: // string : generation id // string : generation name // byte : metagame count // for each metagame: // string : id // string : name // string : description // byte : party size (n) // byte : max team length // int16 : number of bans // for each ban: // int16 : pokemon id // int16 : number of clauses // for each clause: // string : name of clause // byte : if timing is enabled // if timing is enabled: // short : pool length // byte : number of periods // short : period length public void handle(ServerLink link, DataInputStream is) throws IOException { int nGenerations = is.readUnsignedByte(); Generation[] generations = new Generation[nGenerations]; for (int i = 0; i < generations.length; ++i) { String genId = is.readUTF(); String genName = is.readUTF(); generations[i] = Generation.loadGeneration( genId, genName); int nMetagames = is.readUnsignedByte(); for (int j = 0; j < nMetagames; ++j) { String id = is.readUTF(); String name = is.readUTF(); String description = is.readUTF(); int partySize = is.readUnsignedByte(); int maxTeamLength = is.readUnsignedByte(); List<String> banList = new ArrayList<String>(); int banLength = is.readShort(); for (int k = 0; k < banLength; ++k) { int entry = is.readShort(); String species = PokemonSpecies.getNameFromId( link.m_generation, entry); banList.add(species); } List<String> clauses = new ArrayList<String>(); int clauseLength = is.readShort(); for (int k = 0; k < clauseLength; ++k) { String clause = is.readUTF(); clauses.add(clause); } TimerOptions ops; boolean timing = (is.read() != 0); if (timing) { int pool = is.readShort(); int periods = is.read(); int periodLength = is.readShort(); ops = new TimerOptions(pool, periods, periodLength); } else { ops = null; } generations[i].addMetagame(new Metagame(i, name, id, description, partySize, maxTeamLength, banList, clauses, ops)); } } link.m_generations = generations; if (link.m_lobby != null) { link.m_lobby.getFindPanel().updateMetagames(); } } }); //KICK_BAN_MESSAGE new ServerMessage(27, new MessageHandler() { //int32 : channel //string : mod //string : user //int32 : date public void handle(ServerLink link, DataInputStream is) throws IOException{ int id = is.readInt(); String mod = is.readUTF(); String user = is.readUTF(); int date = is.readInt(); link.getLobby().handleBanMessage(id, mod, user, date); } }); //USER_DETAILS new ServerMessage(28, new MessageHandler() { //string : name of the user //string : ip //byte : number of aliases //for each alias: // string : alias //byte : number of bans //for each ban: // int32 : channel // string : banned name // int32 : expiry public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); if ("".equals(user)) { is.readUTF(); is.readByte(); is.readByte(); link.getLobby().informBadLookup(); } else { String ip = is.readUTF(); int acount = is.read(); List<String> aliases = new ArrayList<String>(); for (int i = 0; i < acount; i++) { aliases.add(is.readUTF()); } int bcount = is.read(); List<BanElement> bans = new ArrayList<BanElement>(); for (int i = 0; i < bcount; i++) { int channel = is.readInt(); String name = is.readUTF(); int expiry = is.readInt(); //insert java tuples here bans.add(new BanElement(channel, name, expiry)); } link.getLobby().showLookupResults(user, ip, aliases, bans); } } }); //USER_PERSONAL_MESSAGE new ServerMessage(29, new MessageHandler() { //string : user //string : message public void handle(ServerLink link, DataInputStream is) throws IOException{ String user = is.readUTF(); String message = is.readUTF(); link.informPersonalMessageReceived(user, message); } }); //BATTLE_STATUS_CHANGE new ServerMessage(30, new MessageHandler() { // int32 : field id // byte : party // byte : position // byte : type // byte : effect radius // string : id // string : message // byte : whether the status was applied public void handle(ServerLink link, DataInputStream is) throws IOException { int fid = is.readInt(); int party = is.readUnsignedByte(); int position = is.readUnsignedByte(); int type = is.readUnsignedByte(); int radius = is.readUnsignedByte(); String id = is.readUTF(); String msg = is.readUTF(); boolean applied = (is.read() != 0); BattleWindow wnd = link.getBattle(fid); if (wnd == null) return; msg = Text.parse(msg, wnd); VisualPokemon p = wnd.getPokemon(party, position); switch(type) { case 0: wnd.updateStatus(party, position, radius, id, msg, applied); if (p.getSlot() != -1) { wnd.updateSprite(party, p.getSlot()); } wnd.updateSpectatorForm(); break; case 1: if (applied) { p.setItem(msg); } else { p.setItem("No Item"); } break; case 2: if (applied) { System.out.println("ABILITY ADDED: " + msg); } else { System.out.println("ABILITY REMOVED: " + msg); } break; } } }); //CLAUSE_LIST new ServerMessage(31, new MessageHandler() { //int16 : number of clauses //for each clause: // string : name // string : description public void handle(ServerLink link, DataInputStream is) throws IOException { int count = is.readShort(); List<Clause> clauses = new ArrayList<Clause>(); for (int i = 0; i < count; i++) { String name = is.readUTF(); String desc = is.readUTF(); clauses.add(new Clause(name, desc)); } link.setClauseList(clauses); } }); //INVALID_TEAM new ServerMessage(32, new MessageHandler() { //string : user of the challenge or empty for matchmaking //byte : size of team //int16 : number of violations //for each violation: // int16 : index of the violated clause public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); int teamSize = is.readUnsignedByte(); int count = is.readShort(); int[] clauses = new int[count]; for (int i = 0; i < count; i++) { clauses[i] = is.readShort(); } link.m_lobby.informInvalidTeam(user, teamSize, clauses); } }); // ERROR_MESSAGE new ServerMessage(33, new MessageHandler() { // byte : type // string : details public void handle(ServerLink link, DataInputStream is) throws IOException { int type = is.readUnsignedByte(); String details = is.readUTF(); // Look up these values in the server's network.cpp String message = "Unknown error"; switch (type) { case 0: message = "The user \"" + details + "\" does not " + "exist"; break; case 1: message = details + " is not online"; break; case 3: message = "You lack the authority to perform this " + "action"; break; } JOptionPane.showMessageDialog(link.m_lobby, message, "Error", JOptionPane.ERROR_MESSAGE); } }); // PRIVATE_MESSAGE new ServerMessage(34, new MessageHandler() { // string : user // string : sender // string : message public void handle(ServerLink link, DataInputStream is) throws IOException { String user = is.readUTF(); String sender = is.readUTF(); String message = is.readUTF(); if (sender.length() > 0) { link.m_lobby.handlePrivateMessage(user, sender, message); } else { link.m_lobby.handleInvalidPrivateMessage(user); } } }); // IMPORTANT_MESSAGE new ServerMessage(35, new MessageHandler() { // int32 : channel // string : sender // string : message public void handle(ServerLink link, DataInputStream is) throws IOException { int channel = is.readInt(); String sender = is.readUTF(); String message = is.readUTF(); link.m_lobby.handleImportantMessage(channel, sender, message); } }); // add additional messages here } private static Map<Integer, ServerMessage> m_map; private MessageHandler m_handler; ServerMessage(int code, MessageHandler handler) { m_handler = handler; m_map.put(code, this); } public void handle(ServerLink link, DataInputStream is) throws IOException { m_handler.handle(link, is); } public static ServerMessage getMessage(int code) { return m_map.get(code); } } public static class BanElement { public int channel; public String name; public int expiry; public BanElement(int channel, String name, int expiry) { this.channel = channel; this.name = name; this.expiry = expiry; } } private static int CLIENT_VERSION = 2; private BlockingQueue<OutMessage> m_queue = new LinkedBlockingQueue<OutMessage>(); private String m_host; private int m_port; private Socket m_socket; private DataInputStream m_input; private DataOutputStream m_output; private SecretKeySpec[] m_key = new SecretKeySpec[2]; private String m_name, m_password; private Thread m_messageThread, m_activityThread; private ServerConnect m_serverConnect; private LobbyWindow m_lobby; private static Generation m_generation; private Map<String, ChallengeMediator> m_challenges = new HashMap<String, ChallengeMediator>(); private Map<Integer, BattleWindow> m_battles = new HashMap<Integer, BattleWindow>(); private Generation[] m_generations; private List<MessageListener> m_msgListeners = new ArrayList<MessageListener>(); private List<Clause> m_clauseList; public Generation[] getGenerations() { return m_generations; } public static List<PokemonSpecies> getSpeciesList() { return m_generation.getSpecies(); } public List<PokemonMove> getMoveList() { return m_generation.getMoves(); } public static Generation getGeneration() { return m_generation; } public void setClauseList(List<Clause> clauses) { m_clauseList = clauses; } public List<Clause> getClauseList() { return m_clauseList; } public ServerLink(String host, int port) throws IOException, UnknownHostException { m_host = host; m_port = port; m_socket = new Socket(); m_socket.bind(null); m_socket.connect(new InetSocketAddress(host, port), 5000); m_input = new DataInputStream(m_socket.getInputStream()); m_output = new DataOutputStream(m_socket.getOutputStream()); } public String getHost() { return m_host; } public int getPort() { return m_port; } public String getHostPort() { return m_host + ":" + m_port; } public BattleWindow getBattle(int id) { return m_battles.get(id); } public void sendBattleMessage(int id, String message) throws ChatPane.CommandException { m_lobby.getChannel(id).getChatPane().sendMessage(message); } public LobbyWindow getLobby() { return m_lobby; } public void postChallenge(ChallengeMediator mediator) { m_challenges.put(mediator.getOpponent(), mediator); sendMessage(new OutgoingChallenge(mediator)); } public void resolveChallenge(String opponent, boolean accepted, Pokemon[] team) { sendMessage(new ResolveChallenge(this, opponent, accepted, team)); } public void postChallengeTeam(String opponent, Pokemon[] team) { sendMessage(new ChallengeTeam(this, opponent, team)); } public void withdrawChallenge(String opponent) { sendMessage(new WithdrawChallenge(opponent)); } public void addMessageListener(MessageListener ml) { m_msgListeners.add(ml); } public void removeMessageListener(MessageListener ml) { m_msgListeners.remove(ml); } public void informPersonalMessageReceived(String user, String message) { for (MessageListener ml : m_msgListeners) { ml.informMessageRecevied(user, message); } } public void loadGeneration(Generation gen) { m_generation = gen; } public void registerAccount(String user, String password) { sendMessage(new RegisterAccountMessage(user, password)); } public void setLobbyWindow(LobbyWindow window) { m_lobby = window; } public void joinChannel(String name) { sendMessage(new JoinChannel(name)); } public void partChannel(int channel) { sendMessage(new PartChannelMessage(channel)); } public void requestChannelList() { sendMessage(new RequestChannelListMessage()); } public void sendChannelMessage(int id, String message) { sendMessage(new ChannelMessage(id, message)); } public void sendSwitchAction(int fid, int idx) { sendMessage(new BattleAction(fid, 1, idx, -1)); } public void sendMoveAction(int fid, int idx, int target) { sendMessage(new BattleAction(fid, 0, idx, target)); } public void updateMode(int channel, String user, int mode, boolean enable) { sendMessage(new ModeMessage(channel, user, mode, enable)); } public void queueTeam(int generation, int metagame, boolean rated, Pokemon[] team) { sendMessage(new MetagameQueueMessage( this, generation, metagame, rated, team)); Generation gen = m_generations[generation]; gen.getMetagame(metagame).setTeam(team); } public void cancelQueue(int generation, int metagame, boolean rated) { sendMessage( new MetagameQueueCancelMessage(generation, metagame, rated)); } public void cancelBattleAction(int fid) { sendMessage(new CancelBattleAction(fid)); } public void sendBanMessage(int channel, String user, long length, boolean ipBan) { int date; if (length == 0) { date = 0; } else if (length < 0) { date = 1; } else { long d = (System.currentTimeMillis() / 1000) + length; if (d > Integer.MAX_VALUE) d = Integer.MAX_VALUE; else if (d < Integer.MIN_VALUE) d = Integer.MIN_VALUE; date = (int)d; } sendMessage(new BanMessage(channel, user, date, ipBan)); } public void sendPrivateMessage(String user, String message) { sendMessage(new PrivateMessage(user, message)); } public void requestUserLookup(String user) { sendMessage(new UserDetailMessage(user)); } public void updatePersonalMessage(String msg) { sendMessage(new PersonalUserMessage(msg)); } public void requestUserMessage(String user) { sendMessage(new RequestUserMessage(user)); } public void sendImportantMessage(int channel, String message) { sendMessage(new ImportantMessage(channel, message)); } private static char[] HEX_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * Convert an encoded String to a String with a hexadecimal format. */ public static String toHexString(String encodedString) { byte[] data = new byte[0]; try { data = encodedString.getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException ex) { } int end = data.length; StringBuffer s = new StringBuffer(end * 2); for (int i = 0; i < end; i++) { int high_nibble = (data[i] & 0xf0) >>> 4; int low_nibble = (data[i] & 0x0f); s.append(HEX_TABLE[high_nibble]); s.append(HEX_TABLE[low_nibble]); } return s.toString(); } private static String md5(String input) throws NoSuchAlgorithmException, UnsupportedEncodingException { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] hash = digest.digest(input.getBytes("ISO-8859-1")); return toHexString(new String(hash, "ISO-8859-1")); } private void createKeySpec(int style, String salt) { try { String secret; if (style == 0) { secret = m_password; } else if (style == 1) { secret = md5(m_password); } else if (style == 2) { secret = md5(md5(m_password) + salt); } else { System.out.println("Unknown secret style = " + style); m_key[0] = m_key[1] = null; return; } MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] key = digest.digest(secret.getBytes("ISO-8859-1")); m_key[0] = new SecretKeySpec(key, 0, 16, "AES"); m_key[1] = new SecretKeySpec(key, 16, 16, "AES"); } catch (Exception e) { e.printStackTrace(); } } public void attemptAuthentication(String user, String password) { m_name = user; m_password = password; sendMessage(new RequestChallengeMessage(user)); } /** * Send a message to the server. * @param msg the message to send. */ public void sendMessage(OutMessage msg) { try { m_queue.put(msg); } catch (InterruptedException e) { } } void spawnMessageQueue() { m_messageThread = new Thread(new Runnable() { public void run() { while (!interrupted()) { OutMessage msg; try { msg = m_queue.take(); } catch (InterruptedException e) { return; // end the thread } byte bytes[] = msg.toByteArray(); try { m_output.write(bytes); } catch (IOException e) { } } } }); m_messageThread.start(); } void spawnActivityThread() { m_activityThread = new Thread(new Runnable() { @Override public void run() { while (!interrupted()) { try { Thread.sleep(45 * 1000); } catch (InterruptedException ex) { break; } sendMessage(new OutMessage(18)); // CLIENT_ACTIVITY } } }); m_activityThread.start(); } public void close() { try { m_input.close(); } catch (Exception e) { } try { m_output.close(); } catch (Exception e) { } } /** * protocol is simple: * byte type : type of message * int32 length : length of message body * byte[length] : message body */ @Override public void run() { spawnMessageQueue(); spawnActivityThread(); while (true) { try { int type = m_input.read(); int length = m_input.readInt(); byte[] body = new byte[length]; m_input.readFully(body); // find the right handler to call final ServerMessage msg = ServerMessage.getMessage(type); if (msg == null) { // unknown message type - but we can skip over it and live System.out.println("Unknown message type: " + type); continue; } // call the handler final DataInputStream stream = new DataInputStream( new ByteArrayInputStream(body)); try { java.awt.EventQueue.invokeAndWait(new Runnable() { public void run() { try { msg.handle(ServerLink.this, stream); } catch (IOException e) { e.printStackTrace(); } } }); } catch (Exception e) { e.printStackTrace(); } } catch (IOException e) { // fatal error - exit the while loop break; } } // interrupt the message thread m_activityThread.interrupt(); m_messageThread.interrupt(); if (m_lobby != null) { m_lobby.addImportantMessage("Disconnected from server"); } else if (m_serverConnect != null) { JOptionPane.showMessageDialog(null, "Disconnected from server"); } else { new WelcomeWindow().setVisible(true); } String message = Text.addClass("Disconnected from server", "important"); for (BattleWindow battle : m_battles.values()) { battle.addMessage(null, message, false); } } public static void main(String[] args) throws Exception { ServerLink link = new ServerLink("localhost", 9000); link.attemptAuthentication("test", "test"); link.run(); // block } }