/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.client; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.freecol.FreeCol; import net.sf.freecol.common.io.FreeColModFile; import net.sf.freecol.common.io.Mods; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.FreeColGameObject; import net.sf.freecol.common.model.Game; import net.sf.freecol.common.model.ModelMessage; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.option.BooleanOption; import net.sf.freecol.common.option.IntegerOption; import net.sf.freecol.common.option.ListOption; import net.sf.freecol.common.option.OptionGroup; import net.sf.freecol.common.option.SelectOption; import org.freecolandroid.xml.stream.XMLInputFactory; import org.freecolandroid.xml.stream.XMLStreamReader; /** * Defines how available client options are displayed on the Setting dialog from * File>Preferences Also contains several Comparators used for display purposes. * * <br> * <br> * * Most available client options and their default values are defined * in the file <code>base/client-options.xml</code> in the FreeCol * data directory. They are overridden by the player's personal * settings in the file <code>options.xml</code> in the user * directory. Options that can not be defined in this way because they * are generated dynamically (such as a list of available mods) must * be added to {@link #addDefaultOptions()}. * * Each option should be given an unique identifier (defined as a * constant in this class). In general, the options are called * something like "model.option.UNIQUENAME". Since the options must * also be represented by the GUI, they following two keys must be * added to the file <code>FreeColMessages.properties</code>: * * <ul><li>model.option.UNIQUENAME.name</li> * <li>model.option.UNIQUENAME.shortDescription</li></ul> */ public class ClientOptions extends OptionGroup { private static final Logger logger = Logger.getLogger(ClientOptions.class.getName()); /** * Option for setting the language. */ public static final String LANGUAGE = "model.option.languageOption"; /** * If this option is enabled, the display will recenter in order to display * the active unit if it is not * {@link net.sf.freecol.client.gui.GUI#onScreen(Tile)}). * * @see net.sf.freecol.client.gui.GUI */ public static final String JUMP_TO_ACTIVE_UNIT = "model.option.jumpToActiveUnit"; /** * Selected tiles always gets centered if this option is enabled (even if * the tile is {@link net.sf.freecol.client.gui.GUI#onScreen(Tile)}). * * @see net.sf.freecol.client.gui.GUI */ public static final String ALWAYS_CENTER = "model.option.alwaysCenter"; /** * Used by GUI, this is the minimum number of goods a colony must possess for * the goods to show up at the bottom of the colony panel. * * @see net.sf.freecol.client.gui.GUI */ public static final String MIN_NUMBER_FOR_DISPLAYING_GOODS = "model.option.guiMinNumberToDisplayGoods"; /** * Used by GUI, the number will be displayed when a group of goods are * higher than this number. * * @see net.sf.freecol.client.gui.MapViewer */ public static final String MIN_NUMBER_FOR_DISPLAYING_GOODS_COUNT = "model.option.guiMinNumberToDisplayGoodsCount"; /** * Used by GUI, this is the most repetitions drawn of a goods image for a * single goods grouping. * * @see net.sf.freecol.client.gui.MapViewer */ public static final String MAX_NUMBER_OF_GOODS_IMAGES = "model.option.guiMaxNumberOfGoodsImages"; /** * Whether to display a compass rose or not. */ public static final String DISPLAY_COMPASS_ROSE = "model.option.displayCompassRose"; /** * Whether to display the map controls or not. */ public static final String DISPLAY_MAP_CONTROLS = "model.option.displayMapControls"; /** * Whether to display the grid by default or not. */ public static final String DISPLAY_GRID = "model.option.displayGrid"; /** * Whether to delay on a unit's last move or not. * * TODO: Add this option's name and short description to languages other than English. */ public static final String UNIT_LAST_MOVE_DELAY = "model.option.unitLastMoveDelay"; /** * Pixmap setting to work around Java 2D graphics bug. */ public static final String USE_PIXMAPS = "model.option.usePixmaps"; /** * Whether to remember the positions of various panels. */ public static final String REMEMBER_PANEL_POSITIONS = "model.option.rememberPanelPositions"; /** * Whether to display borders by default or not. */ public static final String DISPLAY_BORDERS = "model.option.displayBorders"; /** * What text to display in the tiles. */ public static final String DISPLAY_TILE_TEXT = "model.option.displayTileText"; public static final int DISPLAY_TILE_TEXT_EMPTY = 0, DISPLAY_TILE_TEXT_NAMES = 1, DISPLAY_TILE_TEXT_OWNERS = 2, DISPLAY_TILE_TEXT_REGIONS = 3; /** * Animation speed for friendly units. */ public static final String MOVE_ANIMATION_SPEED = "model.option.moveAnimationSpeed"; /** * Animation speed for enemy units. */ public static final String ENEMY_MOVE_ANIMATION_SPEED = "model.option.enemyMoveAnimationSpeed"; /** * Used by GUI, this defines the grouping of ModelMessages. Possible values * include nothing, type and source. * * @see net.sf.freecol.client.gui.MapViewer * @see net.sf.freecol.common.model.ModelMessage */ public static final String MESSAGES_GROUP_BY = "model.option.guiMessagesGroupBy"; public static final int MESSAGES_GROUP_BY_NOTHING = 0; public static final int MESSAGES_GROUP_BY_TYPE = 1; public static final int MESSAGES_GROUP_BY_SOURCE = 2; public static final String AUDIO_MIXER = "model.option.audioMixer"; public static final String AUDIO_VOLUME = "model.option.audioVolume"; public static final String AUDIO_ALERTS = "model.option.audioAlerts"; /** * Used by GUI, this defines whether SoL messages will be displayed. * * @see net.sf.freecol.client.gui.MapViewer */ public static final String SHOW_COLONY_WARNINGS = "model.option.guiShowColonyWarnings"; public static final String SHOW_PRECOMBAT = "model.option.guiShowPreCombat"; public static final String SHOW_NOT_BEST_TILE = "model.option.guiShowNotBestTile"; public static final String SHOW_GOODS_MOVEMENT = "model.option.guiShowGoodsMovement"; /** * Use default values for savegames instead of displaying a dialog. <br> * <br> * Possible values for this option are: * <ol> * <li>{@link #SHOW_SAVEGAME_SETTINGS_NEVER}</li> * <li>{@link #SHOW_SAVEGAME_SETTINGS_MULTIPLAYER}</li> * <li>{@link #SHOW_SAVEGAME_SETTINGS_ALWAYS}</li> * </ol> */ public static final String SHOW_SAVEGAME_SETTINGS = "model.option.showSavegameSettings"; /** * A possible value for the {@link SelectOption}: * {@link #SHOW_SAVEGAME_SETTINGS}. Specifies that the dialog should never * be enabled. */ public static final int SHOW_SAVEGAME_SETTINGS_NEVER = 0; /** * A possible value for the {@link SelectOption}: * {@link #SHOW_SAVEGAME_SETTINGS}. Specifies that the dialog should only * be enabled when loading savegames being marked as multiplayer.. */ public static final int SHOW_SAVEGAME_SETTINGS_MULTIPLAYER = 1; /** * A possible value for the {@link SelectOption}: * {@link #SHOW_SAVEGAME_SETTINGS}. Specifies that the dialog should always * be enabled. */ public static final int SHOW_SAVEGAME_SETTINGS_ALWAYS = 2; /** * Option for setting the period of autosaves. The value 0 signals that * autosaving is disabled. */ public static final String AUTOSAVE_PERIOD = "model.option.autosavePeriod"; /** * Option for setting the number of autosaves to keep. If set to 0, all * autosaves are kept. */ public static final String AUTOSAVE_GENERATIONS = "model.option.autosaveGenerations"; /** * Option for setting the number of days autosaves are keep (valid time). If set to 0, * valid time is not checked. */ public static final String AUTOSAVE_VALIDITY = "model.option.autosaveValidity"; /** * Option for deleting autosaves when a new game is started. If set to * true, old autosaves will be deleted if a new game is started. */ public static final String AUTOSAVE_DELETE = "model.option.autosaveDelete"; /** * Option for activating autoscroll when dragging units on the mapboard. */ public static final String MAP_SCROLL_ON_DRAG = "model.option.mapScrollOnDrag"; /** * Option for activating autoscroll when dragging units on the mapboard. */ public static final String AUTO_SCROLL = "model.option.autoScroll"; /** * Option for autoload emigrants on saling to america. */ public static final String AUTOLOAD_EMIGRANTS = "model.option.autoloadEmigrants"; /** * If selected: Enables smooth rendering of the minimap when zoomed out. */ public static final String SMOOTH_MINIMAP_RENDERING = "model.option.smoothRendering"; /** * Default zoom level of the minimap. */ public static final String DEFAULT_MINIMAP_ZOOM = "model.option.defaultZoomLevel"; /** * The color to fill in around the actual map on the minimap. Typically only * visible when the minimap is at full zoom-out, but at the default 'black' * you can't differentiate between the background and the (unexplored) map. * Actually: clientOptions.minimap.color.background */ public static final String MINIMAP_BACKGROUND_COLOR = "model.option.color.background"; public static final String USER_MODS ="userMods"; /** * The Stock the custom house should keep when selling goods. */ public static final String CUSTOM_STOCK = "model.option.customStock"; /** * Generate warning of stock drops below this percentage of capacity. */ public static final String LOW_LEVEL = "model.option.lowLevel"; /** * Generate warning of stock exceeds this percentage of capacity. */ public static final String HIGH_LEVEL = "model.option.highLevel"; /** * Used by GUI to sort colonies. */ public static final String COLONY_COMPARATOR = "model.option.colonyComparator"; public static final int COLONY_COMPARATOR_NAME = 0, COLONY_COMPARATOR_AGE = 1, COLONY_COMPARATOR_POSITION = 2, COLONY_COMPARATOR_SIZE = 3, COLONY_COMPARATOR_SOL = 4; /** * If enabled: Automatically ends the turn when no units can be made active. */ public static final String AUTO_END_TURN = "model.option.autoEndTurn"; /** * Show EndTurnDialog. */ public static final String SHOW_END_TURN_DIALOG = "model.option.showEndTurnDialog"; /** * The type of labour report to display. */ public static final String LABOUR_REPORT = "model.option.labourReport"; public static final int LABOUR_REPORT_CLASSIC = 0; public static final int LABOUR_REPORT_COMPACT = 1; /** * Option for selecting the compact colony report. */ public static final String COLONY_REPORT = "model.option.colonyReport"; public static final int COLONY_REPORT_CLASSIC = 0; public static final int COLONY_REPORT_COMPACT = 1; /** * The Indian demand action. */ public static final String INDIAN_DEMAND_RESPONSE = "model.option.indianDemandResponse"; public static final int INDIAN_DEMAND_RESPONSE_ASK = 0; public static final int INDIAN_DEMAND_RESPONSE_ACCEPT = 1; public static final int INDIAN_DEMAND_RESPONSE_REJECT = 2; /** * The warehouse overflow on unload action. */ public static final String UNLOAD_OVERFLOW_RESPONSE = "model.option.unloadOverflowResponse"; public static final int UNLOAD_OVERFLOW_RESPONSE_ASK = 0; public static final int UNLOAD_OVERFLOW_RESPONSE_NEVER = 1; public static final int UNLOAD_OVERFLOW_RESPONSE_ALWAYS = 2; /** * Style of colony labels. */ public static final String COLONY_LABELS = "model.option.displayColonyLabels"; public static final int COLONY_LABELS_NONE = 0; public static final int COLONY_LABELS_CLASSIC = 1; public static final int COLONY_LABELS_MODERN = 2; /** * Style of map controls. */ public static final String MAP_CONTROLS = "model.option.mapControls"; public static final String MAP_CONTROLS_CORNERS = "CornerMapControls"; public static final String MAP_CONTROLS_CLASSIC = "ClassicMapControls"; /** * Comparators for sorting colonies. */ private static Comparator<Colony> colonyAgeComparator = new Comparator<Colony>() { public int compare(Colony s1, Colony s2) { if (s1.getEstablished().getNumber() > 0 && s2.getEstablished().getNumber() > 0) { return s1.getEstablished().getNumber() - s2.getEstablished().getNumber(); } else { // @compat 0.9.x return s1.getIntegerID().compareTo(s2.getIntegerID()); } // end compatibility code } }; private static Comparator<Colony> colonyNameComparator = new Comparator<Colony>() { public int compare(Colony s1, Colony s2) { return s1.getName().compareTo(s2.getName()); } }; private static Comparator<Colony> colonySizeComparator = new Comparator<Colony>() { // sort size descending, then SoL descending public int compare(Colony s1, Colony s2) { int dsize = s2.getUnitCount() - s1.getUnitCount(); if (dsize == 0) { return s2.getSoL() - s1.getSoL(); } else { return dsize; } } }; private static Comparator<Colony> colonySoLComparator = new Comparator<Colony>() { // sort SoL descending, then size descending public int compare(Colony s1, Colony s2) { int dsol = s2.getSoL() - s1.getSoL(); if (dsol == 0) { return s2.getUnitCount() - s1.getUnitCount(); } else { return dsol; } } }; private static Comparator<Colony> colonyPositionComparator = new Comparator<Colony>() { // sort north to south, then west to east public int compare(Colony s1, Colony s2) { int dy = s1.getTile().getY() - s2.getTile().getY(); if (dy == 0) { return s1.getTile().getX() - s2.getTile().getX(); } else { return dy; } } }; private Comparator<ModelMessage> messageSourceComparator = new Comparator<ModelMessage>() { // sort according to message source public int compare(ModelMessage message1, ModelMessage message2) { String sourceId1 = message1.getSourceId(); String sourceId2 = message2.getSourceId(); if (sourceId1 == sourceId2) { return messageTypeComparator.compare(message1, message2); } Game game = FreeCol.getFreeColClient().getGame(); FreeColGameObject source1 = game.getMessageSource(message1); FreeColGameObject source2 = game.getMessageSource(message2); int base = getClassIndex(source1) - getClassIndex(source2); if (base == 0) { if (source1 instanceof Colony) { return getColonyComparator().compare((Colony) source1, (Colony) source2); } } return base; } private int getClassIndex(Object object) { if (object instanceof Player) { return 10; } else if (object instanceof Colony) { return 20; } else if (object instanceof Europe) { return 30; } else if (object instanceof Unit) { return 40; } else if (object instanceof FreeColGameObject) { return 50; } else { return 1000; } } }; private Comparator<ModelMessage> messageTypeComparator = new Comparator<ModelMessage>() { // sort according to message type public int compare(ModelMessage message1, ModelMessage message2) { return message1.getMessageType().ordinal() - message2.getMessageType().ordinal(); } }; /** * Creates a new <code>ClientOptions</code>. * Unlike other OptionGroup classes, ClientOptions can not supply a * specification as it is needed before the specification is available. */ public ClientOptions() { super(getXMLElementTagName()); addDefaultOptions(); } /** * Adds the options to this <code>GameOptions</code>. */ private void addDefaultOptions() { loadOptions(new File(new File(FreeCol.getDataDirectory(), "base"), "client-options.xml")); } /** * Gets a list of active mods in this ClientOptions. * * @return A list of active mods. */ @SuppressWarnings("unchecked") public List<FreeColModFile> getActiveMods() { final Collection<FreeColModFile> fcmfs = Mods.getAllMods(); List<FreeColModFile> active = new ArrayList<FreeColModFile>(); ListOption<FreeColModFile> options = (ListOption<FreeColModFile>) getOption(ClientOptions.USER_MODS); if (options != null) { for (FreeColModFile modInfo : options.getOptionValues()) { if (modInfo != null) { for (FreeColModFile f : fcmfs) { if (modInfo.getId().equals(f.getId())) { active.add(f); break; } } } } } return active; } /** * Loads the options from the given file. * * @param optionsFile The <code>File</code> to read the options from. */ public void loadOptions(File optionsFile) { try { loadOptions(new BufferedInputStream(new FileInputStream(optionsFile))); } catch (FileNotFoundException e) { logger.warning("Could not find the client options file: " + optionsFile.getPath()); } } /** * Loads the options from the given stream. * * @param in The <code>InputStream</code> to read the options from. */ public void loadOptions(InputStream in) { if (in == null) return; try { XMLStreamReader xsr = XMLInputFactory.newInstance() .createXMLStreamReader(in, "UTF-8"); xsr.nextTag(); readFromXML(xsr); } catch (Exception e) { logger.log(Level.WARNING, "Exception when loading options.", e); } finally { try { in.close(); } catch (Exception e) { logger.log(Level.WARNING, "Exception when closing stream.", e); } } } /** * Updates the options from the given file. * * @param optionsFile The <code>File</code> to read the options from. */ public void updateOptions(File optionsFile) { try { updateOptions(new BufferedInputStream(new FileInputStream(optionsFile))); } catch (FileNotFoundException e) { logger.warning("Could not find the client options file: " + optionsFile.getPath()); } } /** * Updates the options from the given stream. * * @param in The <code>InputStream</code> to read the options from. */ public void updateOptions(InputStream in) { try { XMLStreamReader xsr = XMLInputFactory.newInstance() .createXMLStreamReader(in, "UTF-8"); xsr.nextTag(); readFromXML(xsr); } catch (Exception e) { logger.log(Level.WARNING, "Exception when loading options.", e); } finally { try { if (in != null) in.close(); } catch (Exception e) { logger.log(Level.WARNING, "Exception when closing stream.", e); } } } /** * Return the client's preferred tile text type. * * @return A <code>DISPLAY_TILE_TEXT_</code> value */ public int getDisplayTileText() { return getInteger(DISPLAY_TILE_TEXT); } /** * Gets a fresh list of sorted colonies for a player. * Helper function for a common idiom. * * @param p The <code>Player</code> to get the colony list for. * @return A fresh list of colonies sorted according to the * current comparator. */ public List<Colony> getSortedColonies(Player p) { return p.getSortedColonies(getColonyComparator()); } /** * Return the client's preferred comparator for colonies. * * @return a <code>Comparator</code> value */ public Comparator<Colony> getColonyComparator() { return getColonyComparator(getInteger(COLONY_COMPARATOR)); } /** * Return the colony comparator identified by type. * * @param type an <code>int</code> value * @return a <code>Comparator</code> value */ public static Comparator<Colony> getColonyComparator(int type) { switch (type) { case COLONY_COMPARATOR_AGE: return colonyAgeComparator; case COLONY_COMPARATOR_POSITION: return colonyPositionComparator; case COLONY_COMPARATOR_SIZE: return colonySizeComparator; case COLONY_COMPARATOR_SOL: return colonySoLComparator; case COLONY_COMPARATOR_NAME: return colonyNameComparator; default: throw new IllegalStateException("Unknown comparator"); } } /** * Return the client's preferred comparator for ModelMessages. * * @return a <code>Comparator</code> value */ public Comparator<ModelMessage> getModelMessageComparator() { switch (getInteger(MESSAGES_GROUP_BY)) { case MESSAGES_GROUP_BY_SOURCE: return messageSourceComparator; case MESSAGES_GROUP_BY_TYPE: return messageTypeComparator; default: return null; } } /** * Returns the boolean option associated with a ModelMessage. * * @param message a <code>ModelMessage</code> value * @return a <code>BooleanOption</code> value */ public BooleanOption getBooleanOption(ModelMessage message) { return (BooleanOption) getOption(message.getMessageType().getOptionName()); } /** * Perform backward compatibility fixups on new client options as * they are introduced. Annotate with introduction version so we * can clean these up as they become standard. */ public void fixClientOptions() { // @compat 0.9.x addBooleanOption("model.option.guiShowDemands", "clientOptions.messages", true); addBooleanOption("model.option.guiShowGifts", "clientOptions.messages", true); addBooleanOption("model.option.guiShowGoodsMovement", "clientOptions.messages", true); // @compat 0.10.1 addIntegerOption(COLONY_REPORT, "clientOptions.messages", 0); addBooleanOption(USE_PIXMAPS, "clientOptions.gui", true); } private void addBooleanOption(String id, String gr, boolean val) { if (getOption(id) == null) { BooleanOption op = new BooleanOption(id); op.setGroup(gr); op.setValue(val); add(op); } } private void addIntegerOption(String id, String gr, int val) { if (getOption(id) == null) { IntegerOption op = new IntegerOption(id); op.setGroup(gr); op.setValue(val); add(op); } } protected boolean isCorrectTagName(String tagName) { return getXMLElementTagName().equals(tagName); } /** * Gets the tag name of the root element representing this object. * * @return "clientOptions". */ public static String getXMLElementTagName() { return "clientOptions"; } }