package net.sf.colossus.common;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Class Options lists game options for Colossus.
*
* @author David Ripton
*
* TODO constants should be all uppercase
*/
public final class Options implements IOptions
{
private static final Logger LOGGER = Logger.getLogger(Options.class
.getName());
// Everything is public because we use this class in both the client
// and server packages. (With separate data.)
// Non-options that use the options framework
// Will add player numbers 0 through n-1 to the end of these.
public static final String playerName = "Player name ";
public static final String playerType = "Player type ";
public static final String runClientPlayer = "Network client playername";
public static final String runClientHost = "Network client hostname";
public static final String runClientPort = "Network client port";
public static final String runSpectatorClient = "Spectator client";
public static final String loadGameFileName = "Load game file name";
public static final String webFlagFileName = "Web game flag file";
public static final String serveAtPort = "Run server on port";
public static final String FORCE_BOARD = "Force View Board";
public static final String webServerHost = "Web server name";
public static final String webServerPort = "Web server port";
public static final String webClientLogin = "Web client login";
public static final String webClientPassword = "Web client password";
public static final String proposedGamesTableOption = "Proposed Games Table Column Widths";
// Option names
// Server administrative options
public static final String autosave = "Autosave";
public static final String autosaveMaxKeep = "Max autosave files";
public static final String autosaveVerboseNames = "Verbose autosave names";
public static final String autoStop = "AIs stop when humans dead";
public static final String autoQuit = "Auto quit when game over";
public static final String goOnWithoutObserver = "Go on without observer";
public static final String hotSeatMode = "Hot seat mode";
public static final String keepAccepting = "Keep accepting clients";
public static final String diceStatisticsFile = "Dice statistics file";
public static final String lastJava7Warning = "Last Java 7 warning";
// Rules options
public static final String variant = "Variant";
public static final String variantFileWithFullPath = "Variant full path";
public static final String viewMode = "ViewMode";
public static final String dubiousAsBlanks = "Uncertain as blank (Autoinspector etc.)";
public static final String showMarker = "Show Marker (Autoinspector etc.)";
public static final String localOnlyOwn = "Locally only own legions";
// Web Client specific:
public static final String minPlayersWeb = "Min Players Web Client";
public static final String targPlayersWeb = "Target Players Web Client";
public static final String maxPlayersWeb = "Max Players Web Client";
public static final String uponMessageToFront = "Window to front for message";
/* selections for viewable Legions */
public static final String viewableOwn = "Only own legions";
public static final String viewableLast = "Revealed during last turn";
public static final String viewableEver = "Ever revealed (or concludable) since start";
public static final String viewableAll = "True content for all legions";
public static final String[] viewModeArray = { viewableOwn, viewableEver,
viewableAll };
public static final int viewableOwnNum = 1;
public static final int viewableLastNum = 2;
public static final int viewableEverNum = 3;
public static final int viewableAllNum = 4;
public static final String eventExpiring = "EventExpire";
public static final String eventExpiringNever = "never";
public static final String[] eventExpiringChoices = { "1", "2", "5", "10",
eventExpiringNever };
public static final String balancedTowers = "Balanced starting towers";
public static final String allStacksVisible = "All stacks visible";
public static final String onlyOwnLegions = "Only own legions viewable";
public static final String cumulativeSlow = "Slowing is cumulative";
public static final String oneHexAllowed = "Always allows one hex";
public static final String fixedSequenceBattleDice = "Fixed-sequence battle dice";
public static final String pbBattleHits = "Probability-based battle hits";
public static final String sansLordAutoBattle = "Need lord for battle control";
public static final String inactivityTimeout = "Inactivity timeout";
public static final String noFirstTurnT2TTeleport = "No tower-to-tower Teleport on first turn";
public static final String noFirstTurnTeleport = "No Teleport on first turn";
public static final String towerToTowerTeleportOnly = "Tower-to-Tower Teleport only";
public static final String noTowerTeleport = "No Tower Teleport";
public static final String noTitanTeleport = "No Titan Teleport";
public static final String noFirstTurnWarlockRecruit = "No Warlock recruiting on first turn";
public static final String unlimitedMulligans = "Unlimited Mulligans";
public static final String enableEditingMode = "Enable Editing Mode";
// Those are global options which need to be transferred to clients, even if
// not set (meaning false), but if Client has them stored from earlier synchronizations
// (e.g. removed server.cf file, or now playing on public server)
public static final String[] globalGameOptions = { cumulativeSlow,
oneHexAllowed, fixedSequenceBattleDice, sansLordAutoBattle,
noFirstTurnT2TTeleport, noFirstTurnTeleport, towerToTowerTeleportOnly,
noTowerTeleport, noTitanTeleport, unlimitedMulligans,
noFirstTurnWarlockRecruit, enableEditingMode, inactivityTimeout };
// Display options (client only)
public static final String stealFocus = "Steal focus";
public static final String turnStartBeep = "When my turns starts, beep";
public static final String turnStartToFront = "When my turn starts, window to front";
public static final String turnStartBottomBarYellow = "When my turn starts, turn BottomBar yellow";
public static final String turnStartChatYellow = "When my turn starts, turn Chat background yellow";
public static final String BattleTerrainHazardWindow = "Show Terrain and Hazards";
public static final String showCaretaker = "Show Caretaker's stacks";
public static final String showStatusScreen = "Show game status";
public static final String showAutoInspector = "Show inspector";
public static final String showEventViewer = "Show event window";
public static final String showLogWindow = "Show log window";
public static final String showConnectionLogWindow = "Show connection log window";
public static final String showWebClient = "Show web client";
public static final String suppressedWelcomeDialog = "Suppressed Welcome Dialog";
public static final String showEngagementResults = "Show engagement results";
public static final String useOverlay = "Use Graphical Overlay";
public static final String noBaseColor = "Use black overlay on Chits";
public static final String playerColoredAngels = "Use player colored Angels";
public static final String useColoredBorders = "Use colored borders on Battle Chits";
public static final String doNotInvertDefender = "Do not invert defender's Battle Chits";
public static final String showHitThreshold = "Show needed roll for hit";
public static final String showDiceAjustmentsTerrain = "Show added/lost dice due to terrain";
public static final String showDiceAjustmentsRange = "Show lost dice due to rangestrike";
public static final String showAllRecruitChits = "Show all recruit Chits";
public static final String showRecruitChitsSubmenu = "Show recruit preview chits...";
public static final String showRecruitChitsNone = "None";
public static final String showRecruitChitsStrongest = "Strongest";
public static final String showRecruitChitsRecruitHint = "Recruit Hint";
public static final String showRecruitChitsAll = "All";
public static final int showRecruitChitsNumNone = 0;
public static final int showRecruitChitsNumStrongest = 1;
public static final int showRecruitChitsNumRecruitHint = 2;
public static final int showRecruitChitsNumAll = 3;
public static final String antialias = "Antialias";
public static final String scale = "Scale";
// Window locations and sizes (client only)
public static final String locX = "Location X";
public static final String locY = "Location Y";
public static final String sizeX = "Size X";
public static final String sizeY = "Size Y";
// AI options (client only)
public static final String autoPickColor = "Auto pick color";
public static final String autoPickMarker = "Auto pick markers";
public static final String autoSplit = "Auto split";
public static final String autoMasterMove = "Auto masterboard move";
public static final String autoPickEntrySide = "Auto pick entry sides";
public static final String autoPickLord = "Auto pick teleporting lord";
public static final String autoPickEngagements = "Auto pick engagements";
public static final String autoFlee = "Auto flee";
public static final String autoConcede = "Auto concede";
public static final String autoNegotiate = "Auto negotiate";
public static final String autoForcedStrike = "Auto forced strike";
public static final String autoCarrySingle = "Auto carry single";
public static final String autoRangeSingle = "Auto rangestrike single";
public static final String autoSummonAngels = "Auto summon angels";
public static final String autoAcquireAngels = "Auto acquire angels";
public static final String autoRecruit = "Auto recruit";
public static final String autoPickRecruiter = "Auto pick recruiters";
public static final String autoReinforce = "Auto reinforce";
public static final String autoPlay = "Auto play";
// Confirmations (client only)
public static final String confirmNoRecruit = "Confirm when not all possible recruits taken";
public static final String confirmNoMove = "Confirm when not all possible moves made";
public static final String confirmNoSplit = "Confirm when not all full legions split";
public static final String confirmConcedeWithTitan = "Confirm when you concede with your Titan";
public static final String legionMoveConfirmationSubMenu = "Legion Movement Confirmation";
public static final String legionMoveConfirmationNoMove = "Any legion has not moved";
public static final String legionMoveConfirmationNoUnvisitedMove = "Unvisited legion has not moved";
public static final String legionMoveConfirmationNoConfirm = "No movement confirmation";
public static final int legionMoveConfirmationNumNoConfirm = 0;
public static final int legionMoveConfirmationNumMove = 1;
public static final int legionMoveConfirmationNumUnvisitedMove = 2;
public static final String nextSplitSubMenu = "Split Phase Next Key Mouse Click";
public static final String nextMove = "Move Phase Next Key Left Click";
public static final String nextMuster = "Muster Phase Next Key Left Click";
public static final String nextSplitAllSplitable = "Split Phase Visit All Splittable Legions";
public static final String nextSplitLeftClick = "Split Phase Next Key Left Click (show markers)";
public static final String nextSplitRightClick = "Split Phase Next Key Right Click (show creatures)";
public static final String nextSplitNoClick = "Split Phase Next Key No Mouse Click";
public static final int nextSplitNumNoClick = 0;
public static final int nextSplitNumLeftClick = 1;
public static final int nextSplitNumRightClick = 2;
// AI timing options (client only)
public static final String aiTimeLimit = "AI time limit";
public static final String aiDelay = "AI delay";
// General per-player options (client only)
public static final String favoriteColors = "Favorite colors";
public static final String favoriteLookFeel = "Favorite Look And Feel";
public static final String serverName = "Server name";
public static final String activePreferencesTab = "Active preferences tab";
public static final String editModeActive = "Edit Mode";
public static final String legionListByMarkerId = "Sort legion list by marker Id";
private final Properties props = new Properties();
private final String owner; // playerName, or Constants.optionsServerName
private final String dataPath; // WebServer sets to create a server.cfg file
// AIs and the "startOption" do not read nor write to an actual file
private boolean noFile;
// Unit and functional tests read a file, but should not write anything back
private final boolean readOnly;
private final Map<String, List<Listener>> listeners = new HashMap<String, List<Listener>>();
public Options(String owner, String customPath, boolean noFile,
boolean readOnly)
{
this.owner = owner;
this.dataPath = customPath;
this.noFile = noFile;
this.readOnly = readOnly;
}
public Options(String owner, String customPath, boolean noFile)
{
this.owner = owner;
this.dataPath = customPath;
this.noFile = noFile;
this.readOnly = false;
}
public Options(String owner)
{
this(owner, Constants.DEFAULT_COLOSSUS_HOME, false);
}
public Options(String owner, boolean noFile)
{
this(owner, Constants.DEFAULT_COLOSSUS_HOME, noFile);
}
public String getOptionsFilename()
{
return dataPath + "/" + Constants.OPTIONS_BASE + owner
+ Constants.OPTIONS_EXTENSION;
}
synchronized public void loadOptions()
{
// As long as we don't prevent e.g. Human players to
// choose "<byColor>" have to block it here - otherwise
// it gives File Exceptions due to the "<" character.
if (owner.startsWith(Constants.byColor)
|| owner.startsWith(Constants.byType))
{
this.noFile = true;
}
// Don't load if noFile is set - typically AI players
if (this.noFile)
{
return;
}
String optionsFile = getOptionsFilename();
LOGGER.info("Loading options from " + optionsFile);
FileInputStream in = null;
try
{
in = new FileInputStream(optionsFile);
props.load(in);
// The pure "Player\ Type\ " (without number) is not needed,
// it causes on Client side trouble if there is a listener
props.remove(Options.playerType);
triggerAllOptions();
}
catch (IOException e)
{
// this can happen for the netclient-config and others, so
// don't warn
LOGGER.info("Couldn't read options from " + optionsFile);
return;
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
LOGGER.log(Level.WARNING,
"Could not close options file properly", e);
}
}
}
}
synchronized public void saveOptions()
{
// Don't save if noFile is set - typically AI players
if (this.noFile)
{
return;
}
// Don't save if readOnly mode was set - typically test cases
if (this.readOnly)
{
return;
}
String optionsFile = getOptionsFilename();
File optionsDir = new File(dataPath);
if (!optionsDir.exists() || !optionsDir.isDirectory())
{
LOGGER.log(Level.INFO, "Trying to make directory " + dataPath);
if (!optionsDir.mkdirs())
{
LOGGER.log(Level.SEVERE, "Could not create options directory "
+ optionsDir.toString());
return;
}
}
FileOutputStream out = null;
try
{
out = new FileOutputStream(optionsFile);
props.store(out, Constants.CONFIG_VERSION);
}
catch (IOException e)
{
LOGGER.log(Level.SEVERE, "Couldn't write options to "
+ optionsFile, e);
}
try
{
if (out != null)
{
out.close();
}
}
catch (IOException e)
{
LOGGER.log(Level.SEVERE,
"Couldn't close outputstream after options written to "
+ optionsFile, e);
}
}
synchronized public void setOption(String optname, String value)
{
String oldValue = getStringOption(optname);
if (!value.equals(oldValue))
{
props.setProperty(optname, value);
triggerStringOption(optname, oldValue, value);
}
}
synchronized public void setOption(String optname, boolean value)
{
boolean undefined = isOptionUndefined(optname);
boolean oldValue = getOption(optname);
if (undefined || oldValue != value)
{
setOption(optname, String.valueOf(value));
triggerBooleanOption(optname, oldValue, value);
}
}
synchronized public void setOption(String optname, int value)
{
int oldValue = getIntOption(optname);
if (oldValue != value)
{
setOption(optname, String.valueOf(value));
triggerIntOption(optname, oldValue, value);
}
}
synchronized public String getStringOption(String optname)
{
String value = props.getProperty(optname);
return value;
}
synchronized public String getStringOption(String optname,
String defaultValue)
{
String value = props.getProperty(optname, defaultValue);
return value;
}
synchronized public boolean getOption(String optname)
{
// the "if start with "Auto "... removed, that part is
// now handled by AutoPlay class.
String value = getStringOption(optname);
return (value != null && value.equals("true"));
}
synchronized public boolean getOption(String optname, boolean defaultValue)
{
String value = getStringOption(optname, (defaultValue ? "true"
: "false"));
return (value != null && value.equals("true"));
}
/** Return -1 if the option's value has not been set. */
synchronized public int getIntOption(String optname)
{
String buf = getStringOption(optname);
int value = -1;
try
{
value = Integer.parseInt(buf);
}
catch (NumberFormatException ex)
{
value = -1;
}
return value;
}
public boolean isOptionUndefined(String optname)
{
return (getStringOption(optname) == null);
}
synchronized public void removeOption(String optname)
{
props.remove(optname);
}
// we know we didn't mistreat Properties by adding non-Strings
@SuppressWarnings("unchecked")
synchronized public Enumeration<String> propertyNames()
{
return (Enumeration<String>)props.propertyNames();
}
/** Remove all playerName and playerType entries. */
synchronized public void clearPlayerInfo()
{
Enumeration<String> en = propertyNames();
while (en.hasMoreElements())
{
String name = en.nextElement();
if (name.startsWith(playerName) || name.startsWith(playerType))
{
props.remove(name);
}
}
}
/** Wipe everything. */
synchronized public void clear()
{
props.clear();
}
synchronized public boolean isEmpty()
{
return props.isEmpty();
}
@Override
public String toString()
{
return props.toString();
}
// client compares then only numeric modes (easier and faster in runtime)
// TODO this one and the next one could be better solved with enums
synchronized public int getNumberForViewMode(String viewMode)
{
int val = Options.viewableAllNum;
if (viewMode == null)
{
return Options.viewableAllNum;
}
if (viewMode.equals(Options.viewableAll))
{
val = Options.viewableAllNum;
}
if (viewMode.equals(Options.viewableEver))
{
val = Options.viewableEverNum;
}
if (viewMode.equals(Options.viewableLast))
{
val = Options.viewableLastNum;
}
if (viewMode.equals(Options.viewableOwn))
{
val = Options.viewableOwnNum;
}
return val;
}
/*
// Right now (09/2007) not used anywhere; but perhaps web server should
// store and transmit only the numerical modes, then this would be needed.
// so I keep it here for now.
public static String xgetStringForViewMode(int val)
{
String text = Options.viewableAll;
switch(val)
{
case Options.viewableAllNum: text = Options.viewableAll; break;
case Options.viewableEverNum: text = Options.viewableEver; break;
case Options.viewableLastNum: text = Options.viewableLast; break;
case Options.viewableOwnNum: text = Options.viewableOwn; break;
default: text = Options.viewableAll; break;
}
return text;
}
*/
// client compares then only numeric modes (easier and faster in runtime)
// ((can use switch case statement))
synchronized public int getNumberForRecruitChitSelection(String s)
{
if (s == null || s.equals(""))
{
return Options.showRecruitChitsNumNone;
}
int val = Options.showRecruitChitsNumAll;
if (s.equals(Options.showRecruitChitsNone))
{
val = Options.showRecruitChitsNumNone;
}
if (s.equals(Options.showRecruitChitsStrongest))
{
val = Options.showRecruitChitsNumStrongest;
}
if (s.equals(Options.showRecruitChitsRecruitHint))
{
val = Options.showRecruitChitsNumRecruitHint;
}
if (s.equals(Options.showRecruitChitsAll))
{
val = Options.showRecruitChitsNumAll;
}
return val;
}
synchronized public int getNumberForLegionMoveConfirmation(String s)
{
if (s == null || s.equals(""))
{
return Options.legionMoveConfirmationNumNoConfirm;
}
int val = Options.legionMoveConfirmationNumNoConfirm;
if (s.equals(Options.legionMoveConfirmationNoMove))
{
val = Options.legionMoveConfirmationNumMove;
}
if (s.equals(Options.legionMoveConfirmationNoUnvisitedMove))
{
val = Options.legionMoveConfirmationNumUnvisitedMove;
}
return val;
}
synchronized public int getNumberForNextSplit(String s)
{
if (s == null || s.equals(""))
{
return Options.nextSplitNumNoClick;
}
int val = Options.nextSplitNumLeftClick;
if (s.equals(Options.nextSplitRightClick))
{
val = Options.nextSplitNumRightClick;
}
if (s.equals(Options.nextSplitNoClick))
{
val = Options.nextSplitNumNoClick;
}
return val;
}
private static boolean functionalTestOngoing = false;
public static void setFunctionalTest(boolean val)
{
functionalTestOngoing = val;
}
public static boolean isFunctionalTest()
{
return functionalTestOngoing;
}
private static boolean startupTestOngoing = false;
public static void setStartupTest(boolean val)
{
setFunctionalTest(val);
startupTestOngoing = val;
}
public static boolean isStartupTest()
{
return startupTestOngoing;
}
private static String propNameStresstestRounds = "net.sf.colossus.stressTestRounds";
public static boolean isStresstest()
{
return System.getProperty(propNameStresstestRounds) != null;
}
synchronized public static int getHowManyStresstestRoundsProperty()
{
String propHowMany = System.getProperty(propNameStresstestRounds);
int howMany = 0;
if (propHowMany != null)
{
try
{
int i = Integer.parseInt(propHowMany);
howMany = i;
}
catch (NumberFormatException ex)
{
howMany = 1;
LOGGER.log(Level.WARNING, "NOTE: Value '" + propHowMany
+ "' from property " + propNameStresstestRounds
+ " is not a valid number - using default value 1!");
}
}
return howMany;
}
synchronized public void addListener(String optname, Listener listener)
{
List<Listener> optionListeners = getListenersForOption(optname);
optionListeners.add(listener);
}
private List<Listener> getListenersForOption(String optname)
{
List<Listener> optionListeners = listeners.get(optname);
if (optionListeners == null)
{
optionListeners = new ArrayList<Listener>();
listeners.put(optname, optionListeners);
}
return optionListeners;
}
synchronized public void removeListener(Listener listener)
{
for (List<Listener> optionListeners : listeners.values())
{
optionListeners.remove(listener);
}
}
private void triggerBooleanOption(String optname, boolean oldValue,
boolean newValue)
{
List<Listener> optionListeners = getListenersForOption(optname);
for (Listener listener : optionListeners)
{
listener.booleanOptionChanged(optname, oldValue, newValue);
}
}
private void triggerIntOption(String optname, int oldValue, int newValue)
{
List<Listener> optionListeners = getListenersForOption(optname);
for (Listener listener : optionListeners)
{
listener.intOptionChanged(optname, oldValue, newValue);
}
}
private void triggerStringOption(String optname, String oldValue,
String newValue)
{
List<Listener> optionListeners = getListenersForOption(optname);
for (Listener listener : optionListeners)
{
listener.stringOptionChanged(optname, oldValue, newValue);
}
}
private void triggerAllOptions()
{
for (Map.Entry<Object, Object> option : props.entrySet())
{
String optname = (String)option.getKey();
String stringVal = (String)option.getValue();
// try triggering things as integer or boolean first
// (which implies a string trigger)
// make sure the oldVal is guaranteed to be different
try
{
int intVal = Integer.parseInt(stringVal);
// don't just negate the value, it might be Integer.MIN_VALUE
// used by someone as a marker
if (intVal == 0)
{
triggerIntOption(optname, 1, 0);
}
else
{
triggerIntOption(optname, 0, intVal);
}
}
catch (NumberFormatException e)
{
// so it is not a number, let's try boolean
if (stringVal.equalsIgnoreCase("true"))
{
triggerBooleanOption(optname, false, true);
}
else if (stringVal.equalsIgnoreCase("false"))
{
triggerBooleanOption(optname, true, false);
}
else
{
// neither int nor boolean
triggerStringOption(optname, null, stringVal);
}
}
}
}
}