package net.sf.colossus.server; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JOptionPane; import net.sf.colossus.client.IClient; import net.sf.colossus.common.Constants; import net.sf.colossus.common.Options; import net.sf.colossus.common.WhatNextManager; import net.sf.colossus.common.WhatNextManager.WhatToDoNext; import net.sf.colossus.game.EntrySide; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Phase; import net.sf.colossus.game.Player; import net.sf.colossus.game.PlayerColor; import net.sf.colossus.game.Proposal; import net.sf.colossus.game.actions.AddCreatureAction; import net.sf.colossus.game.actions.Recruitment; import net.sf.colossus.game.actions.Summoning; import net.sf.colossus.util.BuildInfo; import net.sf.colossus.util.ErrorUtils; import net.sf.colossus.util.Glob; import net.sf.colossus.util.InstanceTracker; import net.sf.colossus.variant.BattleHex; import net.sf.colossus.variant.CreatureType; import net.sf.colossus.variant.MasterHex; import net.sf.colossus.xmlparser.TerrainRecruitLoader; /** * Class Server lives on the server side and handles all communcation with * the clients. It talks to the server classes locally, and to the Clients * via the network protocol. * * @author David Ripton */ public final class Server extends Thread implements IServer { private static final Logger LOGGER = Logger.getLogger(Server.class .getName()); private static StartupProgress startLog; private GameServerSide game; private final WhatNextManager whatNextManager; private final MessageRecorder recorder; private final ExtraRollRequest extraRollRequest; private final SuspendGameRequest suspendGameRequest; // A dummy/internal ClientHandler, which stores all messages sent // to "all clients" => can clone from here for spectators private ClientHandlerStub clientStub; /** * Maybe also save things like the originating IP, in case a * connection breaks and we need to authenticate reconnects. * Do not share these references. */ /** Recipients for everything send to "each client" - including the stub */ private final List<IClient> iClients = new ArrayList<IClient>(); /** Only real ClientHandlers (excluding the stub/internal spectator) */ private final List<ClientHandler> realClients = new ArrayList<ClientHandler>(); private final List<IClient> remoteClients = new ArrayList<IClient>(); private final List<RemoteLogHandler> remoteLogHandlers = new ArrayList<RemoteLogHandler>(); /** Map of players to their clients. */ private final Map<Player, IClient> playerToClientMap = new HashMap<Player, IClient>(); /** List of SocketChannels that are currently active */ private final List<SocketChannel> activeSocketChannelList = new ArrayList<SocketChannel>(); /** ClientHandlers to be withdrawn, together with some related (timing) * data; selector thread will do it then when it's the right time for it */ private final Map<String, WithdrawInfo> forcedWithdraws = new HashMap<String, WithdrawInfo>(); /** Number of player clients we're waiting for to *connect* */ private int waitingForClients; /** Number of player clients we're waiting for to *join* * - when last one has joined, then kick of newGame2() or loadGame2() */ private int waitingForPlayersToJoin = 0; /** Semaphor for synchronized access to waitingForPlayersToJoin */ private final Object wfptjSemaphor = new Object(); /** Will be set to true after all clients are properly connected */ private boolean sendPingRequests = false; private int spectators = 0; private int connectionIdCounter = 1; /** Server socket port. */ private final int port; // Cached strike information. private CreatureServerSide striker; private CreatureServerSide target; private int strikeNumber; private List<String> rolls; // Network stuff private ServerSocket serverSocket; private Selector selector = null; private SelectionKey acceptKey = null; private boolean stopAcceptingFlag = false; private final Object guiRequestMutex = new Object(); private boolean guiRequestQuitFlag = false; private boolean guiRequestSaveFlag = false; private String guiRequestSaveFilename = null; private boolean inPauseState = false; private boolean suspendFlag = false; private boolean saveBeforeSuspend = true; /* static so that new instance of Server can destroy a * previously allocated FileServerThread */ private static Thread fileServerThread = null; private boolean serverRunning = false; private boolean obsolete = false; private boolean shuttingDown = false; private boolean forceShutDown = false; private boolean initiateDisposal = false; private String caughtUpAction = ""; private final int timeoutDuringStart = 1000; private final int timeoutDuringGame = 1000; private final int timeoutDuringShutdown = 1000; /** How long in public server games socket shall wait for Clients. */ private final static int WEBGAMES_STARTUP_TIMEOUT_SECS = 20; private final int PING_REQUEST_INTERVAL_SEC = 30; private final long MAX_PING_OVERDUE = 50000; /** * How many ms ago last ping round was done. */ private long lastPingRound = 0; /** * When server started to listed for clients */ private long startInititatedTime = 0; /** * Set to true once all players have connected and game started. * If any client with a player's name then connects from scratch * (= connectionId -1), then it's a player that had to toally * restart his application */ private boolean allInitialConnectsDone = false; /** * Timeout how long server waits for clients before giving up; * in normal/local games 0, meaning forever; * in public server usage set to WEBGAMES_STARTUP_TIMEOUT_SECS */ private int gameStartupTimeoutSecs = 0; // Earlier I have locked on an Boolean object itself, // which I modify... and when this is done too often, // e.g. in ClientSocketThread every read, it caused // StockOverflowException... :-/ private final Object disposeAllClientsDoneMutex = new Object(); private boolean disposeAllClientsDone = false; private final ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // The ClientHandler of which the input is currently processed ClientHandler processingCH = null; // During processing of redoLog, need to override the processing player // with the one who did that event originally // (because right now, all redo is processed while processingCH is the // one of the last player that joined and triggered the loadGame2() etc.) ClientHandler overriddenCH = null; // Channels are queued into here, to be removed from selector on // next possible opportunity ( = when all waiting-to-be-processed keys // have been processed). private final List<ClientHandlerStub> channelChanges = new ArrayList<ClientHandlerStub>(); Server(GameServerSide game, WhatNextManager whatNextMgr, int port) { this.game = game; this.port = port; this.whatNextManager = whatNextMgr; this.recorder = new MessageRecorder(); this.extraRollRequest = new ExtraRollRequest(this); this.suspendGameRequest = new SuspendGameRequest(this); if (startLog != null) { startLog.dispose(); startLog = null; } if (game.getNotifyWebServer().isActive()) { // If started by WebServer, do not log to StartupProgressLog. } else { startLog = new StartupProgress(this); } int expectedPlayers = game.getNumLivingPlayers(); initWaitingForPlayersToJoin(expectedPlayers); waitingForClients = expectedPlayers; if (Constants._CREATE_LOCAL_DUMMY_CLIENT) { waitingForClients += 1; } InstanceTracker.register(this, "only one"); } @Override public void run() { startInititatedTime = new Date().getTime(); if (game.getNotifyWebServer().isActive()) { gameStartupTimeoutSecs = WEBGAMES_STARTUP_TIMEOUT_SECS; } boolean gotAll = waitForClients(); game.actOnWaitForClientsCompleted(gotAll); if (!remoteClients.isEmpty()) { LOGGER.info("Remote clients => setting sendPingRequests to true"); sendPingRequests = true; } else if ("true".equals(System.getProperty("send.pings"))) { LOGGER.info("Property 'send.pings' is true" + " => setting sendPingRequests to true"); sendPingRequests = true; } int timeout = timeoutDuringGame; int disposeRound = 0; while (!shuttingDown && disposeRound < 60) { waitOnSelector(timeout, false); // The following is handling the case that game did initiate // the dispose by itself due to AutoQuit when game over if (initiateDisposal) { if (disposeRound == 0) { LOGGER.info("Game disposal initiated. Waiting for clients" + " to catch up..."); timeout = timeoutDuringShutdown; // Requesting all clients to confirm that they have caught // up with the processing of all the messages at game end. // When all caught up, this will trigger game.dispose(), // which which do stopServerRunning, // which will do stopFileServer, disposeAllClients, // and set shuttingDown and serverRunning to false. // Skip those that are "in trouble" ( where ClientHandler // currently wouldn't in practice send anything anyway...) allRequestConfirmCatchup("DisposeGame", true); } disposeRound++; LOGGER.info("In while !shutting down loop, " + "initDisp true, round=" + disposeRound); } } LOGGER.info("While !shuttingDown loop ends, disposeRound=" + disposeRound); if (serverRunning && disposeRound >= 60) { LOGGER.warning("The game.dispose() not triggered by caughtUp" + " - doing it now."); game.dispose(); } if (shuttingDown) { LOGGER.fine("shuttingDown set, before closeSocketAndSelector()"); closeSocketAndSelector(); LOGGER.fine("shuttingDown set, after closeSocketAndSelector()"); } else { LOGGER.fine("shuttingDown NOT set"); } notifyThatGameFinished(); LOGGER.fine("Server.run() ends."); } void initFileServer() { stopFileServer(); // start if either we expect (alive) remote client players, or // there are already remote connections (e.g. spectators): if (game.getNumRemoteRemaining() > 0 || !remoteClients.isEmpty() || game.getOption(Options.keepAccepting)) { startFileServerIfNotRunning(); } else { LOGGER.finest("No alive remote client or spectator" + " - not launching the file server."); } } void startFileServerIfNotRunning() { if (fileServerThread == null) { fileServerThread = new FileServerThread(this, port + 1); fileServerThread.start(); } } // FileServerThread asks this public boolean isKnownClient(InetAddress requester) { boolean knownIP = false; synchronized (activeSocketChannelList) { Iterator<SocketChannel> it = activeSocketChannelList.iterator(); while (it.hasNext() && !knownIP) { SocketChannel sc = it.next(); InetAddress cIP = sc.socket().getInetAddress(); knownIP = requester.equals(cIP); } } return knownIP; } boolean getAllInitialConnectsDone() { return allInitialConnectsDone; } void initSocketServer() { LOGGER.log(Level.FINEST, "initSocketServer, expecting " + game.getNumLivingPlayers() + " player clients."); LOGGER.log(Level.FINEST, "About to create server socket on port " + port); try { selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); serverSocket = ssc.socket(); serverSocket.setReuseAddress(true); InetSocketAddress address = new InetSocketAddress(port); serverSocket.bind(address); acceptKey = ssc.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException ex) { String message = "Could not create server side socket.\n" + "Configure networking in OS, or check that no previous " + "Colossus instance got stuck and is blocking the socket.\n" + "Got IOException: " + ex; LOGGER.severe(message); JOptionPane.showMessageDialog(startLog.getFrame(), message, "Starting game (server side) failed!", JOptionPane.ERROR_MESSAGE); System.exit(1); } catch (Exception anyex) { String message = "Could not create server side socket.\n" + "Configure networking in OS, or check that no previous " + "Colossus instance got stuck and is blocking the socket.\n" + "Got Exception: " + anyex; LOGGER.severe(message); JOptionPane.showMessageDialog(startLog.getFrame(), message, "Starting game (server side) failed!", JOptionPane.ERROR_MESSAGE); System.exit(1); } } boolean waitForClients() { logToStartLog("\nStarting up, waiting for " + waitingForClients + " player clients at port " + port + "\n"); StringBuilder living = new StringBuilder(""); StringBuilder dead = new StringBuilder(""); for (Player p : game.getPlayers()) { String name = p.getName(); StringBuilder list = p.isDead() ? dead : living; if (list.length() > 0) { list.append(", "); } list.append(name); } logToStartLog("Players expected to join (= alive): " + living + "\n"); if (dead.length() > 0) { logToStartLog("Players already dead before save : " + dead + "\n"); } serverRunning = true; while (waitingForClients > 0 && serverRunning && !shuttingDown) { LOGGER .info("Waiting for clients, before waitOnSelector(), waitingForClients=" + waitingForClients + ", serverRunning=" + serverRunning); waitOnSelector(timeoutDuringStart, true); } return (waitingForClients == 0); } public void createClientHandlerStub() { clientStub = new ClientHandlerStub(this, "clientHandlerStub"); // it's an IClient, but not a real ClientHandler: addIClient(clientStub); } private void addIClient(ClientHandlerStub newClient) { //System.out.println("Adding iClient with clientId=" // + newClient.getConnectionId() + ", clientName=" // + newClient.getClientName()); LOGGER.fine("Adding iClient with clientId=" + newClient.getConnectionId() + ", clientName=" + newClient.getClientName()); iClients.add(newClient); displayIClients(); } private void addRealClient(ClientHandler newClient) { realClients.add(newClient); } private void displayIClients() { //System.out.println("iClients contains now " + iClients.size() + " clients."); for (IClient c : iClients) { if (c instanceof ClientHandler) { // int id = ((ClientHandler)c).getConnectionId(); // System.out.println("* " + id); } else { // System.out.println("* Stub"); } } } public ClientHandler getProcessingCH() { return processingCH; } public void overrideProcessingCH(Player player) { overriddenCH = processingCH; processingCH = (ClientHandler)getClient(player); } public void restoreProcessingCH() { processingCH = overriddenCH; overriddenCH = null; } public List<ClientHandler> getRealClients() { return realClients; } public void waitOnSelector(int timeout, boolean stillWaitingForClients) { try { if (stopAcceptingFlag) { LOGGER.info("stopAccepting flag was set..."); stopAccepting(); stopAcceptingFlag = false; } // LOGGER.log(Level.FINEST, "before select()"); int num = selector.select(timeout); //LOGGER.log(Level.FINEST, "select returned, " + num // + " channels are ready to be processed."); handleForcedWithdraws(); handleOutsideChanges((num == 0), stillWaitingForClients); if (forceShutDown) { LOGGER.log(Level.INFO, "waitOnSelector: force shutdown now true! num=" + num); stopAccepting(); LOGGER.info("calling stopServerRunning"); stopServerRunning(); } handleSelectedKeys(); handleChannelChanges(); repeatTellOneHasNetworkTrouble(); allRequestPingIfNeeded(); } catch (ClosedChannelException cce) { LOGGER.log(Level.SEVERE, "socketChannel.register() failed: ", cce); } catch (IOException ex) { LOGGER.log(Level.SEVERE, "IOException while waiting or processing ready channels", ex); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Exception while waiting on selector", e); String message = "Woooah. An exception was caught while " + "waiting on Selector!" + "\nStack trace:\n" + ErrorUtils.makeStackTraceString(e); ErrorUtils.showExceptionDialog(null, message, "Exception caught!", false); } } private void handleForcedWithdraws() { if (!forcedWithdraws.isEmpty()) { Set<String> keys = forcedWithdraws.keySet(); for (String name : keys) { WithdrawInfo info = forcedWithdraws.get(name); long now = new Date().getTime(); int timeLeft = (int)((info.deadline - now) / 1000); if (timeLeft > 0) { long timeSinceLastNotif = now - info.getLastNotification(); if (timeSinceLastNotif >= info.intervalLen) { othersTellRemainingTime(info.ch, timeLeft); info.setLastNotification(now); } LOGGER.fine("forcedWithdraw for player " + name + ": " + timeLeft + " seconds left."); } else { LOGGER.info("Forced withdraw for player " + name); forcedWithdraws.remove(name); String message = "You were too long disconnected - Game did automatic withdraw! Sorry."; info.ch.messageFromServer(message); game.handlePlayerWithdrawal(game.getPlayerByName(name)); } } } } private void handleOutsideChanges(boolean wasTimeout, boolean stillWaitingForClients) { if (handleGuiRequests()) { // OK, select returned due to a wake-up call } else if (wasTimeout) { // LOGGER.info("Server side select timeout..."); if (stillWaitingForClients) { long now = new Date().getTime(); int alreadyTrying = ((int)(now - startInititatedTime)) / 1000; if (gameStartupTimeoutSecs > 0 && alreadyTrying > gameStartupTimeoutSecs) { String reason = "Waiting for clients timed out - giving up!"; LOGGER.warning(reason); logToStartLog(reason); game.getNotifyWebServer().gameStartupFailed(reason); forceShutDown = true; } } } } private void handleSelectedKeys() throws IOException, ClosedChannelException { Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> readyKeys = selectedKeys.iterator(); while (readyKeys.hasNext() && !shuttingDown) { SelectionKey key = readyKeys.next(); readyKeys.remove(); int readyOps = key.readyOps(); LOGGER.finest("handleSelectedKyes, readyOps is " + readyOps); if (key.isAcceptable() && (key.isReadable() || key.isWritable())) { LOGGER .warning("Oops, key is both acceptable and read-or-write?!?"); } if (key.isAcceptable()) { // Accept the new connection SocketChannel sc = ((ServerSocketChannel)key.channel()) .accept(); sc.configureBlocking(false); LOGGER.log(Level.FINE, "Accepted: sc = " + sc); SelectionKey readKey = sc.register(selector, SelectionKey.OP_READ); LOGGER.info("Another client accepted."); ClientHandler ch = new ClientHandler(this, sc, readKey); readKey.attach(ch); // This is sent only for the reason that the client gets // an initial response quickly. ch.sendToClient("SignOn: processing"); synchronized (activeSocketChannelList) { activeSocketChannelList.add(sc); } } else { boolean anythingDone = false; SocketChannel sc = (SocketChannel)key.channel(); ClientHandler ch = (ClientHandler)key.attachment(); if (ch != null) { if (key.isReadable()) { processingCH = ch; handleReadFromChannel(key, sc); processingCH = null; anythingDone = true; } if (key.isValid() && key.isWritable()) { LOGGER.info("Channel for " + ch.getClientName() + " got writable again."); // unregister write. If next time fails, it will register again. key.interestOps(SelectionKey.OP_READ); // just call it to flush out what is still there ch.clearTemporarilyInTrouble(); ch.flushQueuedContent(); anythingDone = true; } if (!anythingDone) { // can this happen? Just to be sure... LOGGER.warning("Unexpected type of ready Operation: " + key.readyOps()); } } else { // can this happen? Just to be sure... LOGGER.warning("ClientHandler for ready key is null?"); } } } // just to be sure. selectedKeys.clear(); } private void handleChannelChanges() throws IOException { synchronized (channelChanges) { boolean somethingToDo = false; if (!channelChanges.isEmpty()) { LOGGER.info("in synchronized(channelChanges), cc size=" + channelChanges.size()); somethingToDo = true; } // Can't use iterator, because e.g. removal of last human/observer // will add more items to the channelChanges list. while (!channelChanges.isEmpty()) { ClientHandlerStub nextCHS = channelChanges.remove(0); if (ClientHandler.class.isInstance(nextCHS)) { ClientHandler nextCH = (ClientHandler)nextCHS; LOGGER.info("Took from channelChanges CH for " + nextCH.getClientName()); SocketChannel sc = nextCH.getSocketChannel(); SelectionKey key = nextCH.getSelectorKey(); if (key == null) { LOGGER.warning("key for to-be-closed-channel is " + "null for CH: " + nextCH.getClientName()); } else if (sc.isOpen()) { LOGGER.info("calling disconnectChannel()"); // sending dispose and setIsGone is done by ClientHandler disconnectChannel(sc, key); } else { // TODO this should not happen, but it does regularly // - find out why and fix. Until then, just info // of warning LOGGER.info("to-be-closed-channel is not open!"); } } else { // just a stub LOGGER.info("Handling channel changes, stub."); } LOGGER.info("Channel Changes, removing clienthandler " + nextCHS.getClientName() + ", connectionId " + nextCHS.getConnectionId() + " from iClients list, list size is: " + iClients.size()); // For now removed, caused ConcurrentModificationException // when game is closed via GUI iClients.remove(nextCHS); realClients.remove(nextCHS); LOGGER.info("After remove, iClients size=" + iClients.size() + ", realClients size=" + realClients.size()); } if (somethingToDo) { LOGGER.info("after while !channelChanges.isEmpty())"); } channelChanges.clear(); } } /** Shutdown initiated by outside, i.e. NOT by the Server thread itself. * Right now, this is used by StartupProgressDialog's abort button. */ public void externShutdown() { forceShutDown = true; selector.wakeup(); } private void stopAccepting() throws IOException { if (acceptKey == null) { LOGGER.warning("request to stopAccepting but acceptKey is null!"); return; } LOGGER.info("Canceling connection accepting key."); acceptKey.cancel(); acceptKey = null; LOGGER.info("Closing server socket"); serverSocket.close(); serverSocket = null; } /** * Close serverSocket and selector, if needed */ private void closeSocketAndSelector() { LOGGER.info("After selector loop, shuttingDown flag is set. " + "Closing socket and selector, if necessary"); if (serverSocket != null && !serverSocket.isClosed()) { try { serverSocket.close(); LOGGER.fine("ServerSocket now closed."); } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Could not close server socket", ex); } } else { LOGGER.fine("After loop: ok, serverSocket was already closed"); } if (selector != null && selector.isOpen()) { try { selector.close(); LOGGER.fine("Server selector closed."); } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Could not close server socket", ex); } } else { LOGGER.fine("After loop: ok, server Selector was already closed"); } } // I took as model this page: // http://www.javafaq.nu/java-article1102.html // Throws IOException when closing the channel fails. private int handleReadFromChannel(SelectionKey key, SocketChannel sc) throws IOException { byteBuffer.clear(); int read = 0; while (true) { try { int r = sc.read(byteBuffer); if (r <= 0) { if (r == -1) { // Remote entity did shut the socket down. // Do the same from our end and cancel the channel. processingCH.setIsGone("EOF on channel"); if (read > 0) { LOGGER.info("Before EOF processing, calling " + "processByteBuffer to handle the " + read + " bytes that were read before."); processByteBuffer(); read = 0; } withdrawFromGameIfRelevant(null, processingCH.didExplicitDisconnect()); disconnectChannel(sc, key); } break; } read += r; // THIS HERE EXISTS ONLY FOR DEBUG/DEVELOPMENT PURPOSES if (processingCH.wasFakeDisconnectFlagSet()) { // TODO Improve: right now it needs something to be processed from // client before the exception strikes. Fix better, e.g. wakeup // Selector? processingCH.clearDisconnectClient(); LOGGER.warning("After read, throwing the fake exception!"); throw new IOException( "ClientTriggeredFakeServerDisconnectException"); } } catch (IOException e) { // set isGone first, to prevent from sending log info to // client channel - channel is gone anyway... LOGGER.log(Level.WARNING, "IOException '" + e.getMessage() + "' while reading from channel for player " + getPlayerName(), e); processingCH.setIsGone("IOException while reading"); if (read > 0) { LOGGER.warning("Before IOException handling processing, " + "calling processByteBuffer to handle the " + read + " bytes that were read before."); processByteBuffer(); read = 0; } // The remote forcibly/unexpectedly closed the connection, // cancel the selection key and close the channel. withdrawFromGameIfRelevant(e, processingCH.didExplicitDisconnect()); disconnectChannel(sc, key); return 0; } } if (read > 0) { LOGGER.finest("Calling processByteBuffer to process the " + read + " bytes received from channel" + sc); processByteBuffer(); } else { LOGGER.finest("readFromChannel: 0 bytes read."); } return read; } private void processByteBuffer() { byteBuffer.flip(); // NOTE that the following might cause trouble // if logging is set to FINEST for server, // and the disconnect does not properly set the // isGone flag... // No problem any more as currently "send log // stuff to remote clients" is removed. LOGGER.finest("* before ch.processInput()"); processingCH.processInput(byteBuffer); LOGGER.finest("* after ch.processInput()"); } /** * Something with the connection of "processingCH" which makes perhaps Withdraw necessary. * * If client seems to support reconnect, mark CH to be temp. disconnected, * otherwise take care of the proper withdrawal. * @param gotException An exception, if calling this was caused by an (IO)Exception, * otherwise null, i.e. it was triggered by EOF. * @param didDisconnect whether an explicit dicsonnect request message had been * received already from that client ( = no point to wait for reconnect attempt). */ private void withdrawFromGameIfRelevant(Exception gotException, boolean didDisconnect) { if (isWithdrawalIrrelevant()) { return; } Player player = getPlayer(); if (player == null) { LOGGER.warning("Skipping withdrawFromGame processing for " + " ClientHandler of " + getProcessingCH().getClientName() + " - no player found for the name"); return; } if (player.isDead()) { LOGGER.info("Skipping withdrawFromGame processing for " + getPlayerName() + " since player is already dead"); return; } String reason; if (gotException != null) { reason = "Caught: " + gotException.getMessage(); } else { reason = "EOF"; } try { if (didDisconnect) { LOGGER.info(reason + " on channel for client " + getPlayerName() + " after Client explicitly requested" + " disconnect - proceeding withDraw and Disconnecting"); withdrawFromGame(); } else if (!game.getOption(Options.keepAccepting)) { LOGGER.warning(reason + " on channel for client " + getPlayerName() + " - game is not configured to accept " + "reconnects, withDrawing player"); withdrawFromGame(); } else if (processingCH.supportsReconnect()) { LOGGER.info(reason + " on channel for client " + getPlayerName() + " - skipping withDraw, waiting for reconnect attempt"); processingCH.setTemporarilyDisconnected(); triggerWithdrawIfDoesNotReconnect(30000, 6); } else { LOGGER.warning(reason + " on channel for client " + getPlayerName() + " - can't reconnect, withDrawing player"); withdrawFromGame(); } } // just in case. To make sure the disconnect one level up really happens, no matter what. catch (Exception e) { LOGGER.log(Level.SEVERE, "Exception while withdrawFromGameIfRelevant: ", e); } } private void triggerWithdrawIfDoesNotReconnect(final long intervalLen, final int intervals) { String withdrawName = processingCH.getPlayerName(); if (forcedWithdraws.containsKey(withdrawName)) { LOGGER.warning("Removing _still_ existing entry for '" + withdrawName + "' from forcedWithdraws list."); forcedWithdraws.remove(withdrawName); } long howLong = (long)(intervals * intervalLen / 1000.0); LOGGER.info("Initiating delayed withdraw for player " + withdrawName + " intervalLen = " + intervalLen + " count " + intervals + " (= " + howLong + " seconds)"); appendToConnLogs(processingCH, "NOTE: Connection to client '" + withdrawName + "' lost; waiting " + howLong + " seconds for possible reconnect..."); forcedWithdraws.put(withdrawName, new WithdrawInfo(processingCH, intervals, intervalLen)); } /** * Called when EOF encountered on a clienthandler. Store which is the currently * processed CH, and after a timeout withdraw that player. * @param intervalLen * @param intervals */ /* private void old_triggerWithdrawIfDoesNotReconnect(final long intervalLen, final int intervals) { final ClientHandler currentProcessingCH = processingCH; Runnable r = new Runnable() { public void run() { String withdrawName = currentProcessingCH.getPlayerName(); LOGGER.info("Initiating delayed withdraw for player " + withdrawName); for (int i = 0; i < intervals; i++) { WhatNextManager.sleepFor(intervalLen); int remaining = intervals - i; LOGGER.fine("countdown for withdraw: " + remaining + " intervals of " + intervalLen + " ms left."); } LOGGER.info("Time's up! Withdrawing player " + withdrawName); forcedWithdraws.add(withdrawName); selector.wakeup(); } }; new Thread(r).start(); } */ /** * Put the ClientHandler into the queue to be removed * from selector on next possible opportunity */ void queueClientHandlerForChannelChanges(ClientHandlerStub ch) { LOGGER.info("Putting CH " + ch.getSignonName() + " to channelChanges list"); if (currentThread().equals(this)) { // OK, activity which is originated by client, so we will // come to the "after select loop" point when this was // completely processed synchronized (channelChanges) { channelChanges.add(ch); } } else { // some other thread wants to tell the ServerThread to // dispose one client - for example EDT when user pressed // "Abort" button in StartupProgress dialog. synchronized (channelChanges) { channelChanges.add(ch); } } } private final Object waitUntilOverMutex = new Object(); public void notifyThatGameFinished() { synchronized (waitUntilOverMutex) { waitUntilOverMutex.notifyAll(); } } public void waitUntilGameFinishes() { LOGGER.finest("Before synchronized(waitUntilOverMutex)"); synchronized (waitUntilOverMutex) { try { waitUntilOverMutex.wait(); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, "interrupted while waiting to be " + "notified that game is over: ", e); } } LOGGER.finest("After synchronized(waitUntilOverMutex) (=completed)."); } /* * Stops the file server (closes FileServerSocket), * disposes all clients, and closes ServerSockets */ public void stopServerRunning() { if (!game.isGameOver()) { LOGGER .info("stopServerRunning called when game was not over yet."); game.setGameOver(true, "Game stopped by system"); } stopFileServer(); // particularly to remove the loggers if (!iClients.isEmpty()) { disposeAllClients(); } serverRunning = false; shuttingDown = true; } public boolean isServerRunning() { return serverRunning; } // last SocketClientThread going down calls this // Synchronized because *might* also be called from Abort button public synchronized void stopFileServer() { LOGGER .finest("About to stop file server socket on port " + (port + 1)); if (fileServerThread != null) { try { LOGGER.info("Stopping the FileServerThread "); ((FileServerThread)fileServerThread).stopGoingOn(); } catch (Exception e) { LOGGER.log(Level.WARNING, "Couldn't stop FileServerThread, got Exception: " + e); } fileServerThread = null; } else { // no fileserver running } } /** * Close the SocketChannel, cancel the selection key and unregister * the SocketChannel from list of active SocketChannels. * * @param sc SocketChannel of the client * @param key Key for that SocketChannel * @throws IOException */ private void disconnectChannel(SocketChannel sc, SelectionKey key) throws IOException { sc.close(); key.cancel(); unregisterSocketChannel(sc); } public void unregisterSocketChannel(SocketChannel socketChannel) { if (activeSocketChannelList == null) { LOGGER.finest("activeSocketChannelList null"); return; } LOGGER.finest("activeSocketChannelList before synch "); synchronized (activeSocketChannelList) { LOGGER.finest("activeSocketChannelList IN synch "); int index = activeSocketChannelList.indexOf(socketChannel); LOGGER.finest("activeSocketChannelList index = " + index); if (index == -1) { return; } activeSocketChannelList.remove(index); if (!serverRunning) { LOGGER.finest("serverRunning false"); return; } // no client whatsoever left => end the game and close server stuff // Even if socket list is empty, client list may not be empty yet, // and need to empty it and close all loggers. if (activeSocketChannelList.isEmpty()) { LOGGER.finest("Server.unregisterSocketChannel(): " + "activeSocketChannelList empty - stopping server..."); stopServerRunning(); } else if (game.getOption(Options.goOnWithoutObserver)) { LOGGER.finest("\n==========\nOne socket went away, " + "but we go on because goOnWithoutObserver is set...\n"); } // or, if only AI player clients left as "observers", // then close everything, too else if (!anyNonAiSocketsLeft()) { LOGGER.finest("Server.unregisterSocket(): " + "All connections to human or network players gone " + "(no point to keep AIs running if noone sees it) " + "- stopping server..."); stopServerRunning(); } else { LOGGER.finest("Server.unregisterSocket(): ELSE case " + "(i.e. someone is left, so it makes sense to go on)"); } } LOGGER.finest("activeSocketChannelList after synch "); } public void setBoardVisibility(Player player, boolean val) { getClient(player).setBoardActive(val); } public boolean isClientGone(Player player) { ClientHandler ch = (ClientHandler)getClient(player); if (ch == null || ch.isGone()) { return true; } return false; } private boolean anyNonAiSocketsLeft() { if (playerToClientMap.isEmpty()) { return false; } for (Player player : game.getPlayers()) { // It's not AI, plus either not gone, or still alive ( = might reconnect) if (!player.isAI() && (!isClientGone(player) || !player.isDead())) { return true; } } // no nonAI connected/alive any more - return false: return false; } public int getNextConnectionId() { return connectionIdCounter++; } // Game calls this, when a new game starts and new Server is created, // so that old SocketServerThreads can see from this flag // that they shall not do anything any more // - otherwise it can happen that PlayerEliminated messages from // dying game reach clients of the new game... public void setObsolete() { this.obsolete = true; if (startLog != null) { startLog.cleanRef(); } } /** * Name of the player, for which data from socket is currently processed. */ String getPlayerName() { // Return the playerName for the processingCH. // processingCH holds the ClientHandler of the client/player for // which data is currently read or processed, then and only then // when it is reading or processing. While the selector is waiting // for next input, it's always set to null. assert processingCH != null : "No processingCH!"; assert processingCH.getPlayerName() != null : "Name for processingCH must not be null!"; return processingCH.getPlayerName(); } /** * The player, for which data from socket is currently processed. */ private Player getPlayer() { Player p = game.getPlayerByName(getPlayerName()); assert p != null : "game.getPlayer returned null player for name " + getPlayerName(); return p; } /** * Might be a player or a spectator (but not a stub) * @param name Name of the player/client/spectator for * which ClientHandler is needed */ public ClientHandler getClientHandlerByName(String name) { for (ClientHandler c : realClients) { if (c.getClientName().equals(name)) { return c; } } return null; } /** * returns true if the active player is the player owning the connection * from which data is currently processed */ private boolean isActivePlayer() { return getPlayer().equals(game.getActivePlayer()); } private PlayerServerSide getActivePlayerSS() { return (PlayerServerSide)game.getActivePlayer(); } private boolean isBattleActivePlayer() { return game.getBattleSS() != null && game.getBattleSS().getBattleActivePlayer() != null && getPlayer().equals(game.getBattleSS().getBattleActivePlayer()); } /** * This is a connect-from-scratch, i.e. after all initial had already * connected somebody connects with an "empty" client, needs to get * all data since game start re-send. * * @param client * @param clientName * @param remote * @param clientVersion * @param buildInfo * @param spectator * @return Reason why failed, or null if successful */ String handleScratchReconnect(ClientHandler client, String clientName, boolean remote, int clientVersion, String buildInfo, boolean spectator) { boolean isReconnect = false; int connectionId = getNextConnectionId(); client.setConnectionId(connectionId); LOGGER.info("Server.handlePlayerScratchReconnect() called with: " + "clientName: '" + clientName + "', remote: " + remote + ", spectator: " + spectator + ", reconnect: " + isReconnect + ", connectionId now " + connectionId + ", client version '" + clientVersion + "', client build info: '" + buildInfo + "'"); warnIfDifferentBuild(buildInfo); ClientHandler existingCH = getClientHandlerByName(clientName); if (existingCH != null) { detachReplacedClient(existingCH); processingCH.setReplacedCH(existingCH); } if (!spectator) { othersTellReconnectOngoing(existingCH); } Player player = findPlayerForNewConnection(clientName, remote, spectator); if (player != null) { removeFromForcedWithdrawsList(clientName); playerToClientMap.put(player, client); if (remote) { addRemoteClient(client, player); } } return null; } /** * Add a Client. * * @param client * @param clientName * @param remote * @param clientVersion * @param spectator * @param connectionId TODO * @param isReconnect TODO * @return Reason why adding Client was refused, null if all is fine. */ String handleNewConnection(ClientHandler client, String clientName, boolean remote, int clientVersion, String buildInfo, boolean spectator, int connectionId) { // System.out.println("handleNewConn, called with connId " + connectionId); boolean isReconnect; if (connectionId == -1 || connectionId == -2) { isReconnect = false; } else { isReconnect = true; } connectionId = getNextConnectionId(); client.setConnectionId(connectionId); LOGGER.info("Server.handleNewConnection() called with: " + "playerName: '" + clientName + "', remote: " + remote + ", spectator: " + spectator + ", reconnect: " + isReconnect + ", connectionId " + connectionId + ", client version '" + clientVersion + "', client build info: '" + buildInfo + "'"); String reasonRejected = checkClientVersion(clientName, clientVersion, buildInfo); if (reasonRejected != null) { return reasonRejected; } // resolves also <by......> remote connections Player player = findPlayerForNewConnection(clientName, remote, spectator); LOGGER.info("Trying to identify player/client for new connection " + "which identifies itself as player " + clientName); if (spectator) { ++spectators; LOGGER.info("Adding spectator #" + spectators + clientName); startFileServerIfNotRunning(); } else if (player == null) { LOGGER.warning("No Player was found for non-spectator playerName " + clientName + "!"); logToStartLog("NOTE: One client attempted to join with player name " + clientName + " - rejected, because no such player is expected!"); return "No player with name " + clientName + " expected."; } else { LOGGER .info("Regular connect for a client with name " + clientName); } ClientHandler existingCH = getClientHandlerByName(clientName); if (existingCH != null) { // sync missing info. if (!spectator) { othersTellReconnectOngoing(existingCH); } isReconnect = true; LOGGER.info("All right, reconnection of known client!"); // (client).cloneRedoQueue(existingCH); detachReplacedClient(existingCH); processingCH.setReplacedCH(existingCH); removeFromForcedWithdrawsList(clientName); } else { LOGGER.info("Client with name " + clientName + " connected first time; creating new ClientHandler"); } if (player != null) { playerToClientMap.put(player, client); } if (remote) { addRemoteClient(client, player); } if (player != null && isReconnect) { logToStartLog("\nPlayer " + player.getName() + " reconnected, game can continue now.\n"); } else if (player != null) { logToStartLog((remote ? "Remote" : "Local") + " player " + clientName + " signed on."); game.getNotifyWebServer().gotClient(player.getName(), remote); if (waitingForClients > 0) { --waitingForClients; LOGGER.info("Decremented waitingForPlayers (to connect) to " + waitingForClients); if (waitingForClients > 0) { String pluralS = (waitingForClients > 1 ? "s" : ""); logToStartLog(" ==> Waiting for " + waitingForClients + " more player client" + pluralS + " to sign on.\n"); } else { logToStartLog("\nGot clients for all players, game can start now.\n"); } } else { if (player.isDead()) { LOGGER.info("Looks like dead player " + clientName + " connects 'from scratch'"); } else { LOGGER.info("Looks like alive player " + clientName + " connects 'from scratch'"); LOGGER.warning("What shall we do here... ?"); } } } else { if (clientName.equals(Constants.INTERNAL_DUMMY_CLIENT_NAME)) { String msg = "Internal dummy spectator (" + clientName + ") signed on."; LOGGER.info(msg); logToStartLog(msg); --waitingForClients; } else { String msg = (remote ? "Remote" : "Local") + " spectator (" + clientName + ") signed on."; LOGGER.info(msg); logToStartLog(msg); } } // ReasonFail == null means "everything is fine.": return null; } private void detachReplacedClient(ClientHandler existingCH) { iClients.remove(existingCH); realClients.remove(existingCH); /* System.out.println("iClients contains now " + iClients.size() + " clients."); for (IClient c : iClients) { if (c instanceof ClientHandler) { int id = ((ClientHandler)c).getConnectionId(); System.out.println("* " + id); } else { System.out.println("* Stub"); } } */ queueClientHandlerForChannelChanges(existingCH); existingCH.declareObsolete(); } private void removeFromForcedWithdrawsList(String name) { LOGGER.info("Removing player with name " + name + " from forcedWithDrawlist. Size was " + forcedWithdraws.size()); forcedWithdraws.remove(name); LOGGER.info("Removed client with name " + name + " from forcedWithDrawlist. Size now " + forcedWithdraws.size()); } private void warnIfDifferentBuild(String buildInfo) { if (!buildInfo.equals(BuildInfo.getFullBuildInfoString())) { LOGGER.info("NOTE: client build info differs from " + "server build info."); } } private Player findPlayerForNewConnection(final String playerName, final boolean remote, boolean spectator) { Player player = null; boolean mustExist = game.isLoadingGame(); if (spectator) { // Could also be a dead player using the watch game option. LOGGER.info("addClient for " + playerName + " with spectator flag set."); player = game.findNetworkPlayer(playerName, mustExist); } else if (remote) { player = game.findNetworkPlayer(playerName, mustExist); if (player == null) { player = game.getPlayerByNameIgnoreNull(playerName); } } else { player = game.getPlayerByNameIgnoreNull(playerName); } return player; } private String checkClientVersion(final String playerName, final int clientVersion, String buildInfo) { String reasonRejected = null; warnIfDifferentBuild(buildInfo); if (clientVersion <= IServer.MINIMUM_CLIENT_VERSION) { String versionText = (clientVersion == -1 ? "-1 (=no Version defined)" : clientVersion + ""); LOGGER.warning("Rejecting client " + playerName + " because it " + "uses too old Client version: " + versionText + " but server requires at least: " + IServer.MINIMUM_CLIENT_VERSION); // We do not disable the autoCloseStartupLog here, because since a // player will be missing the startup will not reach the point // where to close it automatically. logToStartLog("PROBLEM: One client attempted to join with player" + " name " + playerName + "\n- rejected, because client uses " + "too old version: " + versionText); reasonRejected = "You are using too old Client Version: " + versionText + " - expected at least: " + IServer.MINIMUM_CLIENT_VERSION; } else if (clientVersion != IServer.CLIENT_VERSION) { String diffWhat = (clientVersion < IServer.CLIENT_VERSION ? "older" : "newer"); logToStartLog("NOTE: Client version mismatch detected!!!\n" + "One client attempted to join with player name " + playerName + ", using different (" + diffWhat + ") client version: " + clientVersion + " - trying it anyway."); disableAutoCloseStartupLog(); LOGGER.info("Client " + playerName + " uses Client Version: " + clientVersion + " but we would expect " + IServer.CLIENT_VERSION + " - trying it anyway."); } return reasonRejected; } /** * When the last player has *joined* (not just connected), he calls this * here, and this will proceed with either loadGame2() or newGame2(). */ public void startGame() { boolean sa = !game.getOption(Options.keepAccepting); LOGGER.info("Game started, setting stopAcceptingFlag to " + sa); stopAcceptingFlag = sa; if (game.isLoadingGame()) { logToStartLog("Loading game, sending replay data to clients..."); boolean ok = game.loadGame2(); if (ok) { logToStartLog("Waiting for clients to catch up with replay data...\n"); } else { logToStartLog("Loading/Replay failed!!\n"); if (Options.isStartupTest()) { ErrorUtils .setErrorDuringFunctionalTest("Loading/Replay failed!"); game.stopAllDueToFunctionalTestCompleted(); } else { logToStartLog("\n-- Press Abort button " + "to return to Start Game dialog --\n"); loadFailed(); } return; } } else { game.newGame2(); } logToStartLog("\nStarting the game now.\n"); game.getNotifyWebServer().gameStartupCompleted(); if (startLog != null) { startLog.setCompleted(); } allInitialConnectsDone = true; } /** * Initialize the number of players we wait for to join (thread-safe) * * @param count the number of players that are expected to join */ private void initWaitingForPlayersToJoin(int count) { synchronized (wfptjSemaphor) { waitingForPlayersToJoin = count; } } private void addRemoteClient(final IClient client, final Player player) { RemoteLogHandler remoteLogHandler = new RemoteLogHandler(this); LOGGER.addHandler(remoteLogHandler); remoteLogHandlers.add(remoteLogHandler); remoteClients.add(client); if (player == null) { LOGGER.info("addRemoteClient for observer skips name stuff."); return; } if (!game.isLoadingGame()) { // Returns original name if no other player has this name String uName = game.getUniqueName(player.getName(), player); if (!uName.equals(player.getName())) { // set in player player.setName(uName); } } } void disposeAllClients() { synchronized (disposeAllClientsDoneMutex) { if (disposeAllClientsDone) { return; } disposeAllClientsDone = true; } LOGGER.info("BEGIN disposing all clients..."); for (IClient client : iClients) { // This sends the dispose message, and queues ClientHandler's // channel for being removed from selector. // Actual removal happens after all selector-keys are processed. // @TODO: does that make even sense? shuttingDown is set true, // so the selector loop does not even reach the removal part... client.disposeClient(); } iClients.clear(); realClients.clear(); playerToClientMap.clear(); remoteClients.clear(); LOGGER.fine("Removing all loggers..."); for (RemoteLogHandler handler : remoteLogHandlers) { LOGGER.removeHandler(handler); handler.close(); } remoteLogHandlers.clear(); LOGGER.info("COMPLETED disposing all clients..."); } public void loadFailed() { ErrorUtils.showErrorDialog(startLog.getFrame(), "Loading game failed!", "Loading, replay of history and comparison between saved " + "state and replay result failed!!\n\n" + "Click Abort on the Startup Progress Dialog to return to " + "Game setup dialog to start a different or new one."); } public void cleanupStartlog() { if (startLog != null) { startLog.dispose(); startLog = null; } } public void doCleanup() { cleanupStartlog(); game = null; } void allUpdatePlayerInfo(boolean treatDeadAsAlive, String reason) { LOGGER.finest("AllUpdatePlayerInfo, reason " + reason); for (IClient client : iClients) { client.updatePlayerInfo(getPlayerInfo(treatDeadAsAlive)); } } /** * Sends changed player information/values to all clients. * To new enough clients it uses the optimized form: * - data for each player on it's own line (message) * - only for players where actually anything has changed * - the line contains only the frequently changing values like isDead, * score, free markers. See Player.getChangedPlayerValues(). * * To older clients it sends the data in the old way, where one * line contains always all information about each player. * * @param reason Reason what triggered this sending */ void allUpdateChangedPlayerValues(String reason) { LOGGER.finest("AllUpdateChangedPlayerValues, reason " + reason); List<String> changedValuesStrings = getChangedPlayerValues(); List<String> fullInfo = getPlayerInfo(false); for (IClient client : iClients) { if (client.canHandleChangedValuesOnlyStyle()) { for (String valuesString : changedValuesStrings) { client.updateChangedPlayerValues(valuesString, reason); } } else { client.updatePlayerInfo(fullInfo); } } } void allUpdatePlayerInfo(String reason) { allUpdateChangedPlayerValues(reason); //allUpdatePlayerInfo(false, reason); } void allUpdateCreatureCount(CreatureType type, int count, int deadCount) { for (IClient client : iClients) { client.updateCreatureCount(type, count, deadCount); } } void allTellMovementRoll(int roll, String reason) { for (IClient client : iClients) { client.tellMovementRoll(roll, reason); } } public void leaveCarryMode() { if (!isBattleActivePlayer()) { LOGGER .warning(getPlayerName() + " illegally called leaveCarryMode(): not battle active player"); return; } BattleServerSide battle = game.getBattleSS(); battle.leaveCarryMode(); } public void doneWithBattleMoves() { BattleServerSide battle = game.getBattleSS(); if (!isBattleActivePlayer()) { LOGGER .warning(getPlayerName() + " illegally called doneWithBattleMoves(): battle active player is " + battle.getBattleActivePlayer()); LOGGER.info(processingCH.dumpLastProcessedLines()); getClient(getPlayer()).nak( Constants.doneWithBattleMoves, "Illegal attempt to end phase battle-move: battle active player is " + battle.getBattleActivePlayer()); return; } if (!battle.getBattlePhase().isMovePhase()) { LOGGER.warning(getPlayerName() + " illegally called doneWithBattleMoves(): current phase is " + battle.getBattlePhase().toString() + ")"); LOGGER.info(processingCH.dumpLastProcessedLines()); getClient(getPlayer()).nak( Constants.doneWithBattleMoves, "Illegal attempt to end phase battle-move: current phase is " + battle.getBattlePhase().toString() + ")"); return; } battle.doneWithMoves(); } public void doneWithStrikes() { String reason = isDoneWithStrikesOk(); if (reason != null) { LOGGER.warning(getPlayerName() + " illegally called doneWithStrikes(): " + reason); LOGGER.info(processingCH.dumpLastProcessedLines()); getClient(getPlayer()).nak(Constants.doneWithStrikes, reason); } else { game.getBattleSS().doneWithStrikes(); } } /** * Validates that it it OK to be "done with strikes" now for executing player * @return reason why it's not OK; null if all is ok */ private String isDoneWithStrikesOk() { BattleServerSide battle = game.getBattleSS(); if (!isBattleActivePlayer()) { return "Not Battle Active player"; } else if (!battle.getBattlePhase().isFightPhase()) { return "Not a fight phase: battle phase is " + battle.getBattlePhase().toString(); } else if (battle.isForcedStrikeRemaining()) { return "Forced strikes remain"; } return null; } private IClient getClient(Player player) { if (playerToClientMap.containsKey(player)) { return playerToClientMap.get(player); } else { return null; } } void allInitBoard() { for (Player player : game.getPlayers()) { if (!player.isDead()) { IClient client = getClient(player); if (client != null) { client.initBoard(); } } } clientStub.initBoard(); } void allTellReplay(boolean val, int maxTurn) { for (IClient client : iClients) { client.tellReplay(val, maxTurn); } } void allTellRedo(boolean val) { for (IClient client : iClients) { client.tellRedo(val); } } void allRequestConfirmCatchup(String action, boolean skipInTrouble) { // First put them all to the list, send messages after that synchronized (waitingToCatchup) { caughtUpAction = action; waitingToCatchup.clear(); // check both for (ClientHandler client : realClients) { boolean skip = false; // Do not wait for clients that are already gone, e.g. when // one remote disconnected this might cause a withdrawal // which might cause GameOver. if (client.isGone()) { skip = true; } // In some case (currently: during game dispose) do not send // to clients in trouble, they won't respond probably anyway... if (skipInTrouble && client.isTemporarilyInTrouble()) { skip = true; } if (client.getMillisSincePingReply() > MAX_PING_OVERDUE) { skip = true; } if (!skip) { LOGGER.info("Adding to list for Catchup: " + client.getPlayerName()); waitingToCatchup.add(client); } } LOGGER.info("List size: " + waitingToCatchup.size()); for (IClient client : waitingToCatchup) { client.confirmWhenCaughtUp(); } LOGGER.info("Finished sending"); } } void oneTellAllLegionLocations(ClientHandler client) { List<Legion> legions = game.getAllLegions(); for (Legion legion : legions) { client.tellLegionLocation(legion, legion.getCurrentHex()); } } void allTellAllLegionLocations() { List<Legion> legions = game.getAllLegions(); for (Legion legion : legions) { allTellLegionLocation(legion); } } void allTellLegionLocation(Legion legion) { for (IClient client : iClients) { client.tellLegionLocation(legion, legion.getCurrentHex()); } } void allRemoveLegion(Legion legion) { for (IClient client : iClients) { client.removeLegion(legion); } } void allTellPlayerElim(Player eliminatedPlayer, Player slayer, boolean updateHistory) { for (IClient client : iClients) { client.tellPlayerElim(eliminatedPlayer, slayer); } if (updateHistory) { game.playerElimEvent(eliminatedPlayer, slayer); } } void repeatTellOneHasNetworkTrouble() { ArrayList<ClientHandler> troubleCHs = new ArrayList<ClientHandler>(); for (ClientHandler ch : realClients) { if (ch.isTemporarilyInTrouble() && !ch.isSpectator()) { troubleCHs.add(ch); } } if (!troubleCHs.isEmpty()) { for (ClientHandler chInTrouble : troubleCHs) { othersTellOneHasNetworkTrouble(chInTrouble); } } } void othersTellOneHasNetworkTrouble(ClientHandler chInTrouble) { String playerInTrouble = chInTrouble.getPlayerName(); int howLong = (int)(chInTrouble.howLongAlreadyInTrouble() / 1000L); String message = "Problems writing to client of player " + playerInTrouble + " (" + howLong + " secs) - still trying..."; appendToConnLogs(chInTrouble, message); } void othersTellOnesTroubleIsOver(ClientHandler chInTrouble) { String name = chInTrouble.getPlayerName(); String message = "It seems writing to player " + name + " succeeded now."; appendToConnLogs(chInTrouble, message); } void othersTellReconnectOngoing(ClientHandler chInTrouble) { // Note that we need to use getSignOnName here!! String message = "Player " + chInTrouble.getSignonName() + " reconnected! Data synchronization ongoing"; LOGGER.finest(message); appendToConnLogs(chInTrouble, message); } void othersTellRemainingTime(ClientHandler chInTrouble, int secondsLeft) { // Note that we need to use getSignOnName here!! String playerInTrouble = chInTrouble.getSignonName(); String message = "Player " + playerInTrouble + " has still " + secondsLeft + " seconds left before being withdrawn"; appendToConnLogs(chInTrouble, message); } void othersTellReconnectCompleted(ClientHandler chInTrouble) { String name = chInTrouble.getClientName(); String message = "Client of player " + name + " reconnect now successfully completed."; appendToConnLogs(chInTrouble, message); } void appendToConnLogs(ClientHandler chInTrouble, String message) { // only real handlers, no point to send those to the stub... for (ClientHandler client : realClients) { // no need/use to inform the troubled one itself... if (client != chInTrouble) { // System.out.println("aTLC, client " + client.getClientName() + "(id=" + client.getConnectionId() + "): " + message); client.appendToConnectionLog(message); } else { // System.out.println("aTLC, SKIPPING client " + client.getClientName() + "(id=" + client.getConnectionId() + "): " + message); } } } /** * IF last ping round is at least PING_REQUEST_INTERVAL_SEC seconds ago, * then send a ping request to all clients (except those which are in * trouble anyway). */ void allRequestPingIfNeeded() { // skip it totally if feature is inactive // (default behavior for all-clients-local games) if (!sendPingRequests) { return; } long now = new Date().getTime(); if (now - lastPingRound > 1000 * PING_REQUEST_INTERVAL_SEC) { long ago = (now - lastPingRound) / 1000; LOGGER.finer("Last ping round is " + ago + " secs ago - doing another."); lastPingRound = now; for (ClientHandler client : realClients) { if (!client.isTemporarilyInTrouble()) { client.pingRequest(now); } long msSinceLastReply = client.getMillisSincePingReply(); long secsSinceReply = (long)Math .floor((msSinceLastReply + 50) / 1000); if (msSinceLastReply > MAX_PING_OVERDUE) { String msg = "Looks like there's a ping overdue for client " + client.getClientName() + ": already " + secsSinceReply + " seconds (" + msSinceLastReply + " milliseconds) since last ping reply!"; LOGGER.warning(msg); if (secsSinceReply >= PING_REQUEST_INTERVAL_SEC * 5) { LOGGER.severe("Long time no ping replies from client " + client.getClientName() + "; should close connection now."); } } } } } void allTellGameOver(String message, boolean disposeFollows, boolean suspended) { for (IClient client : iClients) { client.tellGameOver(message, disposeFollows, suspended); } } /** Needed if loading game outside the split phase. */ void allSetupTurnState() { for (IClient client : iClients) { client .setupTurnState(game.getActivePlayer(), game.getTurnNumber()); } } void allSetupSplit() { for (IClient client : iClients) { client.setupSplit(game.getActivePlayer(), game.getTurnNumber()); } allUpdatePlayerInfo("AllSetupSplit"); } void allSetupMove() { for (IClient client : iClients) { client.setupMove(); } } void allSetupFight() { for (IClient client : iClients) { client.setupFight(); } } void allSetupMuster() { for (IClient client : iClients) { client.setupMuster(); } } void kickPhase() { // XXX TODO Should do only for the active Client! for (IClient client : iClients) { client.kickPhase(); } } void allSetupBattleSummon() { BattleServerSide battle = game.getBattleSS(); for (IClient client : iClients) { client.setupBattleSummon(battle.getBattleActivePlayer(), battle.getBattleTurnNumber()); } } void allSetupBattleRecruit() { BattleServerSide battle = game.getBattleSS(); for (IClient client : iClients) { client.setupBattleRecruit(battle.getBattleActivePlayer(), battle.getBattleTurnNumber()); } } void allSetupBattleMove() { BattleServerSide battle = game.getBattleSS(); for (IClient client : iClients) { client.setupBattleMove(battle.getBattleActivePlayer(), battle.getBattleTurnNumber()); } } void allSetupBattleFight() { BattleServerSide battle = game.getBattleSS(); for (IClient client : iClients) { if (battle != null) { client.setupBattleFight(battle.getBattlePhase(), battle.getBattleActivePlayer()); } } } void allPlaceNewChit(CreatureServerSide critter) { boolean inverted = critter.getLegion().equals( game.getBattleSS().getDefendingLegion()); for (IClient client : iClients) { client.placeNewChit(critter.getName(), inverted, critter.getTag(), critter.getCurrentHex()); } } void allRemoveDeadBattleChits() { for (IClient client : iClients) { client.removeDeadBattleChits(); } } void allTellEngagementResults(Legion winner, String method, int points, int turns) { for (IClient client : iClients) { client.tellEngagementResults(winner, method, points, turns); } } void nextEngagement() { IClient client = getClient(game.getActivePlayer()); client.nextEngagement(); } /** Find out if the player wants to acquire an angel or archangel. */ void askAcquireAngel(PlayerServerSide player, Legion legion, List<CreatureType> recruits) { if (legion.getHeight() < 7) { IClient client = getClient(player); if (client != null) { client.askAcquireAngel(legion, recruits); } } } public void acquireAngel(Legion legion, CreatureType angelType) { if (legion != null) { if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called acquireAngel(): player " + getPlayer().getName() + " is not owner of legion " + legion.getMarkerId()); return; } ((LegionServerSide)legion).addAngel(angelType); } } void createSummonAngel(Legion legion) { IClient client = getClient(legion.getPlayer()); client.createSummonAngel(legion); } void reinforce(Legion legion) { IClient client = getClient(legion.getPlayer()); client.doReinforce(legion); } public void doSummon(Summoning event) { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doSummon(): not battle active player"); return; } game.doSummon(event); } /** * Handle mustering for legion. * if recruiting with nothing, recruiterName is a non-null String * that contains "null". */ public void doRecruit(Recruitment event) { IClient client = getClient(getPlayer()); // we can't do the "return" inside the if blocks, because then we miss // the doneReinforcing at the end... // E.g. SimpleAI tried to muster after being attacked, won, acquired // angel (=> legion full) => canRecruit false => "illegal recruit". // => game hangs. Legion legion = event.getLegion(); CreatureType recruit = event.getAddedCreatureType(); CreatureType recruiter = event.getRecruiter(); String recruiterName = (recruiter == null) ? null : recruiter .getName(); if (legion == null) { LOGGER.warning(getPlayerName() + " illegally called doRecruit(): null legion"); client.nak(Constants.doRecruit, "Can't recruit: Null legion"); } else if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called doRecruit(): does not own legion " + legion.getMarkerId()); client.nak(Constants.doRecruit, "Can't recruit: Wrong player"); } else { // Deny, because legion as by itself cannot recruit if (!((LegionServerSide)legion).canRecruit()) { String reason = ((LegionServerSide)legion) .cantRecruitBecause(); LOGGER.warning("Illegal legion " + legion + " (height=" + legion.getHeight() + ") for recruit: " + recruit.getName() + " recruiterName " + recruiterName + "; reason: " + reason); client.nak(Constants.doRecruit, "Illegal recruit, reason: " + reason); } else if (legion.hasMoved() || game.getPhase() == Phase.FIGHT) { ((LegionServerSide)legion).sortCritters(); if (recruit != null) { // TODO pass event in game.doRecruit(legion, recruit, recruiter); } if (legion.getRecruit() != null) { LOGGER.finest("OK, getRecruit() confirms we did recruit"); didRecruit(event, recruiter); } else { // happens at least then when player declined the recruit } } else { LOGGER.warning("Illegal recruit (not moved, not in battle) " + "with legion " + legion.getMarkerId() + " recruit: " + recruit.getName() + " recruiterName " + recruiterName); client.nak(Constants.doRecruit, "Illegal recruit, reason: not moved & not in battle"); } } // Need to always call this to keep game from hanging. if (game.getPhase() == Phase.FIGHT) { if (game.getBattleSS() != null) { game.getBattleSS().doneReinforcing(); } else { game.doneReinforcing(); } } } // TODO should use RecruitEvent void didRecruit(AddCreatureAction event, CreatureType recruiter) { allUpdatePlayerInfo("DidRecruit"); int numRecruiters = (recruiter == null ? 0 : TerrainRecruitLoader .numberOfRecruiterNeeded(recruiter, event.getAddedCreatureType(), event.getLegion().getCurrentHex().getTerrain(), event .getLegion().getCurrentHex())); Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); // TODO pass event around client.didRecruit(event.getLegion(), event.getAddedCreatureType(), recruiter, numRecruiters); } // reveal only if there is something to tell if (recruiter != null) { List<CreatureType> recruiters = new ArrayList<CreatureType>(); for (int i = 0; i < numRecruiters; i++) { recruiters.add(recruiter); } game.revealEvent(true, null, event.getLegion(), recruiters, Constants.reasonRecruiter); } game.addCreatureEvent(event, Constants.reasonRecruited); } void undidRecruit(Legion legion, CreatureType recruit, boolean reinforced) { allUpdatePlayerInfo("UndidRecruit"); Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.undidRecruit(legion, recruit); } game.undoRecruitEvent(legion); String reason = reinforced ? Constants.reasonReinforced : Constants.reasonRecruited; game.removeCreatureEvent(legion, recruit, reason); } public void engage(MasterHex hex) { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called engage(): not active player"); return; } game.engage(hex); } void allTellEngagement(MasterHex hex, Legion attacker, Legion defender) { LOGGER.finest("allTellEngagement() " + hex); Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellEngagement(hex, attacker, defender); } } /** Ask ally's player whether he wants to concede with ally. */ void askConcede(Legion ally, Legion enemy) { IClient client = getClient(ally.getPlayer()); client.askConcede(ally, enemy); } public void concede(Legion legion) { // Should not happen but at least once did - legion was just // eliminated and player still conceded? if (legion == null) { LOGGER.warning(getPlayerName() + " illegally called concede(): null legion!"); return; } // TODO the next line can throw NPEs when quitting the game if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called concede(): does not own legion " + legion.getMarkerId()); return; } game.concede(legion); } public void doNotConcede(Legion legion) { // Can this happen? Just to be sure, similar as in Concede(legion). // Should not happen but at least once did - legion was just // eliminated and player still conceded? if (legion == null) { LOGGER.warning(getPlayerName() + " illegally called doNotconcede(): null legion!"); return; } if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called doNotConcede(): does not own legion " + legion.getMarkerId()); return; } game.doNotConcede(legion); } /** Ask ally's player whether he wants to flee with ally. */ void askFlee(Legion ally, Legion enemy) { IClient client = getClient(ally.getPlayer()); client.askFlee(ally, enemy); } public void flee(Legion legion) { if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called flee(): " + "does not own legion " + legion.getMarkerId()); return; } game.flee(legion); } public void doNotFlee(Legion legion) { if (!getPlayer().equals(legion.getPlayer())) { LOGGER.warning(getPlayerName() + " illegally called doNotFlee(): " + " does not own legion " + legion.getMarkerId()); return; } game.doNotFlee(legion); } void twoNegotiate(Legion attacker, Legion defender) { IClient client1 = getClient(defender.getPlayer()); client1.askNegotiate(attacker, defender); IClient client2 = getClient(attacker.getPlayer()); client2.askNegotiate(attacker, defender); } /** playerName makes a proposal. */ public void makeProposal(String proposalString) { // TODO Validate calling player game.makeProposal(getPlayerName(), proposalString); } /** Tell playerName about proposal. */ void tellProposal(Player player, Proposal proposal) { IClient client = getClient(player); client.tellProposal(proposal.toString()); } public void fight(MasterHex hex) { // TODO Validate calling player game.fight(hex); } public void doBattleMove(int tag, BattleHex hex) { if (hex == null) { LOGGER.info("Ignoring doBattleMove for hex==null - probably a " + "delayed msg related to a recently finished battle"); return; } IClient client = getClient(getPlayer()); if (!isBattleActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doBattleMove(): " + "not battle active player"); client.nak(Constants.doBattleMove, "Wrong player"); return; } String reasonFail = game.getBattleSS().doMove(tag, hex); if (reasonFail != null) { if (processingCH.canHandleBattleMoveNak()) { LOGGER.info("Battle move failed - giving Client a nak " + "doBattleMove (illegal move)"); client.nak(Constants.doBattleMove, "Illegal move: " + reasonFail); } else { LOGGER.info("Battle move failed - skipping the nak for " + "doBattleMove (illegal move) because client can't " + "handle it"); } } } void allTellBattleMove(int tag, BattleHex startingHex, BattleHex endingHex, boolean undo) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellBattleMove(tag, startingHex, endingHex, undo); } } public void strike(int tag, BattleHex hex) { IClient client = getClient(getPlayer()); if (!isBattleActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called strike(): " + "not battle active player"); client.nak(Constants.strike, "Wrong player"); return; } BattleServerSide battle = game.getBattleSS(); if (battle == null) { LOGGER.severe("null battle in Server.strike()"); client.nak(Constants.strike, "No battle"); return; } LegionServerSide legion = battle.getActiveLegion(); if (legion == null) { LOGGER.severe("null active legion in Server.strike()"); client.nak(Constants.strike, "No active legion"); return; } CreatureServerSide critter = legion.getCritterByTag(tag); if (critter == null) { LOGGER .severe("No critter with tag " + tag + " in Server.strike()"); client.nak(Constants.strike, "No critter with that tag"); return; } CreatureServerSide strikeTarget = battle.getCreatureSS(hex); if (strikeTarget == null) { LOGGER.severe("No target in hex " + hex.getLabel() + " in Server.strike()"); client.nak(Constants.strike, "No target in that hex"); return; } if (strikeTarget.getPlayer() == critter.getPlayer()) { LOGGER.severe(critter.getDescription() + " tried to strike allied " + strikeTarget.getDescription()); client.nak(Constants.strike, "Target is friendly"); return; } if (critter.hasStruck()) { LOGGER.severe(critter.getDescription() + " tried to strike twice"); client.nak(Constants.strike, "Critter already struck"); return; } critter.strike(strikeTarget); } public void applyCarries(BattleHex hex) { if (!isBattleActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called applyCarries(): : " + "not battle active player"); return; } BattleServerSide battle = game.getBattleSS(); CreatureServerSide ourTarget = battle.getCreatureSS(hex); battle.applyCarries(ourTarget); } public void undoBattleMove(BattleHex hex) { if (!isBattleActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called undoBattleMove(): " + "not battle active player"); return; } game.getBattleSS().undoMove(hex); } void allTellStrikeResults(CreatureServerSide striker, CreatureServerSide target, int strikeNumber, List<String> rolls, int damage, int carryDamageLeft, Set<String> carryTargetDescriptions) { // Save strike info so that it can be reused for carries. this.striker = striker; this.target = target; this.strikeNumber = strikeNumber; this.rolls = rolls; Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellStrikeResults(striker.getTag(), target.getTag(), strikeNumber, rolls, damage, target.isDead(), false, carryDamageLeft, carryTargetDescriptions); } if (game.getDiceStatCollector() != null) { if (!game.getOption(Options.pbBattleHits)) { game.getDiceStatCollector().addOneSet(game.getTurnNumber(), game.getBattleTurnNumber(), striker, target, strikeNumber, rolls); } } } void allTellCarryResults(CreatureServerSide carryTarget, int carryDamageDone, int carryDamageLeft, Set<String> carryTargetDescriptions) { if (striker == null || target == null || rolls == null) { LOGGER.severe("Called allTellCarryResults() without setup."); if (striker == null) { LOGGER.severe("null striker"); } if (target == null) { LOGGER.severe("null target"); } if (rolls == null) { LOGGER.severe("null rolls"); } return; } Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellStrikeResults(striker.getTag(), carryTarget.getTag(), strikeNumber, rolls, carryDamageDone, carryTarget.isDead(), true, carryDamageLeft, carryTargetDescriptions); } } void allTellHexSlowResults(CreatureServerSide target, int slowValue) { this.target = target; Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellSlowResults(target.getTag(), slowValue); } } void allTellHexDamageResults(CreatureServerSide target, int damage) { this.target = target; Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.tellStrikeResults(Constants.HEX_DAMAGE, target.getTag(), 0, null, damage, target.isDead(), false, 0, null); } } /** Takes a Set of PenaltyOptions. */ void askChooseStrikePenalty(SortedSet<PenaltyOption> penaltyOptions) { Player player = game.getBattleSS().getBattleActivePlayer(); IClient client = getClient(player); List<String> choices = new ArrayList<String>(); Iterator<PenaltyOption> it = penaltyOptions.iterator(); while (it.hasNext()) { PenaltyOption po = it.next(); striker = (CreatureServerSide)po.getStriker(); choices.add(po.toString()); } client.askChooseStrikePenalty(choices); } public void assignStrikePenalty(String prompt) { if (!isBattleActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called assignStrikePenalty(): " + "not battle active player"); getClient(getPlayer()).nak(Constants.assignStrikePenalty, "Wrong player"); } else if (striker.hasStruck()) { LOGGER.warning(getPlayerName() + " illegally called assignStrikePenalty(): " + "already struck"); getClient(getPlayer()).nak(Constants.assignStrikePenalty, "Critter already struck"); } else { striker.assignStrikePenalty(prompt); } } void allInitBattle(MasterHex masterHex) { BattleServerSide battle = game.getBattleSS(); Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.initBattle(masterHex, battle.getBattleTurnNumber(), battle.getBattleActivePlayer(), battle.getBattlePhase(), battle.getAttackingLegion(), battle.getDefendingLegion()); } } void allCleanupBattle() { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.cleanupBattle(); } } public void mulligan() { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called mulligan(): " + "not active player"); return; } int roll = game.mulligan(); LOGGER.finest("Player " + getPlayerName() + " took a mulligan and rolled " + roll); } public void requestExtraRoll() { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally requested extra roll: " + "not active player"); return; } if (!game.isPhase(Phase.MOVE)) { LOGGER.warning(getPlayerName() + " illegally requested extra roll: " + "not movement phase"); return; } extraRollRequest.handleExtraRollRequest(processingCH); } public void extraRollResponse(boolean approved, int requestId) { extraRollRequest.handleExtraRollResponse(requestId, processingCH, approved); } public void requestToSuspendGame(boolean save) { saveBeforeSuspend = save; suspendGameRequest.requestToSuspendGame(); } public void suspendResponse(boolean approved) { suspendGameRequest.handleOneResponse(approved); } public void messageFromServerToAll(String message) { for (ClientHandler client : realClients) { client.messageFromServer(message); } } public void undoSplit(Legion splitoff) { if (!isActivePlayer()) { return; } getActivePlayerSS().undoSplit(splitoff); } void undidSplit(Legion splitoff, Legion survivor, boolean updateHistory, int turn) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.undidSplit(splitoff, survivor, turn); } if (updateHistory) { game.mergeEvent(splitoff.getMarkerId(), survivor.getMarkerId()); } } public void undoMove(Legion legion) { if (!isActivePlayer()) { return; } game.undoMove(legion); } public void allTellUndidMove(Legion legion, MasterHex formerHex, MasterHex currentHex, boolean splitLegionHasForcedMove) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.undidMove(legion, formerHex, currentHex, splitLegionHasForcedMove); } } public void undoRecruit(Legion legion) { if (!isActivePlayer()) { return; } getActivePlayerSS().undoRecruit(legion); } public void doneWithSplits() { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally (wrong player) called " + "doneWithSplits() - active player is " + game.getActivePlayer().getName()); getClient(getPlayer()).nak(Constants.doneWithSplits, "Wrong player"); } else if (game.getTurnNumber() == 1 && game.getActivePlayer().getLegions().size() == 1) { getClient(getPlayer()).nak(Constants.doneWithSplits, "Must split on first turn"); } else { game.advancePhase(Phase.SPLIT, getPlayer()); } } public void doneWithMoves() { PlayerServerSide player = getActivePlayerSS(); if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doneWithMoves(): not active player"); getClient(getPlayer()) .nak(Constants.doneWithMoves, "Wrong player"); } // If any legion has a legal non-teleport move, then // the player must move at least one legion. else if (player.legionsMoved() == 0 && player.countMobileLegions() > 0) { LOGGER.finest("At least one legion must move."); getClient(getPlayer()).nak(Constants.doneWithMoves, "Must move at least one legion"); } // If legions share a hex and have a legal // non-teleport move, force one of them to take it. else if (player.splitLegionHasForcedMove()) { LOGGER.finest("Split legions must be separated."); getClient(getPlayer()).nak(Constants.doneWithMoves, "Must separate split legions"); } // Otherwise, recombine all split legions still in // the same hex, and move on to the next phase. else { player.recombineIllegalSplits(); game.advancePhase(Phase.MOVE, getPlayer()); } } public void doneWithEngagements() { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doneWithEngagements(): " + "not active player"); getClient(getPlayer()).nak(Constants.doneWithEngagements, "Wrong player"); } // Advance only if there are no unresolved engagements. else if (game.findEngagements().size() > 0) { getClient(getPlayer()).nak(Constants.doneWithEngagements, "Must resolve engagements"); } else { game.advancePhase(Phase.FIGHT, getPlayer()); } } public void doneWithRecruits() { if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doneWithRecruits(): not active player"); getClient(getPlayer()).nak(Constants.doneWithRecruits, "Wrong player"); } else { PlayerServerSide player = getActivePlayerSS(); player.commitMoves(); // Mulligans are only allowed on turn 1. if (!game.getOption(Options.unlimitedMulligans)) { player.setMulligansLeft(0); } game.advancePhase(Phase.MUSTER, getPlayer()); } } public boolean isWithdrawalIrrelevant() { return (obsolete || game == null || game.isGameOver() || processingCH .isSpectator()); } /** Withdraw the player for which data was currently processed on socket * (if it is a real one, and withdrawal still makes sense). */ public void withdrawFromGame() { LOGGER.info("Withdrawal for processing client " + processingCH.getClientName() + " requested."); if (isWithdrawalIrrelevant()) { LOGGER.finest("No need for withdraw - game over etc."); return; } // spectators or rejected clients: (can this still happen? Rejects?) if (getPlayerName() == null) { return; } Player player = getPlayer(); if (player == null) { LOGGER.severe("Got null player for playerName '" + getPlayerName() + "' - skipping handlePlayerWithdrawal."); } else { game.handlePlayerWithdrawal(player); } } /** Withdraw a specific player of which we know only the name; e.g. * when one clientHandler when trying to write to another clientHandler * encountered closed socket. * @param playerName Name of the player to withdraw */ public void withdrawFromGame(String playerName) { LOGGER.info("Withdrawal for specific player " + playerName + " requested."); if (isWithdrawalIrrelevant()) { LOGGER.finest("No need for withdraw - game over etc."); return; } // spectators or rejected clients: (can this still happen? Rejects?) if (playerName == null) { LOGGER.finest("No need for withdraw - null player or spectator."); return; } Player player = game.getPlayerByName(playerName); if (player != null) { LOGGER.finest("Doing game.handlePlayerWithdrawal for " + playerName); game.handlePlayerWithdrawal(player); } else { LOGGER.warning("Can't do game.handlePlayerWithdrawal for " + playerName + " because getPlayerByName gave null player!"); } } // client will dispose itself soon, // do not attempt to further read from there. public void sendDisconnect() { queueClientHandlerForChannelChanges(processingCH); clientWontConfirmCatchup(processingCH, "Client disconnected."); } public void stopGame() { if (game != null) { game.dispose(); } } void triggerDispose() { LOGGER.info("triggerDispose() : setting initiateDisposal flag."); initiateDisposal = true; } private List<String> getPlayerInfo(boolean treatDeadAsAlive) { List<String> info = new ArrayList<String>(game.getNumPlayers()); for (Player player : game.getPlayers()) { String longString = ((PlayerServerSide)player) .getStatusInfo(treatDeadAsAlive); info.add(longString); } return info; } /** * Returns a list with strings, one string for each player where data * has changed since last request. String contains only the frequently * changing values, e.g. not color, tower and player type, and others * (compared to the "full" form) which were never really used for update * (titanpower, legioncount, creaturecount) are omitted as well. * * @return List of strings, one for each player with changes */ private List<String> getChangedPlayerValues() { List<String> changes = new ArrayList<String>(game.getNumPlayers()); for (Player player : game.getPlayers()) { PlayerServerSide p = (PlayerServerSide)player; String changedValues = p.getValuesIfChanged(); if (!changedValues.equals("")) { changes.add(changedValues); } } return changes; } public void doSplit(Legion parent, String childId, List<CreatureType> creaturesToSplit) { LOGGER.log(Level.FINER, "Server.doSplit " + parent + " " + childId + " " + Glob.glob(",", creaturesToSplit)); IClient client = getClient(getPlayer()); if (!isActivePlayer()) { LOGGER.warning(getPlayerName() + " illegally called doSplit() " + "- activePlayer is " + game.getActivePlayer()); client.nak(Constants.doSplit, "Cannot split: Wrong player " + "(active player is " + game.getActivePlayer()); return; } if (!game.doSplit(parent, childId, creaturesToSplit)) { LOGGER.warning(getPlayerName() + " tried split for " + parent + ", failed!"); client.nak(Constants.doSplit, "Illegal split / Split failed!"); } } /** Called from game after this legion was split off, or by history */ void allTellDidSplit(Legion parent, Legion child, int turn, boolean history) { MasterHex hex = parent.getCurrentHex(); int childSize = child.getHeight(); LOGGER.log(Level.FINER, "Server.didSplit " + hex + " " + parent + " " + child + " " + childSize); allUpdatePlayerInfo("DidSplit"); IClient activeClient = getClient(game.getActivePlayer()); List<CreatureType> splitoffs = child.getCreatureTypes(); activeClient.didSplit(hex, parent, child, childSize, splitoffs, turn); if (history) { game.splitEvent(parent, child, splitoffs); } if (!game.getOption(Options.allStacksVisible)) { splitoffs.clear(); } Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); if (client != activeClient) { client .didSplit(hex, parent, child, childSize, splitoffs, turn); } } } public void doMove(Legion legion, MasterHex hex, EntrySide entrySide, boolean teleport, CreatureType teleportingLord) { IClient client = getClient(getPlayer()); // Check for "is it the right player", but not during replay / redo if (!game.isReplayOngoing() && !isActivePlayer()) { LOGGER.severe(getPlayerName() + " illegally called doMove() for legion " + legion.getMarkerId() + " to hex " + hex.getLabel() + ": not active player"); client.nak(Constants.doMove, "Wrong player"); return; } MasterHex startingHex = legion.getCurrentHex(); String reasonFail = game.doMove(legion, hex, entrySide, teleport, teleportingLord); if (reasonFail == null) { allTellDidMove(legion, startingHex, hex, entrySide, teleport, teleportingLord); } else { LOGGER.severe(getPlayerName() + " tried to move legion " + legion.getMarkerId() + " from " + startingHex + " to " + hex + " (entryside " + entrySide.getLabel() + ", teleport " + teleport + ", lord " + teleportingLord + "): move failed, reason " + reasonFail); client.nak(Constants.doMove, "Illegal move: " + reasonFail); } } void allTellDidMove(Legion legion, MasterHex startingHex, MasterHex hex, EntrySide entrySide, boolean teleport, CreatureType teleportingLord) { PlayerServerSide player = getActivePlayerSS(); // needed in didMove to decide whether to dis/enable button boolean splitLegionHasForcedMove = player.splitLegionHasForcedMove(); Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.didMove(legion, startingHex, hex, entrySide, teleport, teleportingLord, splitLegionHasForcedMove); } } void allTellDidSummon(Legion receivingLegion, Legion donorLegion, CreatureType summon) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.didSummon(receivingLegion, donorLegion, summon); } } void allTellAddCreature(AddCreatureAction event, boolean updateHistory, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); // TODO pass event into client (requires adding the reason as property of the event) client.addCreature(event.getLegion(), event.getAddedCreatureType(), event.getReason()); } if (updateHistory) { game.addCreatureEvent(event, reason); } } void allTellRemoveCreature(Legion legion, CreatureType creature, boolean updateHistory, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.removeCreature(legion, creature, reason); } if (updateHistory) { game.removeCreatureEvent(legion, creature, reason); } } void allRevealLegion(Legion legion, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.revealCreatures(legion, legion.getCreatureTypes(), reason); } game.revealEvent(true, null, legion, legion.getCreatureTypes(), reason); } /** pass to all clients the 'revealEngagedCreatures' message, * then fire an 'revealEvent' to the history. * @author Towi, copied from allRevealLegion * @param legion the legion marker to reveal which is in a battle * @param isAttacker true if the 'legion' is the atackker in the * battle, false for the defender. */ void allRevealEngagedLegion(final Legion legion, final boolean isAttacker, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.revealEngagedCreatures(legion, legion.getCreatureTypes(), isAttacker, reason); } game.revealEvent(true, null, legion, legion.getCreatureTypes(), reason); } /** Call from History during load game only */ void allRevealLegion(Legion legion, List<CreatureType> creatures, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.revealCreatures(legion, creatures, reason); } } void oneRevealLegion(Legion legion, Player player, String reason) { IClient client = getClient(player); if (client != null) { client.revealCreatures(legion, legion.getCreatureTypes(), reason); } List<Player> li = new ArrayList<Player>(); li.add(player); game.revealEvent(false, li, legion, legion.getCreatureTypes(), reason); } /** Call from History during load game only */ void oneRevealLegion(Player player, Legion legion, List<CreatureType> creatureNames, String reason) { IClient client = getClient(player); if (client != null) { client.revealCreatures(legion, creatureNames, reason); } } void oneUpdateLegionStatus(IClient client) { for (Legion legion : game.getAllLegions()) { client.setLegionStatus(legion, legion.hasMoved(), legion.hasTeleported(), legion.getEntrySide(), legion.getRecruit()); } } void allFullyUpdateLegionStatus() { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); if (client != null) { oneUpdateLegionStatus(client); } } } void allFullyUpdateAllLegionContents(String reason) { for (Legion legion : game.getAllLegions()) { allRevealLegion(legion, reason); } } void allRevealCreatures(Legion legion, List<CreatureType> creatureNames, String reason) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.revealCreatures(legion, creatureNames, reason); } game.revealEvent(true, null, legion, creatureNames, reason); } // XXX Disallow these in network games? public void newGame() { // Nothing special to do, just stop everything and return // back to the main, so that it goes to top of loop and // brings up a new GetPlayers dialog. } public void loadGame(String filename) { LOGGER.warning("Got from client via socket the request to load a game" + ", but 'via Socket' is not supported any more! Ignoring it."); } // This was earlier called from Client via network message // TODO: to be perhaps removed soon? See also SocketClientThread public void saveGame(String filename) { saveGame(filename, false); } public void saveGame(String filename, boolean autoSave) { game.saveGameWithErrorHandling(filename, autoSave); } // User has requested to save game via File=>Save Game or Save Game as... // Inject that into the "handle incoming messages from clients" select // loop, to be sure it does not run concurrently while some message // from client is currently processed and would change the state of the // game while the save is ongoing. public void initiateSaveGame(String filename) { synchronized (guiRequestMutex) { guiRequestSaveFlag = true; guiRequestSaveFilename = filename; selector.wakeup(); } } public void initiateQuitGame() { synchronized (guiRequestMutex) { guiRequestQuitFlag = true; selector.wakeup(); } } public void initiateSuspendGame() { if (saveBeforeSuspend) { game.saveGameWithErrorHandling("null", false); saveBeforeSuspend = false; } game.handleSuspend(); LOGGER.info("In server: initiateSuspendGame"); synchronized (guiRequestMutex) { suspendFlag = true; forceShutDown = true; LOGGER .info("In server: suspendFlag is now true, forceShutdown also"); selector.wakeup(); } } public void setPauseState(boolean newState) { synchronized (guiRequestMutex) { if (newState == inPauseState) { return; } inPauseState = newState; if (inPauseState) { // Just did set it to true, get the selector thread out of // select(), if necessary selector.wakeup(); } else { // Flag was cleared to end the pause guiRequestMutex.notify(); } } } /** * Handle GUI-initiated requests: Save and Pause * @return true if it did something (saving the game) */ public boolean handleGuiRequests() { boolean didSomething = false; synchronized (guiRequestMutex) { if (guiRequestSaveFlag) { game.saveGameWithErrorHandling(guiRequestSaveFilename, false); guiRequestSaveFlag = false; guiRequestSaveFilename = null; didSomething = true; } else if (guiRequestQuitFlag) { if (game != null) { game.dispose(); } didSomething = true; } else if (suspendFlag) { LOGGER.info("The 'suspend' flag was set. OK!"); if (game != null) { LOGGER.fine("in handle gui requests, " + "suspendFlag set: calling triggerdispose"); game.handleSuspend(); triggerDispose(); } else { LOGGER .warning("NOT calling triggerdispose, no game ?!?!?"); } forceShutDown = true; didSomething = true; } else if (inPauseState) { while (inPauseState) { try { guiRequestMutex.wait(); } catch (InterruptedException e) { LOGGER.warning("InterruptedException while waiting " + "on mutes in suspend-ongoing-state!"); } } } } return didSomething; } public void checkServerConnection() { LOGGER.info("Server received checkServerConnection request from " + "client " + getPlayerName() + " - sending confirmation."); processingCH.serverConfirmsConnection(); if (Constants.USE_RECORDER) { recorder.printMessagesToConsole(processingCH); } } public void checkAllConnections(String requestingClientName) { LOGGER.info("Server received checkAllConnections request from " + "client " + getPlayerName() + " - ...."); // processingCH.serverConfirmsConnection(); // if (Constants.USE_RECORDER) // { // recorder.printMessagesToConsole(processingCH); // } Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); ClientHandler reqCH = getClientHandlerByName(requestingClientName); if (reqCH != client) { client.relayedPeerRequest(requestingClientName); } } } public void peerRequestReceived(String requestingClientName, int queueLen) { ClientHandler reqCH = getClientHandlerByName(requestingClientName); if (reqCH != null) { reqCH.peerRequestReceivedBy(getPlayerName(), queueLen); } else { LOGGER.severe("peerReqReceived: reqCH is null?"); } } public void peerRequestProcessed(String requestingClientName) { ClientHandler reqCH = getClientHandlerByName(requestingClientName); if (reqCH != null) { reqCH.peerRequestProcessedBy(getPlayerName()); } else { LOGGER.severe("peerReqProcessed: reqCH is null?"); } } private final HashSet<IClient> waitingToCatchup = new HashSet<IClient>(); /** * Check whether client is currently expected to send a caught-Up * confirmation. * If yes: it won't happen, so act accordingly. * If no : even better so, so just do nothing. * @param reason Reason why client won't send the confirmation * (typically disconnected or something). */ public void clientWontConfirmCatchup(ClientHandler ch, String reason) { String clientName = ch.getClientName(); synchronized (waitingToCatchup) { if (waitingToCatchup.contains(ch)) { waitingToCatchup.remove(ch); int remaining = waitingToCatchup.size(); LOGGER.info("Client " + clientName + " won't confirm catch-up (" + reason + "). Remaining: " + remaining); if (remaining <= 0) { actOnAllCaughtUp(); } } } } public void clientConfirmedCatchup() { ClientHandler ch = processingCH; String clientName = ch.getClientName(); synchronized (waitingToCatchup) { if (waitingToCatchup.contains(ch)) { waitingToCatchup.remove(ch); } else { LOGGER.warning("Client for " + clientName + " not found from waitingForCatchup list!"); } int remaining = waitingToCatchup.size(); LOGGER.info("Client " + clientName + " confirmed catch-up. " + "Remaining: " + remaining); if (remaining <= 0) { actOnAllCaughtUp(); } } } private void actOnAllCaughtUp() { if (caughtUpAction.equals("KickstartGame")) { LOGGER.info("All caught up - doing game.kickstartGame()"); game.kickstartGame(); } else if (caughtUpAction.equals("DisposeGame")) { LOGGER.info("All caught up - doing game.dispose()"); game.dispose(); } else { LOGGER.severe("All clients caught up, but no action set??"); } } private String prettyTime(long when) { if (when == 0L) { return "n/a"; } return new SimpleDateFormat("HH:mm:ss.SSS").format(new Date(when)); } void replyToPing(String playerName, int requestNr, long requestSent, long replySent, long replyReceived) { LOGGER.fine("Ping Reply #" + requestNr + " from " + playerName + ": " + prettyTime(requestSent) + "/" + prettyTime(replySent) + "/" + prettyTime(replyReceived)); } /** Used to change a player name after color is assigned. */ void setPlayerName(Player player, String newName) { LOGGER.finest("Server.setPlayerName() from " + player.getName() + " to " + newName); IClient client = getClient(player); client.setPlayerName(newName); } void askPickColor(Player player, final List<PlayerColor> colorsLeft) { IClient activeClient = getClient(player); for (IClient client : iClients) { if (client != null && client != activeClient) { client.tellWhatsHappening("(Player " + player.getName() + " picks color)"); } } // Do this after loop, so that chances are better that this one // is active/in top at the end. if (activeClient != null) { activeClient.askPickColor(colorsLeft); } } public void assignColor(PlayerColor color) { Player p = getPlayer(); assert p != null : "getPlayer returned null player (in thread " + Thread.currentThread().getName() + ")"; if (!getPlayer().equals(game.getNextColorPicker())) { LOGGER .warning(getPlayerName() + " illegally called assignColor()"); return; } if (getPlayer() == null || getPlayer().getColor() == null) { game.assignColor(getPlayer(), color); } } void askPickFirstMarker(Player player) { IClient activeClient = getClient(player); for (IClient client : iClients) { if (client != null && client != activeClient) { client.tellWhatsHappening("(Player " + player.getName() + " picks initial marker)"); } } // Do this after loop, so that chances are better that this one // is active/in top at the end. if (activeClient != null) { activeClient.askPickFirstMarker(); } } public void assignFirstMarker(String markerId) { Player player = game.getPlayerByName(getPlayerName()); assert player.getMarkersAvailable().contains(markerId) : getPlayerName() + " illegally called assignFirstMarker()"; ((PlayerServerSide)player).setFirstMarker(markerId); game.nextPickColor(); } /** Hack to set color on load game. */ void allSetColor() { for (Player player : game.getPlayers()) { PlayerColor color = player.getColor(); IClient client = getClient(player); if (client != null) { client.setColor(color); } } } // XXX We use Server as a hook for PhaseAdvancer to get to options, // but this is ugly. int getIntOption(String optname) { return game.getIntOption(optname); } void oneSetOption(Player player, String optname, String value) { IClient client = getClient(player); if (client != null) { client.syncOption(optname, value); } } void oneSetOption(Player player, String optname, boolean value) { oneSetOption(player, optname, String.valueOf(value)); } void allSyncOption(String optname, String value) { Iterator<IClient> it = iClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.syncOption(optname, value); } } void allSyncOption(String optname, boolean value) { allSyncOption(optname, String.valueOf(value)); } void allSyncOption(String optname, int value) { allSyncOption(optname, String.valueOf(value)); } /** DO NOT USE: * package so that it can be called from Log4J Appender. * */ void allLog(String message) { Iterator<IClient> it = remoteClients.iterator(); while (it.hasNext()) { IClient client = it.next(); client.log(message); } } void logToStartLog(String message) { if (startLog != null) { startLog.append(message); } } // To disable the auto close when warnings where displayed private void disableAutoCloseStartupLog() { if (startLog != null) { startLog.disableAutoClose(); } } /* * Called by GameServerSide, to initiate the "Quit All" */ public void doSetWhatToDoNext(WhatToDoNext whatToDoNext, boolean triggerQuitTimer) { whatNextManager.setWhatToDoNext(whatToDoNext, triggerQuitTimer); } /* Called from ServerStartupProgress, if user wants to cancel during load * (e.g. one client did not come in). Clean up everything and get * back to the GetPlayers / Game Setup dialog. */ public void startupProgressAbort() { whatNextManager .setWhatToDoNext(WhatToDoNext.GET_PLAYERS_DIALOG, false); stopServerRunning(); if (startLog != null) { startLog.dispose(); startLog.cleanRef(); } } /* if the startupProgressAbort did not succeed well, the button * there changes to a QUIT button, and with that one can request * the whole application */ public void startupProgressQuit() { LOGGER.severe("User pressed 'QUIT' in startupProgress window " + "- doing System.exit(1)"); System.exit(1); } public GameServerSide getGame() { return game; } public MessageRecorder getRecorder() { return recorder; } public void replyToRequestGameInfo() { processingCH.tellInitialGameInfo(game.getVariant().getName(), getGame().getPreliminaryPlayerNames()); } public void requestSyncDelta(int lastReceivedMessageNr, int syncRequestNumber) { LOGGER.info("Client requests sync #" + syncRequestNumber + " after reconnect, last msg nr was " + lastReceivedMessageNr); processingCH.syncAfterReconnect(lastReceivedMessageNr, syncRequestNumber); } private boolean beelzeGodOk() { if (game.getVariant().getName().equals("BeelzeGods12")) { if (!processingCH.canHandleNewVariantXML()) { LOGGER.severe("Client " + processingCH.getClientName() + " is too old for new BG12!"); if (startLog != null) { LOGGER.severe("Calling startLog to inform " + "the hosting player."); startLog.tooOldClient(processingCH.getClientName()); } else { LOGGER.severe("No startLog - aborting game " + "start right away!"); startupProgressAbort(); } return false; } } return true; } public void joinGame(String playerName) { if (!beelzeGodOk()) { return; } addIClient(processingCH); addRealClient(processingCH); // @TODO: move to outside Select loop // => notify main thread to do this? /** * Decrement the number of players we wait for to join by one in a * thread-safe way, and return the new value. * * @return The number of players that still need to join */ int stillMissing; synchronized (wfptjSemaphor) { --waitingForPlayersToJoin; stillMissing = waitingForPlayersToJoin; } if (stillMissing == 0) { LOGGER .info("waitingForPlayersToJoin now zero - calling startGame."); startGame(); } else { LOGGER.info("waitingForPlayersToJoin now " + stillMissing); } } /** * Sent by a player client who had to restart the application, * needs now to get all data from beginning on. */ public void rejoinGame() { ClientHandler replacedCH = processingCH.getReplacedCH(); if (replacedCH == null) { LOGGER.warning("Rejoining game, but replacedCH is null?"); return; } LOGGER.info("Got: rejoinGame from CH " + processingCH.getClientName() + " to replace previous connection with id " + "???" + " and name " + replacedCH.getPlayerName()); // processingCH.initResendQueueFromOther(replacedCH, true); processingCH.initResendQueueFromOther(replacedCH); addIClient(processingCH); addRealClient(processingCH); processingCH.syncAfterReconnect(-1, 0); oneTellAllLegionLocations(processingCH); oneUpdateLegionStatus(processingCH); processingCH.updatePlayerInfo(getPlayerInfo(false)); // Technically totally unnecessary to re-send it to all // (only the new client needs it), but it's much easier this way // at least at the moment... game.updateCaretakerDisplays(); } public void watchGame() { LOGGER.info("Got: watchGame from CH " + processingCH.getClientName()); processingCH.initResendQueueFromStub(clientStub); addIClient(processingCH); addRealClient(processingCH); processingCH.syncAfterReconnect(-1, 0); oneTellAllLegionLocations(processingCH); processingCH.updatePlayerInfo(getPlayerInfo(false)); // Technically totally unnecessary to re-send it to all // (only the new watcher needs it), but it's much easier this way // at least at the moment... game.updateCaretakerDisplays(); } public void logMsgToServer(String severity, String message) { LOGGER.info("CLIENTLOG: " + severity + ": " + message); } public void cheatModeDestroyLegion(Legion legion) { ((LegionServerSide)legion).remove(); } public void enforcedDisconnectClient(String name) { try { ClientHandler handler = getClientHandlerByName(name); if (handler != null) { handler.fakeDisconnectClient(); } } catch (Exception e) { // ignore it... it's for develop/debugging purpose only // (at the moment, at least...;-) } } public class WithdrawInfo { public long deadline; public long intervalLen; public long intervals; public long lastNotification; public ClientHandler ch; public WithdrawInfo(ClientHandler ch, int intervals, long intervalLen) { long now = new Date().getTime(); this.deadline = now + (intervals * intervalLen); this.lastNotification = now; this.ch = ch; this.intervalLen = intervalLen; this.intervals = intervals; } public long getLastNotification() { return lastNotification; } public void setLastNotification(long when) { this.lastNotification = when; } } }