/* * 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.client; import groovy.lang.Script; import java.awt.Desktop; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Toolkit; import java.awt.Transparency; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Vector; import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.CellEditor; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JList; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.SwingConstants; import javax.swing.ToolTipManager; import javax.swing.UIDefaults; import javax.swing.UIManager; import javax.swing.plaf.FontUIResource; import net.tsc.servicediscovery.ServiceAnnouncer; import org.apache.commons.io.FileUtils; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; import com.jidesoft.grid.CellEditorFactory; import com.jidesoft.grid.CellEditorManager; import com.jidesoft.grid.ListComboBoxCellEditor; import com.jidesoft.plaf.LookAndFeelFactory; import com.jidesoft.plaf.UIDefaultsLookup; import com.jidesoft.plaf.basic.ThemePainter; import com.t3.EventDispatcher; import com.t3.TaskBarFlasher; import com.t3.client.swing.NoteFrame; import com.t3.client.swing.SplashScreen; import com.t3.client.swing.T3EventQueue; import com.t3.client.ui.AppMenuBar; import com.t3.client.ui.ConnectionStatusPanel; import com.t3.client.ui.StartServerDialogPreferences; import com.t3.client.ui.T3Frame; import com.t3.client.ui.zone.PlayerView; import com.t3.client.ui.zone.ZoneRenderer; import com.t3.client.ui.zone.ZoneRendererFactory; import com.t3.clientserver.connection.ClientConnection; import com.t3.guid.GUID; import com.t3.image.ImageUtil; import com.t3.image.ThumbnailManager; import com.t3.language.I18N; import com.t3.macro.MacroEngine; import com.t3.model.AssetManager; import com.t3.model.ObservableList; import com.t3.model.Player; import com.t3.model.Zone; import com.t3.model.ZoneFactory; import com.t3.model.campaign.Campaign; import com.t3.model.campaign.CampaignFactory; import com.t3.model.campaign.TokenPropertyType; import com.t3.model.chat.TextMessage; import com.t3.net.RPTURLStreamHandlerFactory; import com.t3.networking.ClientMethodHandler; import com.t3.networking.ServerCommand; import com.t3.networking.ServerCommandClientImpl; import com.t3.networking.ServerConfig; import com.t3.networking.ServerDisconnectHandler; import com.t3.networking.ServerPolicy; import com.t3.networking.T3Connection; import com.t3.networking.T3Server; import com.t3.networking.registry.T3Registry; import com.t3.persistence.BackupManager; import com.t3.persistence.FileUtil; import com.t3.sound.SoundManager; import com.t3.swing.SwingUtil; import com.t3.transfer.AssetTransferManager; import com.t3.util.UPnPUtil; /** */ public class TabletopTool { private static final Logger log = Logger.getLogger(TabletopTool.class); /** * The splash image that comes up during application initialization. */ private static final String SPLASH_IMAGE = "com/t3/client/image/tabletoptool_splash.jpg"; /** * Contains just the version number of TabletopTool, such as <code>1.3.b49</code> * . */ private static final String VERSION_TXT = "com/t3/version.txt"; /** * Specifies the properties file that holds sound information. Only two * sounds currently: <b>Dink</b> and <b>Clink</b>. */ private static final String SOUND_PROPERTIES = "com/t3/client/sounds.properties"; public static final String SND_INVALID_OPERATION = "invalidOperation"; /** * Returns true if currently running on a Mac OS X based operating system. */ public static boolean MAC_OS_X = (System.getProperty("os.name").toLowerCase().startsWith("mac os x")); /** * Returns true if currently running on a Windows based operating system. */ public static boolean WINDOWS = (System.getProperty("os.name").toLowerCase().startsWith("windows")); /** * Version of Java being used. Note that this is the * "specification version", so expect numbers like 1.4, 1.5, and 1.6. */ public static Double JAVA_VERSION; public static enum ZoneEvent { Added, Removed, Activated, Deactivated } public static enum PreferencesEvent { Changed } private static final Dimension THUMBNAIL_SIZE = new Dimension(100, 100); private static ThumbnailManager thumbnailManager; private static String version; private static Campaign campaign; private static ObservableList<Player> playerList; private static ObservableList<TextMessage> messageList; private static Player player; private static ClientConnection conn; private static ClientMethodHandler handler; private static JMenuBar menuBar; private static T3Frame clientFrame; private static NoteFrame profilingNoteFrame; private static T3Server server; private static ServerCommand serverCommand; private static ServerPolicy serverPolicy; private static BackupManager backupManager; private static AssetTransferManager assetTransferManager; private static ServiceAnnouncer announcer; private static AutoSaveManager autoSaveManager; private static SoundManager soundManager; private static TaskBarFlasher taskbarFlasher; private static EventDispatcher eventDispatcher; private static String lastWhisperer; /** * This method looks up the message key in the properties file and returns * the resultant text with the detail message from the * <code>Throwable</code> appended to the end. * * @param msgKey * the string to use when calling {@link I18N#getText(String)} * @param t * the exception to be processed * @return the <code>String</code> result */ public static String generateMessage(String msgKey, Throwable t) { String msg; if (t == null) { msg = I18N.getText(msgKey); } else if (msgKey == null) { msg = t.toString(); } else { msg = I18N.getText(msgKey) + "<br/>" + t.toString(); } return msg; } /** * This method is the base method for putting a dialog box up on the screen * that might be an error, a warning, or just an information message. Do not * use this method if the desired result is a simple confirmation box (use * {@link #confirm(String, Object...)} instead). * * @param message * the key in the properties file to put in the body of the * dialog (formatted using <code>params</code>) * @param titleKey * the key in the properties file to use when creating the title * of the dialog window (formatted using <code>params</code>) * @param messageType * JOptionPane.{ERROR|WARNING|INFORMATION}_MESSAGE * @param params * optional parameters to use when formatting the data from the * properties file */ public static void showMessage(String message, String titleKey, int messageType, Object... params) { String title = I18N.getText(titleKey, params); JOptionPane.showMessageDialog(clientFrame, "<html>" + I18N.getText(message, params), title, messageType); } /** * Same as {@link #showMessage(String, String, int, Object...)} except that * <code>messages</code> is stored into a JList and that component is then * used as the content of the dialog box. This allows multiple strings to be * displayed in a manner consistent with other message dialogs. * * @param messages * the Objects (normally strings) to put in the body of the * dialog; no properties file lookup is performed! * @param titleKey * the key in the properties file to use when creating the title * of the dialog window (formatted using <code>params</code>) * @param messageType * one of <code>JOptionPane.ERROR_MESSAGE</code>, * <code>JOptionPane.WARNING_MESSAGE</code>, * <code>JOptionPane.INFORMATION_MESSAGE</code> * @param params * optional parameters to use when formatting the title text from * the properties file */ public static void showMessage(List<String> messages, String titleKey, int messageType, Object... params) { String title = I18N.getText(titleKey, params); JList<String> list = new JList<String>(new Vector<String>(messages)); JOptionPane.showMessageDialog(clientFrame, list, title, messageType); } /** * Displays the messages provided as <code>messages</code> by calling * {@link #showMessage(List<Object>, String, int, Object...)} and passing * <code>"msg.title.messageDialogFeedback"</code> and * <code>JOptionPane.ERROR_MESSAGE</code> as parameters. * * @param messages * the strings to put in the body of the * dialog; no properties file lookup is performed! */ public static void showFeedback(List<String> messages) { showMessage(messages, "msg.title.messageDialogFeedback", JOptionPane.ERROR_MESSAGE); } /** * Displays a dialog box by calling {@link #showError(String, Throwable)} * and passing <code>null</code> for the second parameter. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} */ public static void showError(String msgKey) { showError(msgKey, null); } /** * Displays a dialog box with a predefined title and type, and a message * crafted by calling {@link #generateMessage(String, Throwable)} and * passing it the two parameters. Also logs an entry using the * {@link Logger#error(Object, Throwable)} method. * <p> * The title is the property key <code>"msg.title.messageDialogError"</code> * , and the dialog type is <code>JOptionPane.ERROR_MESSAGE</code>. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} * @param t * the exception to be processed */ public static void showError(String msgKey, Throwable t) { String msg = generateMessage(msgKey, t); log.error(msgKey, t); if(t!=null) t.printStackTrace(); showMessage(msg, "msg.title.messageDialogError", JOptionPane.ERROR_MESSAGE); } /** * Displays a dialog box by calling {@link #showWarning(String, Throwable)} * and passing <code>null</code> for the second parameter. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} */ public static void showWarning(String msgKey) { showWarning(msgKey, null); } /** * Displays a dialog box with a predefined title and type, and a message * crafted by calling {@link #generateMessage(String, Throwable)} and * passing it the two parameters. Also logs an entry using the * {@link Logger#warn(Object, Throwable)} method. * <p> * The title is the property key * <code>"msg.title.messageDialogWarning"</code>, and the dialog type is * <code>JOptionPane.WARNING_MESSAGE</code>. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} * @param t * the exception to be processed */ public static void showWarning(String msgKey, Throwable t) { String msg = generateMessage(msgKey, t); log.warn(msgKey, t); showMessage(msg, "msg.title.messageDialogWarning", JOptionPane.WARNING_MESSAGE); } /** * Displays a dialog box by calling * {@link #showInformation(String, Throwable)} and passing <code>null</code> * for the second parameter. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} */ public static void showInformation(String msgKey) { showInformation(msgKey, null); } /** * Displays a dialog box with a predefined title and type, and a message * crafted by calling {@link #generateMessage(String, Throwable)} and * passing it the two parameters. Also logs an entry using the * {@link Logger#info(Object, Throwable)} method. * <p> * The title is the property key <code>"msg.title.messageDialogInfo"</code>, * and the dialog type is <code>JOptionPane.INFORMATION_MESSAGE</code>. * * @param msgKey * the key to use when calling {@link I18N#getText(String)} * @param t * the exception to be processed */ public static void showInformation(String msgKey, Throwable t) { String msg = generateMessage(msgKey, t); log.info(msgKey, t); showMessage(msg, "msg.title.messageDialogInfo", JOptionPane.INFORMATION_MESSAGE); } /** * Displays a confirmation dialog that uses the message as a key to the * properties file, and the additional values as parameters to the * formatting of the key lookup. * * @param message * key from the properties file (preferred) or hard-coded string * to display * @param params * optional arguments for the formatting of the property value * @return <code>true</code> if the user clicks the OK button, * <code>false</code> otherwise */ public static boolean confirm(String message, Object... params) { // String msg = I18N.getText(message, params); // log.debug(message); String title = I18N.getText("msg.title.messageDialogConfirm"); // return JOptionPane.showConfirmDialog(clientFrame, msg, title, JOptionPane.OK_OPTION) == JOptionPane.OK_OPTION; return confirmImpl(title, JOptionPane.OK_OPTION, message, params) == JOptionPane.OK_OPTION; } /** * Displays a confirmation dialog that uses the message as a key to the * properties file, and the additional values as parameters to the * formatting of the key lookup. * * @param title * @param buttons * @param message * key from the properties file (preferred) or hard-coded string * to display * @param params * optional arguments for the formatting of the property value * @return <code>true</code> if the user clicks the OK button, * <code>false</code> otherwise */ public static int confirmImpl(String title, int buttons, String message, Object... params) { String msg = I18N.getText(message, params); log.debug(message); return JOptionPane.showConfirmDialog(clientFrame, msg, title, buttons); } /** * This method is specific to deleting a token, but it can be used as a * basis for any other method which wants to be turned off via a property. * * @return true if the token should be deleted. */ public static boolean confirmTokenDelete() { if (!AppPreferences.getTokensWarnWhenDeleted()) { return true; } String msg = I18N.getText("msg.confirm.deleteToken"); log.debug(msg); Object[] options = { I18N.getText("msg.title.messageDialog.yes"), I18N.getText("msg.title.messageDialog.no"), I18N.getText("msg.title.messageDialog.dontAskAgain") }; String title = I18N.getText("msg.title.messageDialogConfirm"); int val = JOptionPane.showOptionDialog(clientFrame, msg, title, JOptionPane.NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); // "Yes, don't show again" Button if (val == 2) { showInformation("msg.confirm.deleteToken.removed"); AppPreferences.setTokensWarnWhenDeleted(false); } // Any version of 'Yes'... if (val == JOptionPane.YES_OPTION || val == 2) { return true; } // Assume 'No' response return false; } private TabletopTool() { // Not to be instantiated throw new Error("cannot construct TabletopTool object!"); } public static BackupManager getBackupManager() { if (backupManager == null) { backupManager = new BackupManager(AppUtil.getAppHome("backup")); } return backupManager; } /** * Launch the platform's web browser and ask it to open the given URL. Note * that this should not be called from any uncontrolled macros as there are * both security and denial-of-service attacks possible. * * @param url */ public static void showDocument(String url) { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); URI uri = null; try { uri = new URI(url); desktop.browse(uri); } catch (Exception e) { TabletopTool.showError(I18N.getText("msg.error.browser.cannotStart", uri), e); } } else { String errorMessage = "msg.error.browser.notFound"; Exception exception = null; String[] envvars = { "T3_BROWSER", "BROWSER" }; String param = envvars[0]; boolean apparentlyItWorked = false; for (String var : envvars) { String browser = System.getenv(var); if (browser != null) { try { param = var + "=\"" + browser + "\""; Runtime.getRuntime().exec(new String[] { browser, url }); apparentlyItWorked = true; } catch (Exception e) { errorMessage = "msg.error.browser.cannotStart"; exception = e; } } } if (apparentlyItWorked == false) { TabletopTool.showError(I18N.getText(errorMessage, param), exception); } } } public static SoundManager getSoundManager() { return soundManager; } public static void playSound(String eventId) { if (AppPreferences.getPlaySystemSounds()) { if (AppPreferences.getPlaySystemSoundsOnlyWhenNotFocused() && isInFocus()) { return; } soundManager.playSoundEvent(eventId); } } public static void updateServerPolicy(ServerPolicy policy) { setServerPolicy(policy); // Give everyone the new policy if (serverCommand != null) { serverCommand.setServerPolicy(policy); } } public static boolean isInFocus() { // TODO: This should probably also check owned windows return getFrame().isFocused(); } // TODO: This method is redundant now. It should be rolled into the // TODO: ExportDialog screenshot method. But until that has proven stable // TODO: for a while, I don't want to mess with this. (version 1.3b70 is most recent) public static BufferedImage takeMapScreenShot(final PlayerView view) { final ZoneRenderer renderer = clientFrame.getCurrentZoneRenderer(); if (renderer == null) { return null; } Dimension size = renderer.getSize(); if (size.width == 0 || size.height == 0) { return null; } BufferedImage image = new BufferedImage(size.width, size.height, Transparency.OPAQUE); final Graphics2D g = image.createGraphics(); g.setClip(0, 0, size.width, size.height); // Have to do this on the EDT so that there aren't any odd side effects // of rendering // using a renderer that's on screen if (!EventQueue.isDispatchThread()) { try { EventQueue.invokeAndWait(new Runnable() { @Override public void run() { renderer.renderZone(g, view); } }); } catch (InterruptedException ie) { TabletopTool.showError("While creating snapshot", ie); } catch (InvocationTargetException ite) { TabletopTool.showError("While creating snapshot", ite); } } else { renderer.renderZone(g, view); } g.dispose(); return image; } public static AutoSaveManager getAutoSaveManager() { if (autoSaveManager == null) { autoSaveManager = new AutoSaveManager(); } return autoSaveManager; } public static EventDispatcher getEventDispatcher() { return eventDispatcher; } private static void registerEvents() { getEventDispatcher().registerEvents(ZoneEvent.values()); getEventDispatcher().registerEvents(PreferencesEvent.values()); } /** * This was added to make it easier to set a breakpoint and locate when the * frame was initialized. * * @param frame */ private static void setClientFrame(T3Frame frame) { clientFrame = frame; } private static void initialize() { // First time AppSetup.install(); // Clean up after ourselves try { FileUtil.delete(AppUtil.getAppHome("tmp"), 2); } catch (IOException ioe) { TabletopTool.showError("While initializing (cleaning tmpdir)", ioe); } // We'll manage our own images ImageIO.setUseCache(false); eventDispatcher = new EventDispatcher(); registerEvents(); soundManager = new SoundManager(); try { soundManager.configure(SOUND_PROPERTIES); soundManager.registerSoundEvent(SND_INVALID_OPERATION, soundManager.getRegisteredSound("Dink")); } catch (IOException ioe) { TabletopTool.showError("While initializing (configuring sound)", ioe); } assetTransferManager = new AssetTransferManager(); assetTransferManager.addConsumerListener(new AssetTransferHandler()); playerList = new ObservableList<Player>(); messageList = new ObservableList<TextMessage>(Collections.synchronizedList(new ArrayList<TextMessage>())); handler = new ClientMethodHandler(); setClientFrame(new T3Frame(menuBar)); serverCommand = new ServerCommandClientImpl(); player = new Player("", Player.Role.GM, ""); try { startPersonalServer(CampaignFactory.createBasicCampaign()); } catch (Exception e) { TabletopTool.showError("While starting personal server", e); } AppActions.updateActions(); ToolTipManager.sharedInstance().setInitialDelay(AppPreferences.getToolTipInitialDelay()); ToolTipManager.sharedInstance().setDismissDelay(AppPreferences.getToolTipDismissDelay()); ChatAutoSave.changeTimeout(AppPreferences.getChatAutosaveTime()); // TODO: make this more formal when we switch to mina new ServerHeartBeatThread().start(); } public static NoteFrame getProfilingNoteFrame() { if (profilingNoteFrame == null) { profilingNoteFrame = new NoteFrame(); profilingNoteFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); profilingNoteFrame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { AppState.setCollectProfilingData(false); profilingNoteFrame.setVisible(false); } }); profilingNoteFrame.setSize(profilingNoteFrame.getPreferredSize()); // It's possible that the SelectionPanel may cause text to be added to the NoteFrame, so it // can happen before TabletopTool.initialize() has had a chance to init the clientFrame. if (clientFrame != null) SwingUtil.centerOver(profilingNoteFrame, clientFrame); } return profilingNoteFrame; } public static String getVersion() { if (version == null) { version = "DEVELOPMENT"; try { if (TabletopTool.class.getClassLoader().getResource(VERSION_TXT) != null) { version = new String(FileUtil.loadResource(VERSION_TXT)); } } catch (IOException ioe) { String msg = I18N.getText("msg.info.versionFile", VERSION_TXT); version = msg; TabletopTool.showError("msg.error.versionFileMissing"); } } return version; } public static boolean isDevelopment() { return "DEVELOPMENT".equals(version); } public static ServerPolicy getServerPolicy() { return serverPolicy; } public static ServerCommand serverCommand() { return serverCommand; } public static T3Server getServer() { return server; } public static void addPlayer(Player player) { if (!playerList.contains(player)) { playerList.add(player); // LATER: Make this non-anonymous playerList.sort(new Comparator<Player>() { @Override public int compare(Player arg0, Player arg1) { return arg0.getName().compareToIgnoreCase(arg1.getName()); } }); if (!player.equals(TabletopTool.getPlayer())) { String msg = MessageFormat.format(I18N.getText("msg.info.playerConnected"), player.getName()); addLocalMessage("<span style='color:#0000ff'><i>" + msg + "</i></span>"); } } } public Player getPlayer(String name) { for (int i = 0; i < playerList.size(); i++) { if (playerList.get(i).getName().equals(name)) { return playerList.get(i); } } return null; } public static void removePlayer(Player player) { if (player == null) { return; } playerList.remove(player); if (TabletopTool.getPlayer() != null && !player.equals(TabletopTool.getPlayer())) { String msg = MessageFormat.format(I18N.getText("msg.info.playerDisconnected"), player.getName()); addLocalMessage("<span style='color:#0000ff'><i>" + msg + "</i></span>"); } } public static ObservableList<TextMessage> getMessageList() { return messageList; } /** * These are the messages that originate from the server */ public static void addServerMessage(TextMessage message) { // Filter if (message.isGM() && !getPlayer().isGM()) { return; } if (message.isWhisper() && !getPlayer().getName().equalsIgnoreCase(message.getTarget())) { return; } if (!getFrame().isCommandPanelVisible()) { getFrame().getChatActionLabel().setVisible(true); } // Flashing if (!isInFocus()) { taskbarFlasher.flash(); } if (message.isWhisper()) { setLastWhisperer(message.getSource()); } messageList.add(message); } /** * These are the messages that are generated locally */ public static void addMessage(TextMessage message) { // Filter stuff addServerMessage(message); if (!message.isMe()) { serverCommand().message(message); } } /** * Add a message only this client can see. This is a shortcut for * addMessage(ME, ...) * * @param message */ public static void addLocalMessage(String message) { addMessage(TextMessage.me(message)); } /** * Add a message all clients can see. This is a shortcut for addMessage(SAY, * ...) * * @param message */ public static void addGlobalMessage(String message) { addMessage(TextMessage.say(message)); } /** * Add a message all specified clients will see. This is a shortcut for * addMessage(WHISPER, ...) and addMessage(GM, ...). The * <code>targets</code> is expected do be in a string list built with * <code>separator</code>. * * @param message * message to be sent * @param targets * string specifying clients to send the message to (spaces are * trimmed) * @param separator * the separator between entries in <code>targets</code> */ public static void addGlobalMessage(String message, String targets, String separator) { List<String> list = new LinkedList<String>(); for (String target : targets.split(separator)) list.add(target.trim()); addGlobalMessage(message, list); } /** * Add a message all specified clients will see. This is a shortcut for * addMessage(WHISPER, ...) and addMessage(GM, ...). * * @param message * message to be sent * @param targets * list of <code>String</code>s specifying clients to send the * message to */ public static void addGlobalMessage(String message, List<String> targets) { for (String target : targets) { if ("gm".equalsIgnoreCase(target)) { addMessage(TextMessage.gm(message)); } else { addMessage(TextMessage.whisper(target, message)); } } } public static Campaign getCampaign() { if (campaign == null) { campaign = new Campaign(); } return campaign; } public static void setCampaign(Campaign campaign) { setCampaign(campaign, null); } public static void setCampaign(Campaign campaign, GUID defaultRendererId) { // Load up the new TabletopTool.campaign = campaign; ZoneRenderer currRenderer = null; // Clean up clientFrame.setCurrentZoneRenderer(null); clientFrame.clearZoneRendererList(); clientFrame.getInitiativePanel().setZone(null); clientFrame.clearTokenTree(); if (campaign == null) { return; } // Install new campaign for (Zone zone : campaign.getZones()) { ZoneRenderer renderer = ZoneRendererFactory.newRenderer(zone); clientFrame.addZoneRenderer(renderer); if ((currRenderer == null || zone.getId().equals(defaultRendererId)) && (getPlayer().isGM() || zone.isVisible())) { currRenderer = renderer; } eventDispatcher.fireEvent(ZoneEvent.Added, campaign, null, zone); } clientFrame.setCurrentZoneRenderer(currRenderer); clientFrame.getInitiativePanel().setOwnerPermissions(campaign.isInitiativeOwnerPermissions()); clientFrame.getInitiativePanel().setMovementLock(campaign.isInitiativeMovementLock()); AssetManager.updateRepositoryList(); TabletopTool.getFrame().getCampaignPanel().reset(); } public static void setServerPolicy(ServerPolicy policy) { serverPolicy = policy; } public static AssetTransferManager getAssetTransferManager() { return assetTransferManager; } public static void startServer(String id, ServerConfig config, ServerPolicy policy, Campaign campaign) throws IOException { if (server != null) { Thread.dumpStack(); showError("msg.error.alreadyRunningServer"); return; } assetTransferManager.flush(); // TODO: the client and server campaign MUST be different objects. // Figure out a better init method server = new T3Server(config, policy); server.setCampaign(campaign); serverPolicy = server.getPolicy(); if (announcer != null) { announcer.stop(); } // Don't announce personal servers if (!config.isPersonalServer()) { announcer = new ServiceAnnouncer(id, server.getConfig().getPort(), AppConstants.SERVICE_GROUP); announcer.start(); } // Registered ? if (config.isServerRegistered() && !config.isPersonalServer()) { try { boolean worked = T3Registry.registerInstance(config.getServerName(), config.getPort()); if (!worked) { TabletopTool.showError("msg.error.alreadyRegistered"); } // TODO: I don't like this } catch (Exception e) { TabletopTool.showError("msg.error.failedCannotRegisterServer", e); } } } public static ThumbnailManager getThumbnailManager() { if (thumbnailManager == null) { thumbnailManager = new ThumbnailManager(AppUtil.getAppHome("imageThumbs"), THUMBNAIL_SIZE); } return thumbnailManager; } public static void stopServer() { if (server == null) { return; } disconnect(); server.stop(); server = null; } public static ObservableList<Player> getPlayerList() { return playerList; } public static List<String> getGMs() { Iterator<Player> pliter = playerList.iterator(); List<String> gms = new ArrayList<String>(playerList.size()); while (pliter.hasNext()) { Player plr = pliter.next(); if (plr.isGM()) { gms.add(plr.getName()); } } return gms; } /** * Whether a specific player is connected to the game */ public static boolean isPlayerConnected(String player) { for (int i = 0; i < playerList.size(); i++) { Player p = playerList.get(i); if (p.getName().equalsIgnoreCase(player)) { return true; } } return false; } public static int getNumberOfPlayers() { return playerList!=null?playerList.size():1; } public static void removeZone(Zone zone) { TabletopTool.serverCommand().removeZone(zone.getId()); TabletopTool.getFrame().removeZoneRenderer(TabletopTool.getFrame().getZoneRenderer(zone.getId())); TabletopTool.getCampaign().removeZone(zone.getId()); } public static void addZone(Zone zone) { if (getCampaign().getZones().size() == 1) { // Remove the default map Zone singleZone = getCampaign().getZones().get(0); if (ZoneFactory.DEFAULT_MAP_NAME.equals(singleZone.getName()) && singleZone.isEmpty()) { removeZone(singleZone); } } getCampaign().putZone(zone); serverCommand().putZone(zone); eventDispatcher.fireEvent(ZoneEvent.Added, getCampaign(), null, zone); // Show the new zone clientFrame.setCurrentZoneRenderer(ZoneRendererFactory.newRenderer(zone)); } public static Player getPlayer() { return player; } public static void startPersonalServer(Campaign campaign) throws IOException { ServerConfig config = ServerConfig.createPersonalServerConfig(); TabletopTool.startServer(null, config, new ServerPolicy(), campaign); String username = System.getProperty("user.name", "Player"); // Connect to server TabletopTool.createConnection("localhost", config.getPort(), new Player(username, Player.Role.GM, null)); // connecting TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.server); } public static void createConnection(String host, int port, Player player) throws UnknownHostException, IOException { if(host==null || host.trim().isEmpty()) throw new IllegalArgumentException("host is null or empty string"); TabletopTool.player = player; TabletopTool.getFrame().getCommandPanel().setImpersonatedToken(null); ClientConnection clientConn = new T3Connection(host, port, player); clientConn.addMessageHandler(handler); clientConn.addActivityListener(clientFrame.getActivityMonitor()); clientConn.addDisconnectHandler(new ServerDisconnectHandler()); clientConn.start(); // LATER: I really, really, really don't like this startup pattern if (clientConn.isAlive()) { conn = clientConn; } clientFrame.getLookupTablePanel().updateView(); clientFrame.getInitiativePanel().updateView(); } public static void closeConnection() throws IOException { if (conn != null) { conn.close(); } } public static ClientConnection getConnection() { return conn; } public static boolean isPersonalServer() { return server != null && server.getConfig().isPersonalServer(); } public static boolean isHostingServer() { return server != null && !server.getConfig().isPersonalServer(); } public static void disconnect() { // Close UPnP port mapping if used StartServerDialogPreferences serverProps = new StartServerDialogPreferences(); if (serverProps.getUseUPnP()) { int port = serverProps.getPort(); UPnPUtil.closePort(port); } boolean isPersonalServer = isPersonalServer(); if (announcer != null) { announcer.stop(); announcer = null; } if (conn == null || !conn.isAlive()) { return; } // Unregister ourselves if (server != null && server.getConfig().isServerRegistered() && !isPersonalServer) { try { T3Registry.unregisterInstance(server.getConfig().getPort()); } catch (Throwable t) { TabletopTool.showError("While unregistering server instance", t); } } try { conn.close(); conn = null; playerList.clear(); } catch (IOException ioe) { // This isn't critical, we're closing it anyway log.debug("While closing connection", ioe); } TabletopTool.getFrame().getConnectionStatusPanel().setStatus(ConnectionStatusPanel.Status.disconnected); if (!isPersonalServer) { addLocalMessage("<span style='color:blue'><i>" + I18N.getText("msg.info.disconnected") + "</i></span>"); } } public static T3Frame getFrame() { return clientFrame; } private static void configureLogging() { String logging = null; try { logging = new String(FileUtil.loadResource("com/t3/client/logging.xml"), "UTF-8"); } catch (IOException ioe) { System.err.println("Could not load logging configuration file: " + ioe); return; } File localLoggingConfigFile = new File(AppUtil.getAppHome(), "logging.xml"); String localConfig = ""; if (localLoggingConfigFile.exists()) { try { localConfig = FileUtils.readFileToString(localLoggingConfigFile, "UTF-8"); } catch (IOException e) { e.printStackTrace(); } } logging = logging.replace("INSERT_LOCAL_CONFIG_HERE", localConfig); logging = logging.replace("${appHome}", AppUtil.getAppHome().getAbsolutePath().replace('\\', '/')); // Configure new DOMConfigurator().doConfigure(new ByteArrayInputStream(logging.getBytes()), LogManager.getLoggerRepository()); } private static final void configureJide() { com.jidesoft.utils.Lm.verifyLicense("T3 Team", "MapTool", "EOCG3JevAmlHo1o6OdcI80kvcf2wQkI2"); LookAndFeelFactory.UIDefaultsCustomizer uiDefaultsCustomizer = new LookAndFeelFactory.UIDefaultsCustomizer() { @Override public void customize(UIDefaults defaults) { ThemePainter painter = (ThemePainter) UIDefaultsLookup.get("Theme.painter"); defaults.put("OptionPaneUI", "com.jidesoft.plaf.basic.BasicJideOptionPaneUI"); defaults.put("OptionPane.showBanner", Boolean.TRUE); // show banner or not. default is true defaults.put("OptionPane.bannerIcon", new ImageIcon(TabletopTool.class.getClassLoader().getResource("com/t3/client/image/tabletoptool_icon.png"))); defaults.put("OptionPane.bannerFontSize", 13); defaults.put("OptionPane.bannerFontStyle", Font.BOLD); defaults.put("OptionPane.bannerMaxCharsPerLine", 60); defaults.put("OptionPane.bannerForeground", painter != null ? painter.getOptionPaneBannerForeground() : null); // you should adjust this if banner background is not the default gradient paint defaults.put("OptionPane.bannerBorder", null); // use default border // set both bannerBackgroundDk and bannerBackgroundLt to null if you don't want gradient defaults.put("OptionPane.bannerBackgroundDk", painter != null ? painter.getOptionPaneBannerDk() : null); defaults.put("OptionPane.bannerBackgroundLt", painter != null ? painter.getOptionPaneBannerLt() : null); defaults.put("OptionPane.bannerBackgroundDirection", Boolean.TRUE); // default is true // optionally, you can set a Paint object for BannerPanel. If so, the three UIDefaults related to banner background above will be ignored. defaults.put("OptionPane.bannerBackgroundPaint", null); defaults.put("OptionPane.buttonAreaBorder", BorderFactory.createEmptyBorder(6, 6, 6, 6)); defaults.put("OptionPane.buttonOrientation", SwingConstants.RIGHT); } }; uiDefaultsCustomizer.customize(UIManager.getDefaults()); //register cell editors and renderers for(TokenPropertyType tpt:TokenPropertyType.values()) tpt.registerCellEditors(); CellEditorManager.registerEditor(TokenPropertyType.class, new CellEditorFactory() { @Override public CellEditor create() { return new ListComboBoxCellEditor(TokenPropertyType.values()); } }); } public static void main(String[] args) { if (MAC_OS_X) { // On OSX the menu bar at the top of the screen can be enabled at any time, but the // title (ie. name of the application) has to be set before the GUI is initialized (by // creating a frame, loading a splash screen, etc). So we do it here. System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("com.apple.mrj.application.apple.menu.about.name", "About TabletopTool..."); System.setProperty("apple.awt.brushMetalLook", "true"); } // Before anything else, create a place to store all the data try { AppUtil.getAppHome(); } catch (Throwable t) { t.printStackTrace(); // Create an empty frame so there's something to click on if the dialog goes in the background JFrame frame = new JFrame(); SwingUtil.centerOnScreen(frame); frame.setVisible(true); String errorCreatingDir = "Error creating data directory"; log.error(errorCreatingDir, t); JOptionPane.showMessageDialog(frame, t.getMessage(), errorCreatingDir, JOptionPane.ERROR_MESSAGE); System.exit(1); } verifyJavaVersion(); configureLogging(); // System properties System.setProperty("swing.aatext", "true"); // System.setProperty("sun.java2d.opengl", "true"); final SplashScreen splash = new SplashScreen(SPLASH_IMAGE, getVersion()); splash.showSplashScreen(); // Protocol handlers // cp:// is registered by the RPTURLStreamHandlerFactory constructor (why?) RPTURLStreamHandlerFactory factory = new RPTURLStreamHandlerFactory(); factory.registerProtocol("asset", new AssetURLStreamHandler()); URL.setURLStreamHandlerFactory(factory); MacroEngine.initialize(); configureJide(); //TODO find out how to not call this twice without destroying error windows final Toolkit tk = Toolkit.getDefaultToolkit(); tk.getSystemEventQueue().push(new T3EventQueue()); // LAF try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); configureJide(); if(WINDOWS) LookAndFeelFactory.installJideExtension(LookAndFeelFactory.XERTO_STYLE); else LookAndFeelFactory.installJideExtension(); if (MAC_OS_X) { macOSXicon(); } menuBar=new AppMenuBar(); } catch (Exception e) { TabletopTool.showError("msg.error.lafSetup", e); System.exit(1); } /** * This is a tweak that makes the Chinese version work better. * <p> * Consider reviewing <a * href="http://en.wikipedia.org/wiki/CJK_characters" * >http://en.wikipedia.org/wiki/CJK_characters</a> before making * changes. And http://www.scarfboy.com/coding/unicode-tool is also a * really cool site. */ if (Locale.CHINA.equals(Locale.getDefault())) { // The following font name appears to be "Sim Sun". It can be downloaded // from here: http://fr.cooltext.com/Fonts-Unicode-Chinese Font f = new Font("\u65B0\u5B8B\u4F53", Font.PLAIN, 12); FontUIResource fontRes = new FontUIResource(f); for (Enumeration<Object> keys = UIManager.getDefaults().keys(); keys.hasMoreElements();) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value instanceof FontUIResource) UIManager.put(key, fontRes); } } // Draw frame contents on resize tk.setDynamicLayout(true); EventQueue.invokeLater(new Runnable() { @Override public void run() { initialize(); EventQueue.invokeLater(new Runnable() { @Override public void run() { clientFrame.setVisible(true); splash.hideSplashScreen(); EventQueue.invokeLater(new Runnable() { @Override public void run() { postInitialize(); } }); } }); } }); // new Thread(new HeapSpy()).start(); } /** * Check to see if we're running on Java 6+. * <p> * While TabletopTool itself doesn't use any Java 6-specific features, we use a * couple dozen third-party libraries and a search of those JAR files * indicate that <i>they DO use</i> Java 6. So it's best if we warn users * that they might be going along happily and suddenly hit a Java runtime * error! It might even be something they do every time they run the * program, but some piece of data was different and the library took a * different path and the Java 6-only method was invoked... * <p> * This method uses the system property <b>java.specification.version</b> as * it seemed the easiest thing to test. :) */ private static void verifyJavaVersion() { String version = System.getProperty("java.specification.version"); boolean keepgoing = true; if (version == null) { keepgoing = confirm("msg.error.unknownJavaVersion"); JAVA_VERSION = 1.5; } else { JAVA_VERSION = Double.valueOf(version); if (JAVA_VERSION < 1.7) { keepgoing = confirm("msg.error.wrongJavaVersion", version); } } if (!keepgoing) System.exit(1); } /** * If we're running on OSX we should call this method to download and * install the TabletopTool logo from the main web site. We cache this image so * that it appears correctly if the application is later executed in * "offline" mode, so to speak. */ private static void macOSXicon() { // If we're running on OSX, add the dock icon image // -- and change our application name to just "TabletopTool" (not currently) // We wait until after we call initialize() so that the asset and image managers // are configured. Image img = null; File logoFile = new File(AppUtil.getAppHome("config"), "tabletoptool-dock-icon.png"); URL logoURL = null; try { logoURL = new URL("http://services.tabletoptool.com/logo_large.png"); } catch (MalformedURLException e) { showError("Attemping to form URL -- shouldn't happen as URL is hard-coded", e); } try { img = ImageUtil.bytesToImage(FileUtils.readFileToByteArray(logoFile)); } catch (IOException e) { log.debug("Attemping to read cached icon: " + logoFile, e); try { img = ImageUtil.bytesToImage(FileUtil.getBytes(logoURL)); // If we did download the logo, save it to the 'config' dir for later use. BufferedImage bimg = ImageUtil.createCompatibleImage(img, img.getWidth(null), img.getHeight(null), null); FileUtils.writeByteArrayToFile(logoFile, ImageUtil.imageToBytes(bimg, "png")); img = bimg; } catch (IOException e1) { log.warn("Cannot read '" + logoURL + "' or cached '" + logoFile + "'; no dock icon", e1); } } /* * Unfortunately the next line doesn't allow Eclipse to compile the code * on anything but a Mac. Too bad because there's no problem at runtime * since this code wouldn't be executed an any machine *except* a Mac. * Sigh. * * com.apple.eawt.Application appl = * com.apple.eawt.Application.getApplication(); */ try { Class<?> appClass = Class.forName("com.apple.eawt.Application"); Method getApplication = appClass.getDeclaredMethod("getApplication", (Class[]) null); Object appl = getApplication.invoke(null, (Object[]) null); Method setDockIconImage = appl.getClass().getDeclaredMethod("setDockIconImage", new Class[] { java.awt.Image.class }); // If we couldn't grab the image for some reason, don't set the dock bar icon! Duh! if (img != null) setDockIconImage.invoke(appl, new Object[] { img }); if (T3Util.isDebugEnabled()) { // For some reason Mac users don't like the dock badge icon. But from a development standpoint I like seeing the // version number in the dock bar. So we'll only include it when running with T3_DEV on the command line. Method setDockIconBadge = appl.getClass().getDeclaredMethod("setDockIconBadge", new Class[] { java.lang.String.class }); String vers = getVersion(); vers = vers.substring(vers.length() - 2); vers = vers.replaceAll("[^0-9]", "0"); // Convert all non-digits to zeroes setDockIconBadge.invoke(appl, new Object[] { vers }); } } catch (Exception e) { log.info("Cannot find/invoke methods on com.apple.eawt.Application; use -X command line options to set dock bar attributes", e); } } private static void postInitialize() { // Check to see if there is an autosave file from MT crashing getAutoSaveManager().check(); getAutoSaveManager().restart(); taskbarFlasher = new TaskBarFlasher(clientFrame); } /** * Return whether the campaign file has changed. Only checks to see if there * is a single empty map with the default name * (ZoneFactory.DEFAULT_MAP_NAME). If so, the campaign is "empty". We really * should check against things like campaign property changes as well, * including campaign macros... */ public static boolean isCampaignDirty() { // TODO: This is a very naive check, but it's better than nothing if (getCampaign().getZones().size() == 1) { Zone singleZone = TabletopTool.getCampaign().getZones().get(0); if (ZoneFactory.DEFAULT_MAP_NAME.equals(singleZone.getName()) && singleZone.isEmpty()) { return false; } } return true; } public static void setLastWhisperer(String lastWhisperer) { if (lastWhisperer != null) { TabletopTool.lastWhisperer = lastWhisperer; } } public static String getLastWhisperer() { return lastWhisperer; } public static boolean useToolTipsForUnformatedRolls() { if (isPersonalServer()) { return AppPreferences.getUseToolTipForInlineRoll(); } else { return getServerPolicy().getUseToolTipsForDefaultRollFormat(); } } private static class ServerHeartBeatThread extends Thread { @Override public void run() { // This should always run, so we should be able to safely // loop forever while (true) { try { Thread.sleep(20000); } catch (InterruptedException e) { e.printStackTrace(); } ServerCommand command = serverCommand; if (command != null) { command.heartbeat(getPlayer().getName()); } } } } public static MacroEngine<Script> getParser() { return MacroEngine.getInstance(); } }