freecol/src/net/sf/freecol/client/ClientOptions.java

1036 lines
37 KiB
Java

/**
* Copyright (C) 2002-2022 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 static net.sf.freecol.common.util.CollectionUtils.makeUnmodifiableList;
import static net.sf.freecol.common.util.CollectionUtils.makeUnmodifiableMap;
import static net.sf.freecol.common.util.CollectionUtils.toListNoNulls;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import java.awt.Dimension;
import java.awt.Point;
import java.io.File;
import java.io.IOException;
import java.text.Collator;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.FreeCol;
import net.sf.freecol.common.FreeColException;
import net.sf.freecol.common.io.FreeColDirectories;
import net.sf.freecol.common.io.FreeColModFile;
import net.sf.freecol.common.io.FreeColSavegameFile;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.option.BooleanOption;
import net.sf.freecol.common.option.IntegerOption;
import net.sf.freecol.common.option.Option;
import net.sf.freecol.common.option.OptionGroup;
import net.sf.freecol.common.option.PercentageOption;
import net.sf.freecol.common.option.RangeOption;
import net.sf.freecol.common.option.TextOption;
import net.sf.freecol.common.util.LogBuilder;
/**
* Defines how available client options are displayed on the Setting
* dialog from File/Preferences Also contains several Comparators used
* for display purposes.
*
* Most available client options and their default values are defined
* in the file {@code base/client-options.xml} in the FreeCol
* data directory. They are overridden by the player's personal
* settings in the file {@code options.xml} in the user
* directory. Note that some options are generated and added dynamically.
*
* 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}:
*
* <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());
public static final String TAG = "clientOptions";
//
// Constants for each client option.
// Keep these in sync with data/base/client-options.xml.
//
// clientOptions.personal
private static final String PERSONAL_GROUP
= "clientOptions.personal";
/** Option for the player's preferred name. */
public static final String NAME
= "model.option.playerName";
/** Special option for setting the language. */
public static final String LANGUAGE
= "model.option.languageOption";
/** Value for automatic language selection. */
public static final String AUTOMATIC
= "clientOptions.gui.languageOption.autoDetectLanguage";
// clientOptions.gui is an old heading, the constant is still used
// in fixClientOptions below. Several "value" constants still
// have clientOptions.gui.* names.
private static final String GUI_GROUP
= "clientOptions.gui";
// clientOptions.display
private static final String DISPLAY_GROUP
= "clientOptions.display";
/**
* Option to control the display scale factor.
*/
public static final String DISPLAY_SCALING
= "model.option.displayScaling";
/**
* Enable manual override of main font size.
*/
public static final String MANUAL_MAIN_FONT_SIZE
= "model.option.manualMainFontSize";
/**
* Value to use to override the main font size, if enabled by the above.
*/
public static final String MAIN_FONT_SIZE
= "model.option.mainFontSize";
/** Pixmap setting to work around Java 2D graphics bug. */
public static final String USE_PIXMAPS
= "model.option.usePixmaps";
/** Enable use of openGL pipeline. */
public static final String USE_OPENGL
= "model.option.useOpenGL";
/** Enable use of XRender pipeline (unix specific). */
public static final String USE_XRENDER
= "model.option.useXRender";
public static final String USE_TERRAIN_ANIMATIONS
= "model.option.useTerrainAnimations";
/** Allows the user to determine the tradeoff between quality and rendering speed. */
public static final String GRAPHICS_QUALITY
= "model.option.graphicsQuality";
public static final int GRAPHICS_QUALITY_LOWEST = 0;
public static final int GRAPHICS_QUALITY_LOW = 10;
public static final int GRAPHICS_QUALITY_NORMAL = 20;
public static final int GRAPHICS_QUALITY_HIGH = 30;
public static final int GRAPHICS_QUALITY_HIGHEST = 40;
/** Animation speed for our 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";
/** Animation speed for friendly units. */
public static final String FRIENDLY_MOVE_ANIMATION_SPEED
= "model.option.friendlyMoveAnimationSpeed";
// clientOptions.interface
private static final String INTERFACE_GROUP
= "clientOptions.interface";
// clientOptions.interface.mapView
private static final String MAPVIEW_GROUP
= "clientOptions.interface.mapView";
/** Default zoom level of the minimap. */
public static final String DEFAULT_ZOOM_LEVEL
= "model.option.defaultZoomLevel";
/**
* Selected tiles always gets centered if this option is enabled (even if
* the tile is on screen.
*/
public static final String ALWAYS_CENTER
= "model.option.alwaysCenter";
/**
* If this option is enabled, the display will recenter in order
* to display the active unit if it is not on screen.
*/
public static final String JUMP_TO_ACTIVE_UNIT
= "model.option.jumpToActiveUnit";
/** Option to scroll when dragging units on the mapboard. */
public static final String MAP_SCROLL_ON_DRAG
= "model.option.mapScrollOnDrag";
/** Option to auto-scroll on mouse movement. */
public static final String AUTO_SCROLL
= "model.option.autoScroll";
/** Whether to display the grid by default or not. */
public static final String DISPLAY_GRID
= "model.option.displayGrid";
/** Whether to display borders by default or not. */
public static final String DISPLAY_BORDERS
= "model.option.displayBorders";
/** Whether to draw the fog of war on the actual map or not. */
public static final String DISPLAY_FOG_OF_WAR
= "model.option.displayFogOfWar";
/** Whether to delay on a unit's last move or not. */
public static final String UNIT_LAST_MOVE_DELAY
= "model.option.unitLastMoveDelay";
/** 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;
/** Style of colony labels. */
public static final String DISPLAY_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;
// clientOptions.interface.mapControls
private static final String MAPCONTROLS_GROUP
= "clientOptions.interface.mapControls";
/** 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";
/** The type of map controls, corner or classic. */
public static final String MAP_CONTROLS
= "model.option.mapControls";
/** Styles of map controls. */
public static final String MAP_CONTROLS_CORNERS
= "clientOptions.gui.mapControls.CornerMapControls";
public static final String MAP_CONTROLS_CLASSIC
= "clientOptions.gui.mapControls.ClassicMapControls";
/** Whether to draw the fog of war on the minimap. */
public static final String MINIMAP_TOGGLE_FOG_OF_WAR
= "model.option.miniMapToggleFogOfWar";
/** Whether to draw the borders on the minimap. */
public static final String MINIMAP_TOGGLE_BORDERS
= "model.option.miniMapToggleBorders";
/**
* 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";
// clientOptions.interface.messages
private static final String MESSAGES_GROUP
= "clientOptions.messages"; // should be .interface.warehouse
/**
* Used by GUI, this defines the grouping of ModelMessages.
* Possible values include nothing, type and source.
*
* @see net.sf.freecol.client.gui.mapviewer.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,
MESSAGES_GROUP_BY_TYPE = 1,
MESSAGES_GROUP_BY_SOURCE = 2;
/** Show goods movement messages. Used by followTradeRoute. */
public static final String SHOW_GOODS_MOVEMENT
= "model.option.guiShowGoodsMovement";
/** Show warnings about colony sites. */
public static final String SHOW_COLONY_WARNINGS
= "model.option.guiShowColonyWarnings";
/** Show the pre-combat dialog? */
public static final String SHOW_PRECOMBAT
= "model.option.guiShowPreCombat";
/** Show warnings about suboptimal colony tile choice. */
public static final String SHOW_NOT_BEST_TILE
= "model.option.guiShowNotBestTile";
public static final String SHOW_REGION_NAMING
= "model.option.guiShowRegionNaming";
/** 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 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;
// clientOptions.interface.warehouse
private static final String WAREHOUSE_GROUP
= "clientOptions.warehouse"; // should be .interface.warehouse
/** The amount of 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";
/**
* Should trade route units check production to determine goods levels at
* stops along its route?
*/
public static final String STOCK_ACCOUNTS_FOR_PRODUCTION
= "model.option.stockAccountsForProduction";
// clientOptions.other
private static final String OTHER_GROUP
= "clientOptions.other"; // should be .interface.other
/** Whether to remember the positions of various dialogs and panels. */
public static final String REMEMBER_PANEL_POSITIONS
= "model.option.rememberPanelPositions";
/** Whether to remember the sizes of various dialogs and panels. */
public static final String REMEMBER_PANEL_SIZES
= "model.option.rememberPanelSizes";
/** Whether to display end turn grey background or not. */
public static final String DISABLE_GRAY_LAYER
= "model.option.disableGrayLayer";
/** Option to autoload emigrants on sailing to america. */
public static final String AUTOLOAD_EMIGRANTS
= "model.option.autoloadEmigrants";
/** Option to autoload sentried units. */
public static final String AUTOLOAD_SENTRIES
= "model.option.autoloadSentries";
/** Automatically end the turn when no units can be * made active. */
public static final String AUTO_END_TURN
= "model.option.autoEndTurn";
/** Show the end turn dialog. */
public static final String SHOW_END_TURN_DIALOG
= "model.option.showEndTurnDialog";
/** Set the default native 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;
/** Set the default 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;
/**
* 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.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.MapViewer
*/
public static final String MAX_NUMBER_OF_GOODS_IMAGES
= "model.option.guiMaxNumberOfGoodsImages";
/**
* 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 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;
// clientOptions.savegames
private static final String SAVEGAMES_GROUP
= "clientOptions.savegames";
/** Use default values for savegames instead of displaying a dialog. */
public static final String SHOW_SAVEGAME_SETTINGS
= "model.option.showSavegameSettings";
public static final int SHOW_SAVEGAME_SETTINGS_NEVER = 0,
SHOW_SAVEGAME_SETTINGS_MULTIPLAYER = 1,
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 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";
/**
* Whether to display confirmation for the overwrite of existing
* save files.
*/
public static final String CONFIRM_SAVE_OVERWRITE
= "model.option.confirmSaveOverwrite";
/** Prefix for the auto-save file. */
public static final String AUTO_SAVE_PREFIX
= "model.option.autoSavePrefix";
/** Stem of the last-turn save file name. */
public static final String LAST_TURN_NAME
= "model.option.lastTurnName";
/** Stem of the before-last-turn save file name. */
public static final String BEFORE_LAST_TURN_NAME
= "model.option.beforeLastTurnName";
// clientOptions.audio
private static final String AUDIO_GROUP
= "clientOptions.audio";
/** Choose the default mixer. */
public static final String AUDIO_MIXER
= "model.option.audioMixer";
/** The volume level to set. */
public static final String AUDIO_VOLUME
= "model.option.audioVolume";
/** The volume level to set for the music. */
public static final String MUSIC_VOLUME
= "model.option.musicVolume";
/** The volume level to set. */
public static final String SOUND_EFFECTS_VOLUME
= "model.option.soundEffectsVolume";
/** Play an alert sound on message arrival. */
public static final String AUDIO_ALERTS
= "model.option.audioAlerts";
// clientOptions.mods
/** The mods. */
public static final String USER_MODS
= "clientOptions.mods.userMods";
// clientOptions.etc
/** The dynamic placement options. */
public static final String ETC
= "clientOptions.etc";
// The special keys that are read early.
private static final List<String> specialKeys
= makeUnmodifiableList(LANGUAGE, USE_OPENGL, USE_PIXMAPS, USE_XRENDER);
// Comparators for sorting colonies.
/** Compare by ascending age. */
private static final Comparator<Colony> colonyAgeComparator
= Comparator.comparingInt(c -> c.getEstablished().getNumber());
/** Compare by name, initialized at run time. */
private static AtomicReference<Comparator<Colony>> colonyNameComparator
= new AtomicReference<Comparator<Colony>>(null);
/** Compare by descending size then liberty. */
private static final Comparator<Colony> colonySizeComparator
= Comparator.comparingInt(Colony::getUnitCount)
.thenComparingInt(Colony::getSonsOfLiberty)
.reversed();
/** Compare by descending liberty then size. */
private static final Comparator<Colony> colonySoLComparator
= Comparator.comparingInt(Colony::getSonsOfLiberty)
.thenComparingInt(Colony::getUnitCount)
.reversed();
/** Compare by position on the map. */
private static final Comparator<Colony> colonyPositionComparator
= Comparator.comparingInt(c -> Location.rankOf(c));
/** Friendly move animation speed option values. */
private static final Map<Integer, String> friendlyMoveAnimationSpeeds
= makeUnmodifiableMap(new Integer[] { 0, 1, 2, 3 },
new String[] {
"clientOptions.gui.friendlyMoveAnimationSpeed.off",
"clientOptions.gui.friendlyMoveAnimationSpeed.slow",
"clientOptions.gui.friendlyMoveAnimationSpeed.normal",
"clientOptions.gui.friendlyMoveAnimationSpeed.fast"
});
private static final Map<Integer, String> graphicsQualityChoices
= makeUnmodifiableMap(new Integer[] {
GRAPHICS_QUALITY_LOWEST,
GRAPHICS_QUALITY_LOW,
GRAPHICS_QUALITY_NORMAL,
GRAPHICS_QUALITY_HIGH,
GRAPHICS_QUALITY_HIGHEST
},
new String[] {
"clientOptions.gui.graphicsQuality.lowest",
"clientOptions.gui.graphicsQuality.low",
"clientOptions.gui.graphicsQuality.normal",
"clientOptions.gui.graphicsQuality.high",
"clientOptions.gui.graphicsQuality.highest"
});
/**
* Creates a new {@code ClientOptions}.
*
* Unlike other OptionGroup classes, ClientOptions can not supply a
* specification as it is needed before the specification is available.
*/
public ClientOptions() {
super(TAG);
}
/**
* Loads the options from the given save game.
*
* @param save The {@code FreeColSaveGame} to read the options from.
* @return True if the options were loaded without error.
*/
private boolean load(FreeColSavegameFile save) {
if (save == null) return false;
boolean ret = false;
try (
FreeColXMLReader xr = save.getClientOptionsFreeColXMLReader();
) {
ret = load(xr);
} catch (IOException|XMLStreamException ex) {
logger.log(Level.WARNING, "Load OptionGroup(" + getId()
+ ") from file " + save.getPath() + " crashed", ex);
return false;
}
logger.info("Load OptionGroup(" + getId() + ") from " + save.getPath()
+ ((ret) ? " succeeded" : " failed"));
return ret;
}
/**
* Merge the options from the given file game.
*
* @param file The {@code File} to merge the options from.
* @return True if the options were merge without error.
*/
public boolean merge(File file) {
ClientOptions clop = new ClientOptions();
if (!clop.load(file)) return false;
LogBuilder lb = new LogBuilder(64);
boolean ret = this.merge(clop, lb);
lb.shrink("\n"); lb.log(logger, Level.FINEST);
return ret;
}
/**
* Merge the options from the given save game.
*
* @param save The {@code FreeColSaveGame} to merge from.
* @return True if the options were merged without error.
*/
public boolean merge(FreeColSavegameFile save) {
ClientOptions clop = new ClientOptions();
if (!clop.load(save)) return false;
LogBuilder lb = new LogBuilder(64);
/** Only the mods should be loaded from the savegame: */
boolean ret = this.merge(clop.getOption(USER_MODS), lb);
lb.shrink("\n"); lb.log(logger, Level.FINEST);
return ret;
}
/**
* Gets a list of active mods in this ClientOptions.
*
* @return A list of active mods.
*/
public List<FreeColModFile> getActiveMods() {
final Predicate<FreeColModFile> validModPred = m ->
m != null && m.getId() != null;
final Function<FreeColModFile, FreeColModFile> modFileMapper = m ->
FreeColModFile.getFreeColModFile(m.getId());
return transform(getModList(ClientOptions.USER_MODS), validModPred,
modFileMapper, toListNoNulls());
}
/**
* Get the client's preferred tile text type.
*
* @return A {@code DISPLAY_TILE_TEXT_} value
*/
public int getDisplayTileText() {
return getInteger(DISPLAY_TILE_TEXT);
}
/**
* Get the client's preferred comparator for colonies.
*
* @return The current colony {@code Comparator}.
*/
public Comparator<Colony> getColonyComparator() {
return getColonyComparatorInternal(getInteger(COLONY_COMPARATOR));
}
/**
* Get the colony comparator identified by type.
*
* @param type The colony comparator option integer value.
* @return The corresponding colony {@code Comparator}.
*/
private static Comparator<Colony> getColonyComparatorInternal(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:
Comparator<Colony> cnc = colonyNameComparator.get();
if (cnc == null) {
// Can not be done statically, must wait for CLI parsing
cnc = Comparator.comparing(Colony::getName,
Collator.getInstance(FreeCol.getLocale()));
colonyNameComparator.set(cnc);
}
return cnc;
default:
throw new RuntimeException("Unknown comparator: " + type);
}
}
/**
* Return the client's preferred comparator for ModelMessages.
*
* @param game The {@code Game} to extract a comparator for.
* @return The preferred {@code Comparator}.
*/
public Comparator<ModelMessage> getModelMessageComparator(Game game) {
switch (getInteger(MESSAGES_GROUP_BY)) {
case MESSAGES_GROUP_BY_SOURCE:
Map<String, Comparator<?>> specialized
= new HashMap<>();
specialized.put("Colony", getColonyComparator());
return ModelMessage.getSourceComparator(game, specialized);
case MESSAGES_GROUP_BY_TYPE:
return ModelMessage.messageTypeComparator;
default:
return null;
}
}
public boolean isRiverAnimationEnabled() {
return isTerrainAnimationsEnabled() && getRange(ClientOptions.GRAPHICS_QUALITY) >= ClientOptions.GRAPHICS_QUALITY_NORMAL;
}
public boolean isTerrainAnimationsEnabled() {
return getBoolean(ClientOptions.USE_TERRAIN_ANIMATIONS) && getRange(ClientOptions.GRAPHICS_QUALITY) >= ClientOptions.GRAPHICS_QUALITY_LOW;
}
/**
* 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.11.0
addBooleanOption(MINIMAP_TOGGLE_BORDERS,
GUI_GROUP, true);
addBooleanOption(MINIMAP_TOGGLE_FOG_OF_WAR,
GUI_GROUP, true);
addTextOption(AUTO_SAVE_PREFIX,
SAVEGAMES_GROUP, "Autosave");
addTextOption(LAST_TURN_NAME,
SAVEGAMES_GROUP, "last-turn");
addTextOption(BEFORE_LAST_TURN_NAME,
SAVEGAMES_GROUP, "before-last-turn");
// end @compat 0.11.0
// @compat 0.11.1
addBooleanOption(STOCK_ACCOUNTS_FOR_PRODUCTION,
WAREHOUSE_GROUP, false);
// end @compat 0.11.1
// @compat 0.11.3
addBooleanOption(AUTOLOAD_SENTRIES,
OTHER_GROUP, false);
try { // Zoom range was increased
RangeOption ro = getOption(DEFAULT_ZOOM_LEVEL, RangeOption.class);
if (ro.getItemValues().size() != 6) {
Integer value = ro.getValue();
ro.clearItemValues();
ro.addItemValue(1, "1");
ro.addItemValue(2, "2");
ro.addItemValue(3, "3");
ro.addItemValue(4, "4");
ro.addItemValue(5, "5");
ro.addItemValue(6, "6");
ro.setValue(value); // Make sure the value is valid
}
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to fix " + DEFAULT_ZOOM_LEVEL
+ " option", e);
}
// end @compat 0.11.3
// @compat 0.11.6
addBooleanOption(SHOW_REGION_NAMING,
MESSAGES_GROUP, true);
// These have computed keys in ModelMessage.MessageType
addBooleanOption("model.option.guiShowCombatResult",
MESSAGES_GROUP, true);
addBooleanOption("model.option.guiShowUnitRepaired",
MESSAGES_GROUP, true);
addBooleanOption("model.option.guiShowUnitArrived",
MESSAGES_GROUP, true);
addBooleanOption("model.option.guiShowDisasters",
MESSAGES_GROUP, true);
addBooleanOption(USE_OPENGL,
GUI_GROUP, true);
addBooleanOption(USE_XRENDER,
GUI_GROUP, true);
addRangeOption(FRIENDLY_MOVE_ANIMATION_SPEED,
GUI_GROUP, 3, friendlyMoveAnimationSpeeds);
// Reorg ~early 2022
// - model.option.smoothRendering is no longer used
// - ClientOptions.GUI was split into DISPLAY_GROUP and INTERFACE_GROUP
// - LANGUAGE is now in the PERSONAL group
regroup(LANGUAGE, PERSONAL_GROUP);
// - DISPLAY_SCALING added to DISPLAY_GROUP
// - font overrides added to DISPLAY_GROUP
// - USE_* booleans and animation speed move to DISPLAY_GROUP
addOptionGroup(DISPLAY_GROUP, TAG);
addIntegerOption(DISPLAY_SCALING,
DISPLAY_GROUP, 0);
addBooleanOption(MANUAL_MAIN_FONT_SIZE,
DISPLAY_GROUP, false);
addIntegerOption(MAIN_FONT_SIZE,
DISPLAY_GROUP, 12);
for (String n: new String[] {
USE_PIXMAPS, USE_OPENGL, USE_XRENDER,
MOVE_ANIMATION_SPEED, ENEMY_MOVE_ANIMATION_SPEED,
FRIENDLY_MOVE_ANIMATION_SPEED }) {
regroup(n, DISPLAY_GROUP);
}
// Most of the remaining options move to the MAPVIEW_GROUP...
addOptionGroup(INTERFACE_GROUP, TAG);
addOptionGroup(MAPVIEW_GROUP, INTERFACE_GROUP);
for (String n: new String[] {
DEFAULT_ZOOM_LEVEL, ALWAYS_CENTER, JUMP_TO_ACTIVE_UNIT,
MAP_SCROLL_ON_DRAG, AUTO_SCROLL,
DISPLAY_GRID, DISPLAY_BORDERS, UNIT_LAST_MOVE_DELAY,
DISPLAY_TILE_TEXT, DISPLAY_COLONY_LABELS }) {
regroup(n, MAPVIEW_GROUP);
}
// ...except the minimap controls which are in MAPCONTROLS_GROUP
addOptionGroup(MAPCONTROLS_GROUP, INTERFACE_GROUP);
for (String n: new String[] {
DISPLAY_COMPASS_ROSE, DISPLAY_MAP_CONTROLS,
MINIMAP_TOGGLE_FOG_OF_WAR, MINIMAP_TOGGLE_BORDERS,
//MINIMAP_SMOOTH_RENDERING,
MINIMAP_BACKGROUND_COLOR }) {
regroup(n, MAPCONTROLS_GROUP);
}
// MESSAGES,WAREHOUSE,OTHER_GROUP move to INTERFACE_GROUP
regroup(MESSAGES_GROUP, INTERFACE_GROUP);
regroup(WAREHOUSE_GROUP, INTERFACE_GROUP);
regroup(OTHER_GROUP, INTERFACE_GROUP);
addBooleanOption(DISPLAY_FOG_OF_WAR, MAPCONTROLS_GROUP, false);
// end @compat 0.11.6
// @compat 0.12.0
/* Gone after 0.13.0:
final PercentageOption volumeOption = getOption(AUDIO_VOLUME, PercentageOption.class);
volumeOption.setPreviewEnabled(true);
*/
addRangeOption(GRAPHICS_QUALITY, DISPLAY_GROUP, 20, graphicsQualityChoices);
addBooleanOption(USE_TERRAIN_ANIMATIONS, DISPLAY_GROUP, true);
// end @compat 0.12.0
// @compat 0.13.0
addPercentageOption(MUSIC_VOLUME, AUDIO_GROUP, 100);
addPercentageOption(SOUND_EFFECTS_VOLUME, AUDIO_GROUP, 100);
// end @compat 0.13.0
// @compat 1.1.0
remove("model.option.mapControls");
// end @compat 1.1.0
}
private void addPercentageOption(String id, String gr, int val) {
if (!hasOption(id, PercentageOption.class)) {
PercentageOption op = new PercentageOption(id, null);
op.setGroup(gr);
op.setValue(val);
add(op);
}
}
private void addBooleanOption(String id, String gr, boolean val) {
if (!hasOption(id, BooleanOption.class)) {
BooleanOption op = new BooleanOption(id, null);
op.setGroup(gr);
op.setValue(val);
add(op);
}
}
private void addIntegerOption(String id, String gr, int val) {
if (!hasOption(id, IntegerOption.class)) {
IntegerOption op = new IntegerOption(id, null);
op.setGroup(gr);
op.setValue(val);
add(op);
}
}
private void addOptionGroup(String id, String gr) {
if (!hasOption(id, OptionGroup.class)) {
OptionGroup og = new OptionGroup(id);
og.setGroup(gr);
add(og);
}
}
/**
* Add a new range option.
*
* @param id The option identifier.
* @param gr The group identifier.
* @param rank The option rank.
* @param entries The options in the range.
*/
private void addRangeOption(String id, String gr, int rank,
Map<Integer, String> entries) {
if (!hasOption(id, RangeOption.class)) {
RangeOption op = new RangeOption(id, (Specification)null);
op.setGroup(gr);
op.clearItemValues();
for (Entry<Integer, String> e : entries.entrySet()) {
op.addItemValue(e.getKey(), e.getValue());
}
op.setValueRank(rank);
add(op);
}
}
/**
* Add a new text option.
*
* @param id The option identifier.
* @param gr The group identifier.
* @param val The the text value.
*/
private void addTextOption(String id, String gr, String val) {
if (!hasOption(id, TextOption.class)) {
TextOption op = new TextOption(id, null);
op.setGroup(gr);
op.setValue(val);
add(op);
}
}
/**
* Move an option to a different group.
*
* @param id The identifier for the option to move.
* @param gr The identifier for the option group to move to.
*/
private void regroup(String id, String gr) {
Option op = getOption(id);
if (op != null) op.setGroup(gr);
}
/**
* Extract the panel position options for a given panel class name
* as a point.
*
* @param className The panel class name.
* @return A {@code Point} for the position if found, else null.
*/
public Point getPanelPosition(String className) {
OptionGroup etc = getOptionGroup(ETC);
try {
return (etc == null) ? null
: new Point(etc.getInteger(className + ".x"),
etc.getInteger(className + ".y"));
} catch (Exception ex) {
logger.finest("Missing position option for " + className);
return null;
}
}
/**
* Extract the panel size options for a given panel class name as
* a dimension.
*
* @param className The panel class name.
* @return A {@code Dimension} for the size if found, else null.
*/
public Dimension getPanelSize(String className) {
OptionGroup etc = getOptionGroup(ETC);
try {
return (etc == null) ? null
: new Dimension(etc.getInteger(className + ".w"),
etc.getInteger(className + ".h"));
} catch (Exception ex) {
logger.finest("Missing size option for " + className);
return null;
}
}
/**
* Extracts the special options from the client options file.
*
* Note: called early, the logger is not yet reliably available.
*
* @return A map of the special options and the values found for them.
* @exception FreeColException if there is a problem reading the stream.
*/
public static Map<String,String> getSpecialOptions()
throws FreeColException {
// Initialize the map
Map<String, String> ret = new HashMap<>(specialKeys.size());
for (String key : specialKeys) ret.put(key, null);
// Extract the values and return
final File optionsFile = FreeColDirectories.getClientOptionsFile();
try (FreeColXMLReader xr = new FreeColXMLReader(optionsFile)) {
xr.readAttributeValues(ret, "value");
} catch (IOException|XMLStreamException xe) {
throw new FreeColException(xe);
}
return ret;
}
// Serialization
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
}