package org.gbcpainter.env;
import net.jcip.annotations.GuardedBy;
import org.gbcpainter.loaders.textures.ResolutionTextureLoader;
import org.ini4j.IniPreferences;
import org.ini4j.Wini;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.prefs.Preferences;
/**
* Keeps the game settings like controls, level and texture folders, etc.
* <p/>
* In order to retrieve those settings {@link #getInstance()} must be used to get the singleton
* <p/>
* The default config file is "config.ini" and it's loaded/saved from/in the current working path.
*
* @author Lorenzo Pellegrini
*/
public class GameSettings {
@NotNull
@NonNls
private static final String DEFAULT_LEVELS_PATH = "levels";
@NotNull
@NonNls
private static final String DEFAULT_TEXTURES_PATH = "textures";
@NotNull
@NonNls
private static final String DEFAULT_CONFIG_FILE = "config.ini";
@NotNull
@NonNls
private static final String GRAPHICS_SETTINGS_SECTION = "Graphics";
@NotNull
@NonNls
private static final String CONTROLS_SETTINGS_SECTION = "Controls";
@NotNull
@NonNls
private static final String RESOURCES_SETTINGS_SECTION = "Resources";
private static final int DEFAULT_UP_KEY = KeyEvent.VK_W;
private static final int DEFAULT_DOWN_KEY = KeyEvent.VK_S;
private static final int DEFAULT_LEFT_KEY = KeyEvent.VK_A;
private static final int DEFAULT_RIGHT_KEY = KeyEvent.VK_D;
private static final int DEFAULT_PAUSE_KEY = KeyEvent.VK_ESCAPE;
private static final int DEFAULT_MOVE_MILLIS_TIMEOUT = 150;
private static final int DEFAULT_FPS = 45;
private static final int DEFAULT_ANTIALIAS = 0;
private static final int DEFAULT_OPENGL = 0;
@NotNull
private static final List<STRING_SETTINGS_TYPE> immutableStringSettings = Collections
.emptyList();
@NotNull
private static final List<INTEGER_SETTINGS_TYPE> immutableIntegerSettings = Arrays
.asList( INTEGER_SETTINGS_TYPE.ANTIALIAS, INTEGER_SETTINGS_TYPE.OPENGL );
@Nullable
@GuardedBy ( "org.gbcpainter.env.GameSettings.class" )
private static GameSettings ourInstance;
@NotNull
private final Map<STRING_SETTINGS_TYPE, String> stringSettingsMap = new EnumMap<>(
STRING_SETTINGS_TYPE.class );
@NotNull
private final Map<INTEGER_SETTINGS_TYPE, Integer> integerSettingsMap = new EnumMap<>(
INTEGER_SETTINGS_TYPE.class );
@NotNull
private final Map<STRING_SETTINGS_TYPE, String> nextRunStringSettingsMap = new HashMap<>(
immutableStringSettings.size() );
@NotNull
private final Map<INTEGER_SETTINGS_TYPE, Integer> nextRunIntegerSettingsMap = new HashMap<>(
immutableIntegerSettings.size() );
private GameSettings() {
Preferences preferences;
try {
preferences = this.loadPreferences();
} catch ( IOException e ) {
preferences = null;
}
if ( preferences == null ) {
this.stringSettingsMap.put( STRING_SETTINGS_TYPE.LEVELS_PATH, DEFAULT_LEVELS_PATH );
this.stringSettingsMap.put( STRING_SETTINGS_TYPE.TEXTURES_PATH, DEFAULT_TEXTURES_PATH );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_UP, DEFAULT_UP_KEY );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_DOWN, DEFAULT_DOWN_KEY );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_LEFT, DEFAULT_LEFT_KEY );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_RIGHT, DEFAULT_RIGHT_KEY );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_PAUSE, DEFAULT_PAUSE_KEY );
this.integerSettingsMap
.put( INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT, DEFAULT_MOVE_MILLIS_TIMEOUT );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.REDRAW_FPS, DEFAULT_FPS );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.ANTIALIAS, DEFAULT_ANTIALIAS );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.OPENGL, DEFAULT_OPENGL );
} else {
{
Preferences resourcesSection = preferences.node( RESOURCES_SETTINGS_SECTION );
this.stringSettingsMap.put( STRING_SETTINGS_TYPE.LEVELS_PATH,
resourcesSection
.get( STRING_SETTINGS_TYPE.LEVELS_PATH.toString(),
DEFAULT_LEVELS_PATH ) );
this.stringSettingsMap.put( STRING_SETTINGS_TYPE.TEXTURES_PATH,
resourcesSection
.get( STRING_SETTINGS_TYPE.TEXTURES_PATH.toString(),
DEFAULT_TEXTURES_PATH ) );
}
{
Preferences controlsSection = preferences.node( CONTROLS_SETTINGS_SECTION );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_UP,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.KEY_UP,
DEFAULT_UP_KEY ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_DOWN,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.KEY_DOWN,
DEFAULT_DOWN_KEY ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_LEFT,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.KEY_LEFT,
DEFAULT_LEFT_KEY ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_RIGHT,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.KEY_RIGHT,
DEFAULT_RIGHT_KEY ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT,
DEFAULT_MOVE_MILLIS_TIMEOUT ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.KEY_PAUSE,
getInteger( controlsSection,
INTEGER_SETTINGS_TYPE.KEY_PAUSE,
DEFAULT_PAUSE_KEY ) );
}
{
Preferences graphicsSection = preferences.node( GRAPHICS_SETTINGS_SECTION );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.ANTIALIAS,
getInteger( graphicsSection,
INTEGER_SETTINGS_TYPE.ANTIALIAS,
DEFAULT_ANTIALIAS ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.OPENGL,
getInteger( graphicsSection,
INTEGER_SETTINGS_TYPE.OPENGL,
DEFAULT_OPENGL ) );
this.integerSettingsMap.put( INTEGER_SETTINGS_TYPE.REDRAW_FPS,
getInteger( graphicsSection,
INTEGER_SETTINGS_TYPE.REDRAW_FPS,
DEFAULT_FPS ) );
}
}
}
/**
* Loads the singleton instance from the default config file.
*
* @throws java.lang.IllegalStateException If settings were already loaded.
* @throws java.lang.Exception If an error occours while loading the configuration.
*/
public static void loadInstance() throws Exception {
synchronized (org.gbcpainter.env.GameSettings.class) {
if ( ourInstance == null ) {
ourInstance = new GameSettings();
} else {
throw new IllegalStateException( "Settings already loaded" );
}
}
}
/**
* Retrieves the singleton.
*
* @return The settings singleton.
*/
@NotNull
public static GameSettings getInstance() {
final GameSettings localInstance;
synchronized (org.gbcpainter.env.GameSettings.class) {
localInstance = ourInstance;
}
if ( localInstance == null ) {
throw new IllegalStateException( "Singleton not initialized" );
}
return localInstance;
}
@NotNull
private static Integer getInteger( @NotNull Preferences prefs,
@NotNull INTEGER_SETTINGS_TYPE key,
@NotNull Integer defaultValue ) {
String strValue = prefs.get( key.toString(), "" + defaultValue );
Integer result;
try {
result = Integer.parseInt( strValue );
} catch ( NumberFormatException e ) {
result = defaultValue;
}
return result;
}
/**
* Utility method used to retrieve a resource directory location in the actual file system.
* <p/>
* This method must be used to retrieve the location of a resource folder. First the location of
* the jar/{@link org.gbcpainter.env.GameSettings} class file is checked.
*
* @param directory The directory to be resolved
*
* @return The resolved directory. It is never null. The described file may not exist
*/
@NotNull
public static File getRelativeDirectory( @NotNull @NonNls String directory ) {
URL root = ResolutionTextureLoader.class.getProtectionDomain().getCodeSource()
.getLocation();
final File candidate;
File workingDirectory = new File( directory );
if ( workingDirectory.exists() && workingDirectory.isDirectory() ) {
return workingDirectory;
}
File location = new File( "." );
try {
location = new File( root.toURI() );
} catch ( URISyntaxException ignored ) {
}
if ( location.isFile() ) {
candidate = new File( location.getParent(), directory );
} else {
candidate = new File( location, directory );
}
if ( ! candidate.exists() ) {
/* Fail... */
return workingDirectory;
} else {
return candidate;
}
}
/**
* Updates a setting.
*
* @param type The setting to modify. It cannot be an immutable setting.
* @param value The new value of the setting
*
* @throws java.lang.IllegalArgumentException If the specified setting is immutable or the value
* is not compatible
*/
public synchronized void updateData( @NotNull STRING_SETTINGS_TYPE type,
@NotNull @NonNls String value ) {
if ( immutableStringSettings.contains( type ) ) {
throw new IllegalArgumentException( "Immutable setting " + type );
}
this.getStringSettingsMap().put( type, value );
}
/**
* Updates a setting.
*
* @param type The setting to modify. It cannot be an immutable setting
* @param value The new value of the setting
*
* @throws java.lang.IllegalArgumentException If the specified setting is immutable or the value
* is not compatible
*/
public synchronized void updateData( @NotNull INTEGER_SETTINGS_TYPE type, int value ) {
if ( immutableIntegerSettings.contains( type ) ) {
throw new IllegalArgumentException( "Immutable setting " + type );
}
this.getIntegerSettingsMap().put( type, value );
}
/**
* Updates a setting for future application instances.
* <p/>
* The updated value can be queried via {@link #getNextRunValue(org.gbcpainter.env.GameSettings.STRING_SETTINGS_TYPE)}
* but doesn't affect the current value of the setting. The new value will be written to the
* config file via {@link #storeSettings()} and will be used in future application instances
*
* @param type The setting to modify. It can be an immutable setting.
* @param value The new value of the setting
*
* @throws java.lang.IllegalArgumentException If the specified value is not compatible
*/
public synchronized void updateNextRunData( @NotNull STRING_SETTINGS_TYPE type,
@NotNull @NonNls String value ) {
this.nextRunStringSettingsMap.put( type, value );
}
/**
* Updates a setting for future application instances.
* <p/>
* The updated value can be queried via {@link #getNextRunValue(org.gbcpainter.env.GameSettings.INTEGER_SETTINGS_TYPE)}
* but doesn't affect the current value of the setting. The new value will be written to the
* config file via {@link #storeSettings()} and will be used in future application instances
*
* @param type The setting to modify. It can be an immutable setting.
* @param value The new value of the setting
*
* @throws java.lang.IllegalArgumentException If the specified value is not compatible
*/
public synchronized void updateNextRunData( @NotNull INTEGER_SETTINGS_TYPE type, int value ) {
this.nextRunIntegerSettingsMap.put( type, value );
}
/**
* Returns the current value of a setting.
* <p/>
* If the setting is immutable {@link #getInstanceValue(org.gbcpainter.env.GameSettings.STRING_SETTINGS_TYPE)}
* should be used to obtain a better performance.
*
* @param type The setting
*
* @return The value of the setting
*/
@NotNull
@NonNls
public synchronized String getValue( @NotNull STRING_SETTINGS_TYPE type ) {
final String value = this.getStringSettingsMap().get( type );
if ( value == null ) {
throw new IllegalArgumentException( "Unknown enum constant " + type );
}
return value;
}
/**
* Returns the current value of a setting.
* <p/>
* If the setting is immutable {@link #getInstanceValue(org.gbcpainter.env.GameSettings.INTEGER_SETTINGS_TYPE)}
* should be used to obtain a better performance.
*
* @param type The setting
*
* @return The value of the setting
*/
public synchronized int getValue( @NotNull INTEGER_SETTINGS_TYPE type ) {
final Integer value = this.getIntegerSettingsMap().get( type );
if ( value == null ) {
throw new IllegalArgumentException( "Unknown enum constant " + type );
}
return value;
}
/**
* Returns the current value of an immutable setting
* <p/>
* If a setting is immutable this method should be used instead of {@link
* #getValue(org.gbcpainter.env.GameSettings.STRING_SETTINGS_TYPE)} to obtain a better
* performance.
*
* @param type The immutable setting
*
* @return The value of the setting
*
* @throws java.lang.IllegalArgumentException If the setting is not immutable.
*/
@NotNull
public String getInstanceValue( @NotNull STRING_SETTINGS_TYPE type ) {
if ( ! immutableStringSettings.contains( type ) ) {
throw new IllegalArgumentException( "Non immutable setting " + type );
}
final String value = this.getStringSettingsMap().get( type );
if ( value == null ) {
throw new IllegalArgumentException( "Unknown enum constant " + type );
}
return value;
}
/**
* Returns the current value of an immutable setting
* <p/>
* If a setting is immutable this method should be used instead of {@link
* #getValue(org.gbcpainter.env.GameSettings.INTEGER_SETTINGS_TYPE)} to obtain a better
* performance.
*
* @param type The immutable setting
*
* @return The value of the setting
*
* @throws java.lang.IllegalArgumentException If the setting is not immutable.
*/
public int getInstanceValue( @NotNull INTEGER_SETTINGS_TYPE type ) {
if ( ! immutableIntegerSettings.contains( type ) ) {
throw new IllegalArgumentException( "Non immutable setting " + type );
}
final Integer value = this.getIntegerSettingsMap().get( type );
if ( value == null ) {
throw new IllegalArgumentException( "Unknown enum constant " + type );
}
return value;
}
/**
* Returns the current value of an immutable setting
* <p/>
* If a setting is immutable this method should be used instead of {@link
* #getValue(org.gbcpainter.env.GameSettings.STRING_SETTINGS_TYPE)} to obtain a better
* performance.
*
* @param type The immutable setting
*
* @return The value of the setting
*
* @throws java.lang.IllegalArgumentException If the setting is not immutable.
*/
@NotNull
public synchronized String getNextRunValue( @NotNull STRING_SETTINGS_TYPE type ) {
String value = this.nextRunStringSettingsMap.get( type );
if ( value == null ) {
value = this.getValue( type );
}
return value;
}
/**
* Returns the setting's value that will be saved in the config file.
*
* @param type The setting
*
* @return The value that will be saved in the config file
*
* @see #storeSettings()
*/
public synchronized int getNextRunValue( @NotNull INTEGER_SETTINGS_TYPE type ) {
Integer value = this.nextRunIntegerSettingsMap.get( type );
if ( value == null ) {
value = this.getValue( type );
}
return value;
}
/**
* Stores all the settings in the config file.
*
* @throws IOException If an exception occours.
*/
public synchronized void storeSettings() throws IOException {
Wini settingsFile = new Wini();
settingsFile.add( GRAPHICS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.ANTIALIAS.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.ANTIALIAS ) );
settingsFile.add( GRAPHICS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.OPENGL.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.OPENGL ) );
settingsFile.add( GRAPHICS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.REDRAW_FPS.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.REDRAW_FPS ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.KEY_LEFT.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.KEY_LEFT ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.KEY_RIGHT.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.KEY_RIGHT ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.KEY_UP.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.KEY_UP ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.KEY_DOWN.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.KEY_DOWN ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.KEY_PAUSE.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.KEY_PAUSE ) );
settingsFile.add( CONTROLS_SETTINGS_SECTION,
INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT.toString(),
"" + this.getNextRunValue( INTEGER_SETTINGS_TYPE.MOVE_MILLIS_TIMEOUT ) );
settingsFile.add( RESOURCES_SETTINGS_SECTION,
STRING_SETTINGS_TYPE.LEVELS_PATH.toString(),
this. getNextRunValue( STRING_SETTINGS_TYPE.LEVELS_PATH ) );
settingsFile.add( RESOURCES_SETTINGS_SECTION,
STRING_SETTINGS_TYPE.TEXTURES_PATH.toString(),
this.getNextRunValue( STRING_SETTINGS_TYPE.TEXTURES_PATH ) );
File preferencesDirectory = getRelativeDirectory( "." );
settingsFile.store( new File( preferencesDirectory, DEFAULT_CONFIG_FILE ) );
}
@NotNull
private Map<STRING_SETTINGS_TYPE, String> getStringSettingsMap() {
return this.stringSettingsMap;
}
@NotNull
private Map<INTEGER_SETTINGS_TYPE, Integer> getIntegerSettingsMap() {
return this.integerSettingsMap;
}
@NotNull
private Preferences loadPreferences() throws IOException {
File preferencesDirectory = getRelativeDirectory( "." );
Wini ini = new Wini( new File( preferencesDirectory, DEFAULT_CONFIG_FILE ) );
return new IniPreferences( ini );
}
/**
* Returns a string rappresentation of the stored settings
* <p/>
* It shouldn't be used to display in a user view.
*
* @return A string representing the actual settings mapping
*/
@Override
@NonNls
public synchronized String toString() {
return "GameSettings{" +
"stringSettingsMap=" +this. getStringSettingsMap() +
"integerSettingsMap=" + this.getIntegerSettingsMap() +
'}';
}
/**
* An enum defining all the available settings that have a {@link java.lang.String} value
*/
public static enum STRING_SETTINGS_TYPE {
LEVELS_PATH, TEXTURES_PATH;
@NonNls
public String toString() {
switch ( this ) {
case LEVELS_PATH:
return "LevelsPath";
case TEXTURES_PATH:
return "TexturesPath";
}
throw new AssertionError( "Unknown enum constant " + this );
}
}
/**
* An enum defining all the available settings that have an {@link java.lang.Integer} value
*/
public static enum INTEGER_SETTINGS_TYPE {
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_PAUSE, MOVE_MILLIS_TIMEOUT, REDRAW_FPS,
ANTIALIAS, OPENGL;
@NonNls
public String toString() {
switch ( this ) {
case KEY_UP:
return "KeyUp";
case KEY_DOWN:
return "KeyDown";
case KEY_LEFT:
return "KeyLeft";
case KEY_RIGHT:
return "KeyRight";
case KEY_PAUSE:
return "KeyPause";
case MOVE_MILLIS_TIMEOUT:
return "MoveTimeout";
case REDRAW_FPS:
return "Fps";
case ANTIALIAS:
return "Antialias";
case OPENGL:
return "Opengl";
}
throw new AssertionError( "Unknown enum constant " + this );
}
}
}