/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.networking; import java.awt.geom.Area; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import com.t3.MD5Key; import com.t3.client.TabletopTool; import com.t3.client.ui.zone.FogUtil; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.clientserver.handler.AbstractMethodHandler; import com.t3.common.T3Constants; import com.t3.guid.GUID; import com.t3.model.Asset; import com.t3.model.AssetManager; import com.t3.model.ExposedAreaMetaData; import com.t3.model.Label; import com.t3.model.MacroButtonProperties; import com.t3.model.Pointer; import com.t3.model.Token; import com.t3.model.Zone; import com.t3.model.Zone.VisionType; import com.t3.model.ZonePoint; import com.t3.model.campaign.Campaign; import com.t3.model.campaign.CampaignProperties; import com.t3.model.chat.TextMessage; import com.t3.model.drawing.Drawable; import com.t3.model.drawing.DrawnElement; import com.t3.model.drawing.Pen; import com.t3.model.grid.Grid; import com.t3.model.initiative.InitiativeList; import com.t3.model.initiative.InitiativeValue; import com.t3.model.initiative.InitiativeList.TokenInitiative; import com.t3.transfer.AssetProducer; /** * @author drice */ public class ServerMethodHandler extends AbstractMethodHandler<NetworkCommand> implements ServerCommand { private final T3Server server; private final Object MUTEX = new Object(); public ServerMethodHandler(T3Server server) { this.server = server; } @Override public void handleMethod(String id, NetworkCommand method, Object... parameters) { try { RPCContext context = new RPCContext(id, method, parameters); RPCContext.setCurrent(context); switch (method) { case bootPlayer: bootPlayer(context.getString(0)); break; case bringTokensToFront: bringTokensToFront(context.getGUID(0), (Set<GUID>) context.get(1)); break; case draw: draw(context.getGUID(0), (Pen) context.get(1), (Drawable) context.get(2)); break; case enforceZoneView: enforceZoneView(context.getGUID(0), context.getInt(1), context.getInt(2), context.getDouble(3), context.getInt(4), context.getInt(5)); break; case exposeFoW: exposeFoW(context.getGUID(0), (Area) context.get(1), (Set<GUID>) context.get(2)); break; case getAsset: getAsset((MD5Key) context.get(0)); break; case getZone: getZone(context.getGUID(0)); break; case hideFoW: hideFoW(context.getGUID(0), (Area) context.get(1), (Set<GUID>) context.get(2)); break; case setFoW: setFoW(context.getGUID(0), (Area) context.get(1), (Set<GUID>) context.get(2)); break; case hidePointer: hidePointer(context.getString(0)); break; case setLiveTypingLabel: setLiveTypingLabel(context.getString(0), context.getBool(1)); break; case enforceNotification: enforceNotification(context.getBool(0)); break; case message: message((TextMessage) context.get(0)); break; case putAsset: putAsset((Asset) context.get(0)); break; case putLabel: putLabel(context.getGUID(0), (Label) context.get(1)); break; case putToken: putToken(context.getGUID(0), (Token) context.get(1)); break; case putZone: putZone((Zone) context.get(0)); break; case removeZone: removeZone(context.getGUID(0)); break; case removeAsset: removeAsset((MD5Key) context.get(0)); break; case removeToken: removeToken(context.getGUID(0), context.getGUID(1)); break; case removeLabel: removeLabel(context.getGUID(0), context.getGUID(1)); break; case sendTokensToBack: sendTokensToBack(context.getGUID(0), (Set<GUID>) context.get(1)); break; case setCampaign: setCampaign((Campaign) context.get(0)); break; case setZoneGridSize: setZoneGridSize(context.getGUID(0), context.getInt(1), context.getInt(2), context.getInt(3), context.getInt(4)); break; case setZoneVisibility: setZoneVisibility(context.getGUID(0), (Boolean) context.get(1)); break; case setZoneHasFoW: setZoneHasFoW(context.getGUID(0), context.getBool(1)); break; case showPointer: showPointer(context.getString(0), (Pointer) context.get(1)); break; case startTokenMove: startTokenMove(context.getString(0), context.getGUID(1), context.getGUID(2), (Set<GUID>) context.get(3)); break; case stopTokenMove: stopTokenMove(context.getGUID(0), context.getGUID(1)); break; case toggleTokenMoveWaypoint: toggleTokenMoveWaypoint(context.getGUID(0), context.getGUID(1), (ZonePoint) context.get(2)); break; case undoDraw: undoDraw(context.getGUID(0), context.getGUID(1)); break; case updateTokenMove: updateTokenMove(context.getGUID(0), context.getGUID(1), context.getInt(2), context.getInt(3)); break; case clearAllDrawings: clearAllDrawings(context.getGUID(0), (Zone.Layer) context.get(1)); break; case enforceZone: enforceZone(context.getGUID(0)); break; case setServerPolicy: setServerPolicy((ServerPolicy) context.get(0)); break; case addTopology: addTopology(context.getGUID(0), (Area) context.get(1)); break; case removeTopology: removeTopology(context.getGUID(0), (Area) context.get(1)); break; case renameZone: renameZone(context.getGUID(0), context.getString(1)); break; case heartbeat: heartbeat(context.getString(0)); break; case updateCampaign: updateCampaign((CampaignProperties) context.get(0)); break; case movePointer: movePointer(context.getString(0), context.getInt(1), context.getInt(2)); break; case updateInitiative: updateInitiative((InitiativeList) context.get(0), (Boolean) context.get(1)); break; case updateTokenInitiative: updateTokenInitiative(context.getGUID(0), context.getGUID(1), context.getBool(2), (InitiativeValue)context.get(3), context.getInt(4)); break; case setVisionType: setVisionType(context.getGUID(0), (VisionType) context.get(1)); break; case setBoard: setBoard(context.getGUID(0), (MD5Key) context.get(1), context.getInt(2), context.getInt(3)); break; case updateCampaignMacros: updateCampaignMacros((List<MacroButtonProperties>) context.get(0)); break; case setTokenLocation: setTokenLocation(context.getGUID(0), context.getGUID(1), context.getInt(2), context.getInt(3)); break; case exposePCArea: exposePCArea(context.getGUID(0)); break; case updateExposedAreaMeta: updateExposedAreaMeta(context.getGUID(0), context.getGUID(1), (ExposedAreaMetaData) context.get(2)); break; } } finally { RPCContext.setCurrent(null); } } /** * Send the current call to all other clients except for the sender */ private void forwardToClients() { server.getConnection().broadcastCallMethod(new String[] { RPCContext.getCurrent().id }, RPCContext.getCurrent().method, RPCContext.getCurrent().parameters); } /** * Send the current call to all clients including the sender */ private void forwardToAllClients() { server.getConnection().broadcastCallMethod(new String[] {}, RPCContext.getCurrent().method, RPCContext.getCurrent().parameters); } private void broadcastToClients(String exclude, NetworkCommand method, Object... parameters) { server.getConnection().broadcastCallMethod(new String[] { exclude }, method, parameters); } private void broadcastToAllClients(NetworkCommand method, Object... parameters) { server.getConnection().broadcastCallMethod(new String[] {}, method, parameters); } //// // SERVER COMMAND @Override public void setVisionType(GUID zoneGUID, VisionType visionType) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.setVisionType(visionType); server.getConnection().broadcastCallMethod(NetworkCommand.setUseVision, RPCContext.getCurrent().parameters); } @Override public void heartbeat(String data) { // Nothing to do yet } @Override public void enforceZone(GUID zoneGUID) { forwardToClients(); } @Override public void updateCampaign(CampaignProperties properties) { server.getCampaign().replaceCampaignProperties(properties); forwardToClients(); } @Override public void bringTokensToFront(GUID zoneGUID, Set<GUID> tokenSet) { synchronized (MUTEX) { Zone zone = server.getCampaign().getZone(zoneGUID); // Get the tokens to update List<Token> tokenList = new ArrayList<Token>(); for (GUID tokenGUID : tokenSet) { Token token = zone.getToken(tokenGUID); if (token != null) { tokenList.add(token); } } // Arrange Collections.sort(tokenList, Zone.TOKEN_Z_ORDER_COMPARATOR); // Update int z = zone.getLargestZOrder() + 1; for (Token token : tokenList) { token.setZOrder(z++); } // Broadcast for (Token token : tokenList) { broadcastToAllClients(NetworkCommand.putToken, zoneGUID, token); } } } @Override public void clearAllDrawings(GUID zoneGUID, Zone.Layer layer) { Zone zone = server.getCampaign().getZone(zoneGUID); List<DrawnElement> list = zone.getDrawnElements(layer); zone.clearDrawables(list); // FJE Empties the DrawableUndoManager and empties the list forwardToAllClients(); } @Override public void draw(GUID zoneGUID, Pen pen, Drawable drawable) { server.getConnection().broadcastCallMethod(NetworkCommand.draw, RPCContext.getCurrent().parameters); Zone zone = server.getCampaign().getZone(zoneGUID); zone.addDrawable(new DrawnElement(drawable, pen)); } @Override public void enforceZoneView(GUID zoneGUID, int x, int y, double scale, int width, int height) { forwardToClients(); } @Override public void exposeFoW(GUID zoneGUID, Area area, Set<GUID> selectedToks) { Zone zone = server.getCampaign().getZone(zoneGUID); // this can return a zone that's not in T3Frame.zoneRenderList??? zone.exposeArea(area, selectedToks); server.getConnection().broadcastCallMethod(NetworkCommand.exposeFoW, RPCContext.getCurrent().parameters); } @Override public void exposePCArea(GUID zoneGUID) { ZoneRenderer renderer = TabletopTool.getFrame().getZoneRenderer(zoneGUID); FogUtil.exposePCArea(renderer); server.getConnection().broadcastCallMethod(NetworkCommand.exposePCArea, RPCContext.getCurrent().parameters); } @Override public void getAsset(MD5Key assetID) { if (assetID == null || assetID.toString().length() == 0) { return; } try { AssetProducer producer = new AssetProducer(assetID, AssetManager.getAssetInfo(assetID).getProperty(AssetManager.NAME), AssetManager.getAssetCacheFile(assetID)); server.getConnection().callMethod(RPCContext.getCurrent().id, T3Constants.Channel.IMAGE, NetworkCommand.startAssetTransfer, producer.getHeader()); server.addAssetProducer(RPCContext.getCurrent().id, producer); } catch (IOException ioe) { ioe.printStackTrace(); // Old fashioned way server.getConnection().callMethod(RPCContext.getCurrent().id, NetworkCommand.putAsset, AssetManager.getAsset(assetID)); } catch (IllegalArgumentException iae) { // Sending an empty asset will cause a failure of the image to load on the client side, showing a broken // image instead of blowing up Asset asset = new Asset("broken", new byte[] {}); asset.setId(assetID); server.getConnection().callMethod(RPCContext.getCurrent().id, NetworkCommand.putAsset, asset); } } @Override public void getZone(GUID zoneGUID) { server.getConnection().callMethod(RPCContext.getCurrent().id, NetworkCommand.putZone, server.getCampaign().getZone(zoneGUID)); } @Override public void hideFoW(GUID zoneGUID, Area area, Set<GUID> selectedToks) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.hideArea(area, selectedToks); server.getConnection().broadcastCallMethod(NetworkCommand.hideFoW, RPCContext.getCurrent().parameters); } @Override public void setFoW(GUID zoneGUID, Area area, Set<GUID> selectedToks) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.setFogArea(area, selectedToks); server.getConnection().broadcastCallMethod(NetworkCommand.setFoW, RPCContext.getCurrent().parameters); } @Override public void hidePointer(String player) { forwardToAllClients(); } @Override public void movePointer(String player, int x, int y) { forwardToAllClients(); } @Override public void updateInitiative(InitiativeList list, Boolean ownerPermission) { if (list != null) { if (list.getZone() == null) return; Zone zone = server.getCampaign().getZone(list.getZone().getId()); zone.setInitiativeList(list); } else if (ownerPermission != null) { TabletopTool.getFrame().getInitiativePanel().setOwnerPermissions(ownerPermission.booleanValue()); } forwardToAllClients(); } @Override public void updateTokenInitiative(GUID zoneId, GUID tokenId, Boolean hold, InitiativeValue state, Integer index) { Zone zone = server.getCampaign().getZone(zoneId); InitiativeList list = zone.getInitiativeList(); TokenInitiative ti = list.getTokenInitiative(index); if (!ti.getId().equals(tokenId)) { // Index doesn't point to same token, try to find it Token token = zone.getToken(tokenId); List<Integer> tokenIndex = list.indexOf(token); // If token in list more than one time, punt if (tokenIndex.size() != 1) return; ti = list.getTokenInitiative(tokenIndex.get(0)); } // endif ti.update(hold, state); forwardToAllClients(); } @Override public void renameZone(GUID zoneGUID, String name) { Zone zone = server.getCampaign().getZone(zoneGUID); if (zone != null) { zone.setName(name); forwardToAllClients(); } } @Override public void message(TextMessage message) { forwardToClients(); } @Override public void putAsset(Asset asset) { AssetManager.putAsset(asset); } @Override public void putLabel(GUID zoneGUID, Label label) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.putLabel(label); forwardToClients(); } @Override public void putToken(GUID zoneGUID, Token token) { Zone zone = server.getCampaign().getZone(zoneGUID); boolean newToken = zone.getToken(token.getId()) == null; synchronized (MUTEX) { // Set z-order for new tokens if (newToken) { token.setZOrder(zone.getLargestZOrder() + 1); } zone.putToken(token); } if (newToken) { forwardToAllClients(); } else { forwardToClients(); } } @Override public void putZone(Zone zone) { server.getCampaign().putZone(zone); forwardToClients(); } @Override public void removeAsset(MD5Key assetID) { AssetManager.removeAsset(assetID); } @Override public void removeLabel(GUID zoneGUID, GUID labelGUID) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.removeLabel(labelGUID); server.getConnection().broadcastCallMethod(NetworkCommand.removeLabel, RPCContext.getCurrent().parameters); } @Override public void removeToken(GUID zoneGUID, GUID tokenGUID) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.removeToken(tokenGUID); server.getConnection().broadcastCallMethod(NetworkCommand.removeToken, RPCContext.getCurrent().parameters); } @Override public void removeZone(GUID zoneGUID) { server.getCampaign().removeZone(zoneGUID); forwardToClients(); } @Override public void sendTokensToBack(GUID zoneGUID, Set<GUID> tokenSet) { synchronized (MUTEX) { Zone zone = server.getCampaign().getZone(zoneGUID); // Get the tokens to update List<Token> tokenList = new ArrayList<Token>(); for (GUID tokenGUID : tokenSet) { Token token = zone.getToken(tokenGUID); if (token != null) { tokenList.add(token); } } // Arrange Collections.sort(tokenList, Zone.TOKEN_Z_ORDER_COMPARATOR); // Update int z = zone.getSmallestZOrder() - 1; for (Token token : tokenList) { token.setZOrder(z--); } // Broadcast for (Token token : tokenList) { broadcastToAllClients(NetworkCommand.putToken, zoneGUID, token); } } } @Override public void setCampaign(Campaign campaign) { server.setCampaign(campaign); forwardToClients(); } @Override public void setZoneGridSize(GUID zoneGUID, int offsetX, int offsetY, int size, int color) { Zone zone = server.getCampaign().getZone(zoneGUID); Grid grid = zone.getGrid(); grid.setSize(size); grid.setOffset(offsetX, offsetY); zone.setGridColor(color); server.getConnection().broadcastCallMethod(NetworkCommand.setZoneGridSize, RPCContext.getCurrent().parameters); } @Override public void setZoneHasFoW(GUID zoneGUID, boolean hasFog) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.setHasFog(hasFog); server.getConnection().broadcastCallMethod(NetworkCommand.setZoneHasFoW, RPCContext.getCurrent().parameters); } @Override public void setZoneVisibility(GUID zoneGUID, boolean visible) { server.getCampaign().getZone(zoneGUID).setVisible(visible); server.getConnection().broadcastCallMethod(NetworkCommand.setZoneVisibility, RPCContext.getCurrent().parameters); } @Override public void showPointer(String player, Pointer pointer) { server.getConnection().broadcastCallMethod(NetworkCommand.showPointer, RPCContext.getCurrent().parameters); } @Override public void setLiveTypingLabel(String label, boolean show) { forwardToClients(); } @Override public void enforceNotification(Boolean enforce) { forwardToClients(); } @Override public void bootPlayer(String player) { forwardToClients(); // And just to be sure, remove them from the server server.releaseClientConnection(server.getConnectionId(player)); } @Override public void startTokenMove(String playerId, GUID zoneGUID, GUID tokenGUID, Set<GUID> tokenList) { forwardToClients(); } @Override public void stopTokenMove(GUID zoneGUID, GUID tokenGUID) { forwardToClients(); } @Override public void toggleTokenMoveWaypoint(GUID zoneGUID, GUID tokenGUID, ZonePoint cp) { forwardToClients(); } @Override public void undoDraw(GUID zoneGUID, GUID drawableGUID) { //TODO: This is a problem. The contents of the UndoManager are not synchronized across machines // so if one machine uses Meta-Z to undo a drawing, that drawable will be removed on all // machines, but there is no attempt to keep the UndoManager in sync. So that same drawable // will still be in the UndoManager queue on other machines. Ideally we should be filtering // the local Undomanager queue based on the drawable (removing it when we find it), but // the Swing UndoManager doesn't provide that capability so we would need to subclass it. // And if we're going to do that, we may as well fix the other problems: the UndoManager should // be per-map and per-layer (?) and not a singleton instance for the entire application! But // now we're talking a pretty intrusive set of changes: when a zone is deleted, the UndoManagers // would need to be cleared and duplicating a zone means doing a deep copy on the UndoManager // or flushing it entirely in the new zone. We'll save all of this for a separate patch against 1.3 or // for 1.4. server.getConnection().broadcastCallMethod(NetworkCommand.undoDraw, zoneGUID, drawableGUID); Zone zone = server.getCampaign().getZone(zoneGUID); zone.removeDrawable(drawableGUID); } @Override public void updateTokenMove(GUID zoneGUID, GUID tokenGUID, int x, int y) { forwardToClients(); } public void setTokenLocation(GUID zoneGUID, GUID tokenGUID, int x, int y) { forwardToClients(); } @Override public void setServerPolicy(ServerPolicy policy) { forwardToClients(); } @Override public void addTopology(GUID zoneGUID, Area area) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.addTopology(area); forwardToClients(); } @Override public void removeTopology(GUID zoneGUID, Area area) { Zone zone = server.getCampaign().getZone(zoneGUID); zone.removeTopology(area); forwardToClients(); } @Override public void updateCampaignMacros(List<MacroButtonProperties> properties) { TabletopTool.getCampaign().setMacroButtonPropertiesArray(new ArrayList<MacroButtonProperties>(properties)); forwardToClients(); } @Override public void setBoard(GUID zoneGUID, MD5Key mapId, int x, int y) { forwardToClients(); } /* * (non-Javadoc) * * @see com.t3.networking.ServerCommand#updateExposedAreaMeta(com.t3.GUID, * com.t3.GUID, com.t3.model.ExposedAreaMetaData) */ @Override public void updateExposedAreaMeta(GUID zoneGUID, GUID tokenExposedAreaGUID, ExposedAreaMetaData meta) { forwardToClients(); } //// // CONTEXT private static class RPCContext { private static ThreadLocal<RPCContext> threadLocal = new ThreadLocal<RPCContext>(); public String id; public NetworkCommand method; public Object[] parameters; public RPCContext(String id, NetworkCommand method, Object[] parameters) { this.id = id; this.method = method; this.parameters = parameters; } public static boolean hasCurrent() { return threadLocal.get() != null; } public static RPCContext getCurrent() { return threadLocal.get(); } public static void setCurrent(RPCContext context) { threadLocal.set(context); } //// // Convenience methods public GUID getGUID(int index) { return (GUID) parameters[index]; } public Integer getInt(int index) { return (Integer) parameters[index]; } public Double getDouble(int index) { return (Double) parameters[index]; } public Object get(int index) { return parameters[index]; } public String getString(int index) { return (String) parameters[index]; } public Boolean getBool(int index) { return (Boolean) parameters[index]; } } }