diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index a804cb31..b569f776 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -593,8 +593,9 @@ public enum GameImage { return; // try to load multiple images + File skinDir = Options.getSkin().getDirectory(); if (filenameFormat != null) { - if (((defaultImages = loadImageArray(Options.getSkinDir())) != null) || + if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) || ((defaultImages = loadImageArray(null)) != null)) { process(); return; @@ -602,7 +603,7 @@ public enum GameImage { } // try to load a single image - if (((defaultImage = loadImageSingle(Options.getSkinDir())) != null) || + if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) || ((defaultImage = loadImageSingle(null)) != null)) { process(); return; diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 3524863b..baa98a50 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -45,7 +45,6 @@ import org.newdawn.slick.SlickException; import org.newdawn.slick.state.StateBasedGame; import org.newdawn.slick.state.transition.FadeInTransition; import org.newdawn.slick.state.transition.FadeOutTransition; -import org.newdawn.slick.util.ClasspathLocation; import org.newdawn.slick.util.DefaultLogSystem; import org.newdawn.slick.util.FileSystemLocation; import org.newdawn.slick.util.Log; @@ -128,10 +127,6 @@ public class Opsu extends StateBasedGame { System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath()); // set the resource paths - ResourceLoader.removeAllResourceLocations(); - ResourceLoader.addResourceLocation(new FileSystemLocation(Options.getSkinDir())); - ResourceLoader.addResourceLocation(new ClasspathLocation()); - ResourceLoader.addResourceLocation(new FileSystemLocation(new File("."))); ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/"))); // initialize databases diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index ef5b620e..de0f0291 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -20,6 +20,8 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.beatmap.Beatmap; +import itdelatrisu.opsu.skins.Skin; +import itdelatrisu.opsu.skins.SkinLoader; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -38,7 +40,10 @@ import org.lwjgl.input.Keyboard; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; +import org.newdawn.slick.util.ClasspathLocation; +import org.newdawn.slick.util.FileSystemLocation; import org.newdawn.slick.util.Log; +import org.newdawn.slick.util.ResourceLoader; /** * Handles all user options. @@ -56,13 +61,20 @@ public class Options { /** File for storing user options. */ private static final File OPTIONS_FILE = new File(CONFIG_DIR, ".opsu.cfg"); - /** Beatmap directories (where to search for files). */ + /** Beatmap directories (where to search for files). */ private static final String[] BEATMAP_DIRS = { "C:/Program Files (x86)/osu!/Songs/", "C:/Program Files/osu!/Songs/", new File(DATA_DIR, "Songs/").getPath() }; + /** Skin directories (where to search for skins). */ + private static final String[] SKIN_ROOT_DIRS = { + "C:/Program Files (x86)/osu!/Skins/", + "C:/Program Files/osu!/Skins/", + new File(DATA_DIR, "Skins/").getPath() + }; + /** Cached beatmap database name. */ public static final File BEATMAP_DB = new File(DATA_DIR, ".opsu.db"); @@ -96,8 +108,8 @@ public class Options { /** The replay directory (created when needed). */ private static File replayDir; - /** The current skin directory (for user skins). */ - private static File skinDir; + /** The root skin directory. */ + private static File skinRootDir; /** Port binding. */ private static int port = 49250; @@ -153,6 +165,16 @@ public class Options { } }, // FULLSCREEN ("Fullscreen Mode", "Restart to apply changes.", false), + SKIN ("Skin", "Restart (Ctrl+Shift+F5) to apply skin changes.") { + @Override + public String getValueString() { return skinName; } + + @Override + public void click(GameContainer container) { + skinDirIndex = (skinDirIndex + 1) % skinDirs.length; + skinName = skinDirs[skinDirIndex]; + } + }, TARGET_FPS ("Frame Limiter", "Higher values may cause high CPU usage.") { @Override public String getValueString() { @@ -449,6 +471,18 @@ public class Options { /** Current screen resolution. */ private static Resolution resolution = Resolution.RES_1024_768; + /** The available skin directories. */ + private static String[] skinDirs; + + /** The index in the skinDirs array. */ + private static int skinDirIndex = 0; + + /** The name of the skin. */ + private static String skinName = "Default"; + + /** The current skin. */ + private static Skin skin; + /** Frame limiters. */ private static final int[] targetFPS = { 60, 120, 240 }; @@ -857,15 +891,67 @@ public class Options { * If invalid, this will create a "Skins" folder in the root directory. * @return the skin directory */ - public static File getSkinDir() { - if (skinDir != null && skinDir.isDirectory()) - return skinDir; + public static File getSkinRootDir() { + if (skinRootDir != null && skinRootDir.isDirectory()) + return skinRootDir; - skinDir = new File(DATA_DIR, "Skins/"); - skinDir.mkdir(); - return skinDir; + // search for directory + for (int i = 0; i < SKIN_ROOT_DIRS.length; i++) { + skinRootDir = new File(SKIN_ROOT_DIRS[i]); + if (skinRootDir.isDirectory()) + return skinRootDir; + } + skinRootDir.mkdir(); // none found, create new directory + return skinRootDir; } + /** + * Loads the skin given by the current skin directory. + * If the directory is invalid, the default skin will be loaded. + */ + public static void loadSkin() { + File root = getSkinRootDir(); + File skinDir = new File(root, skinName); + if (!skinDir.isDirectory()) { // invalid skin name + skinName = Skin.DEFAULT_SKIN_NAME; + skinDir = null; + } + + // create available skins list + File[] dirs = SkinLoader.getSkinDirectories(root); + skinDirs = new String[dirs.length + 1]; + skinDirs[0] = Skin.DEFAULT_SKIN_NAME; + for (int i = 0; i < dirs.length; i++) + skinDirs[i + 1] = dirs[i].getName(); + + // set skin and modify resource locations + ResourceLoader.removeAllResourceLocations(); + if (skinDir == null) + skin = new Skin(null); + else { + // set skin index + for (int i = 1; i < skinDirs.length; i++) { + if (skinDirs[i].equals(skinName)) { + skinDirIndex = i; + break; + } + } + + // load the skin + skin = SkinLoader.loadSkin(skinDir); + ResourceLoader.addResourceLocation(new FileSystemLocation(skinDir)); + } + ResourceLoader.addResourceLocation(new ClasspathLocation()); + ResourceLoader.addResourceLocation(new FileSystemLocation(new File("."))); + ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/"))); + } + + /** + * Returns the current skin. + * @return the skin, or null if no skin is loaded (see {@link #loadSkin()}) + */ + public static Skin getSkin() { return skin; } + /** * Returns a dummy Beatmap containing the theme song. * @return the theme song beatmap @@ -928,8 +1014,8 @@ public class Options { case "ReplayDirectory": replayDir = new File(value); break; - case "Skin": - skinDir = new File(value); + case "SkinDirectory": + skinRootDir = new File(value); break; case "ThemeSong": themeString = value; @@ -948,6 +1034,9 @@ public class Options { // case "Fullscreen": // GameOption.FULLSCREEN.setValue(Boolean.parseBoolean(value)); // break; + case "Skin": + skinName = value; + break; case "FrameSync": i = Integer.parseInt(value); for (int j = 0; j < targetFPS.length; j++) { @@ -1098,7 +1187,7 @@ public class Options { writer.newLine(); writer.write(String.format("ReplayDirectory = %s", getReplayDir().getAbsolutePath())); writer.newLine(); - writer.write(String.format("Skin = %s", getSkinDir().getAbsolutePath())); + writer.write(String.format("SkinDirectory = %s", getSkinRootDir().getAbsolutePath())); writer.newLine(); writer.write(String.format("ThemeSong = %s", themeString)); writer.newLine(); @@ -1108,6 +1197,8 @@ public class Options { writer.newLine(); // writer.write(String.format("Fullscreen = %b", isFullscreen())); // writer.newLine(); + writer.write(String.format("Skin = %s", skinName)); + writer.newLine(); writer.write(String.format("FrameSync = %d", targetFPS[targetFPSindex])); writer.newLine(); writer.write(String.format("FpsCounter = %b", isFPSCounterEnabled())); diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 71e395dd..80ee74b0 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -179,6 +179,9 @@ public class Utils { ErrorHandler.error("Failed to load fonts.", e, true); } + // load skin + Options.loadSkin(); + // initialize game images for (GameImage img : GameImage.values()) { if (img.isPreload()) diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index c6022879..a16a46fc 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -177,11 +177,14 @@ public class SoundController { */ private static String getSoundFileName(String filename) { String wav = String.format("%s.wav", filename), mp3 = String.format("%s.mp3", filename); - File skinWAV = new File(Options.getSkinDir(), wav), skinMP3 = new File(Options.getSkinDir(), mp3); - if (skinWAV.isFile()) - return skinWAV.getAbsolutePath(); - if (skinMP3.isFile()) - return skinMP3.getAbsolutePath(); + File skinDir = Options.getSkin().getDirectory(); + if (skinDir != null) { + File skinWAV = new File(skinDir, wav), skinMP3 = new File(skinDir, mp3); + if (skinWAV.isFile()) + return skinWAV.getAbsolutePath(); + if (skinMP3.isFile()) + return skinMP3.getAbsolutePath(); + } if (ResourceLoader.resourceExists(wav)) return wav; if (ResourceLoader.resourceExists(mp3)) diff --git a/src/itdelatrisu/opsu/skins/Skin.java b/src/itdelatrisu/opsu/skins/Skin.java new file mode 100644 index 00000000..d1e70e2b --- /dev/null +++ b/src/itdelatrisu/opsu/skins/Skin.java @@ -0,0 +1,374 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! 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 3 of the License, or + * (at your option) any later version. + * + * opsu! 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 opsu!. If not, see . + */ + +package itdelatrisu.opsu.skins; + +import java.io.File; + +import org.newdawn.slick.Color; + +/** + * Skin configuration (skin.ini). + */ +public class Skin { + /** The default skin name. */ + public static final String DEFAULT_SKIN_NAME = "Default"; + + /** Slider styles. */ + public static final byte + STYLE_PEPPYSLIDER = 1, + STYLE_MMSLIDER = 2, + STYLE_TOONSLIDER = 3, + STYLE_OPENGLSLIDER = 4; + + /** The latest skin version. */ + protected static final int LATEST_VERSION = 2; + + /** The default list of combos with combo sounds. */ + private static final int[] DEFAULT_CUSTOM_COMBO_BURST_SOUNDS = { 50, 75, 100, 200, 300 }; + + /** The default combo colors (used when a beatmap does not provide custom colors). */ + private static final Color[] DEFAULT_COMBO = { + new Color(255, 192, 0), + new Color(0, 202, 0), + new Color(18, 124, 255), + new Color(242, 24, 57) + }; + + /** The default menu visualization bar color. */ + private static final Color DEFAULT_MENU_GLOW = new Color(0, 78, 155); + + /** The default slider border color. */ + private static final Color DEFAULT_SLIDER_BORDER = new Color(38, 38, 38); + + /** The default slider ball color. */ + private static final Color DEFAULT_SLIDER_BALL = new Color(2, 170, 255); + + /** The default spinner approach circle color. */ + private static final Color DEFAULT_SPINNER_APPROACH_CIRCLE = new Color(77, 139, 217); + + /** The default color of the active text in the song selection menu. */ + private static final Color DEFAULT_SONG_SELECT_ACTIVE_TEXT = new Color(255, 255, 255); + + /** The default color of the inactive text in the song selection menu. */ + private static final Color DEFAULT_SONG_SELECT_INACTIVE_TEXT = new Color(178, 178, 178); + + /** The default color of the stars that fall from the cursor during breaks. */ + private static final Color DEFAULT_STAR_BREAK_ADDITIVE = new Color(255, 182, 193); + + /** The skin directory. */ + private File dir; + + /** + * [General] + */ + + /** The name of the skin. */ + protected String name = "opsu! Default Skin"; + + /** The skin author. */ + protected String author = "[various authors]"; + + /** The skin version. */ + protected int version = LATEST_VERSION; + + /** When a slider has a reverse, should the ball sprite flip horizontally? */ + protected boolean sliderBallFlip = false; + + /** Should the cursor sprite rotate constantly? */ + protected boolean cursorRotate = true; + + /** Should the cursor expand when clicked? */ + protected boolean cursorExpand = true; + + /** Should the cursor have an origin at the center of the image? (if not, the top-left corner is used) */ + protected boolean cursorCentre = true; + + /** The number of frames in the slider ball animation. */ + protected int sliderBallFrames = 10; + + /** Should the hitcircleoverlay sprite be drawn above the hircircle combo number? */ + protected boolean hitCircleOverlayAboveNumber = true; + + /** Should the sound frequency be modulated depending on the spinner score? */ + protected boolean spinnerFrequencyModulate = false; + + /** Should the normal hitsound always be played? */ + protected boolean layeredHitSounds = true; + + /** Should the spinner fade the playfield? */ + protected boolean spinnerFadePlayfield = true; + + /** Should the last spinner bar blink? */ + protected boolean spinnerNoBlink = false; + + /** Should the slider combo color tint the slider ball? */ + protected boolean allowSliderBallTint = false; + + /** The FPS of animations. */ + protected int animationFramerate = -1; + + /** Should the cursor trail sprite rotate constantly? */ + protected boolean cursorTrailRotate = false; + + /** List of combos with combo sounds. */ + protected int[] customComboBurstSounds = DEFAULT_CUSTOM_COMBO_BURST_SOUNDS; + + /** Should the combo burst sprites appear in random order? */ + protected boolean comboBurstRandom = false; + + /** The slider style to use (see STYLE_* constants). */ + protected byte sliderStyle = 2; + + /** + * [Colours] + */ + + /** Combo colors (max 8). */ + protected Color[] combo = DEFAULT_COMBO; + + /** The menu visualization bar color. */ + protected Color menuGlow = DEFAULT_MENU_GLOW; + + /** The color for the slider border. */ + protected Color sliderBorder = DEFAULT_SLIDER_BORDER; + + /** The slider ball color. */ + protected Color sliderBall = DEFAULT_SLIDER_BALL; + + /** The spinner approach circle color. */ + protected Color spinnerApproachCircle = DEFAULT_SPINNER_APPROACH_CIRCLE; + + /** The color of text in the currently active group in song selection. */ + protected Color songSelectActiveText = DEFAULT_SONG_SELECT_ACTIVE_TEXT; + + /** The color of text in the inactive groups in song selection. */ + protected Color songSelectInactiveText = DEFAULT_SONG_SELECT_INACTIVE_TEXT; + + /** The color of the stars that fall from the cursor (star2 sprite) in breaks. */ + protected Color starBreakAdditive = DEFAULT_STAR_BREAK_ADDITIVE; + + /** + * [Fonts] + */ + + /** The prefix for the hitcircle font sprites. */ + protected String hitCirclePrefix = "default"; + + /** How much should the hitcircle font sprites overlap? */ + protected int hitCircleOverlap = -2; + + /** The prefix for the score font sprites. */ + protected String scorePrefix = "score"; + + /** How much should the score font sprites overlap? */ + protected int scoreOverlap = 0; + + /** The prefix for the combo font sprites. */ + protected String comboPrefix = "score"; + + /** How much should the combo font sprites overlap? */ + protected int comboOverlap = 0; + + /** + * Constructor. + * @param dir the skin directory + */ + public Skin(File dir) { + this.dir = dir; + } + + /** + * Returns the skin directory. + */ + public File getDirectory() { return dir; } + + /** + * Returns the name of the skin. + */ + public String getName() { return name; } + + /** + * Returns the skin author. + */ + public String getAuthor() { return author; } + + /** + * Returns the skin version. + */ + public int getVersion() { return version; } + + /** + * Returns whether the slider ball should be flipped horizontally during a reverse. + */ + public boolean isSliderBallFlipped() { return sliderBallFlip; } + + /** + * Returns whether the cursor should rotate. + */ + public boolean isCursorRotated() { return cursorRotate; } + + /** + * Returns whether the cursor should expand when clicked. + */ + public boolean isCursorExpanded() { return cursorExpand; } + + /** + * Returns whether the cursor should have an origin in the center. + * @return {@code true} if center, {@code false} if top-left corner + */ + public boolean isCursorCentered() { return cursorCentre; } + + /** + * Returns the number of frames in the slider ball animation. + */ + public int getSliderBallFrames() { return sliderBallFrames; } + + /** + * Returns whether the hit circle overlay should be drawn above the combo number. + */ + public boolean isHitCircleOverlayAboveNumber() { return hitCircleOverlayAboveNumber; } + + /** + * Returns whether the sound frequency should be modulated depending on the spinner score. + */ + public boolean isSpinnerFrequencyModulated() { return spinnerFrequencyModulate; } + + /** + * Returns whether the normal hitsound should always be played (and layered on other sounds). + */ + public boolean isLayeredHitSounds() { return layeredHitSounds; } + + /** + * Returns whether the playfield should fade for spinners. + */ + public boolean isSpinnerFadePlayfield() { return spinnerFadePlayfield; } + + /** + * Returns whether the last spinner bar should blink. + */ + public boolean isSpinnerNoBlink() { return spinnerNoBlink; } + + /** + * Returns whether the slider ball should be tinted with the slider combo color. + */ + public boolean isAllowSliderBallTint() { return allowSliderBallTint; } + + /** + * Returns the frame rate of animations. + * @return the FPS, or {@code -1} (TODO) + */ + public int getAnimationFramerate() { return animationFramerate; } + + /** + * Returns whether the cursor trail should rotate. + */ + public boolean isCursorTrailRotated() { return cursorTrailRotate; } + + /** + * Returns a list of combos with combo sounds. + */ + public int[] getCustomComboBurstSounds() { return customComboBurstSounds; } + + /** + * Returns whether combo bursts should appear in random order. + */ + public boolean isComboBurstRandom() { return comboBurstRandom; } + + /** + * Returns the slider style. + * + * @return the style (see STYLE_* constants) + */ + public byte getSliderStyle() { return sliderStyle; } + + /** + * Returns the list of combo colors (max 8). + */ + public Color[] getComboColors() { return combo; } + + /** + * Returns the menu visualization bar color. + */ + public Color getMenuGlowColor() { return menuGlow; } + + /** + * Returns the slider border color. + */ + public Color getSliderBorderColor() { return sliderBorder; } + + /** + * Returns the slider ball color. + */ + public Color getSliderBallColor() { return sliderBall; } + + /** + * Returns the spinner approach circle color. + */ + public Color getSpinnerApproachCircleColor() { return spinnerApproachCircle; } + + /** + * Returns the color of the active text in the song selection menu. + */ + public Color getSongSelectActiveTextColor() { return songSelectActiveText; } + + /** + * Returns the color of the inactive text in the song selection menu. + */ + public Color getSongSelectInactiveTextColor() { return songSelectInactiveText; } + + /** + * Returns the color of the stars that fall from the cursor during breaks. + */ + public Color getStarBreakAdditiveColor() { return starBreakAdditive; } + + /** + * Returns the prefix for the hit circle font sprites. + */ + public String getHitCircleFontPrefix() { return hitCirclePrefix; } + + /** + * Returns the amount of overlap between the hit circle font sprites. + */ + public int getHitCircleFontOverlap() { return hitCircleOverlap; } + + /** + * Returns the prefix for the score font sprites. + */ + public String getScoreFontPrefix() { return scorePrefix; } + + /** + * Returns the amount of overlap between the score font sprites. + */ + public int getScoreFontOverlap() { return scoreOverlap; } + + /** + * Returns the prefix for the combo font sprites. + */ + public String getComboFontPrefix() { return comboPrefix; } + + /** + * Returns the amount of overlap between the combo font sprites. + */ + public int getComboFontOverlap() { return comboOverlap; } +} diff --git a/src/itdelatrisu/opsu/skins/SkinLoader.java b/src/itdelatrisu/opsu/skins/SkinLoader.java new file mode 100644 index 00000000..a0a7362e --- /dev/null +++ b/src/itdelatrisu/opsu/skins/SkinLoader.java @@ -0,0 +1,299 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! 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 3 of the License, or + * (at your option) any later version. + * + * opsu! 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 opsu!. If not, see . + */ + +package itdelatrisu.opsu.skins; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.LinkedList; + +import org.newdawn.slick.Color; +import org.newdawn.slick.util.Log; + +/** + * Loads skin configuration files. + */ +public class SkinLoader { + /** Name of the skin configuration file. */ + private static final String CONFIG_FILENAME = "skin.ini"; + + // This class should not be instantiated. + private SkinLoader() {} + + /** + * Returns a list of all subdirectories in the Skins directory. + * @param root the root directory (search has depth 1) + * @return an array of skin directories + */ + public static File[] getSkinDirectories(File root) { + ArrayList dirs = new ArrayList(); + for (File dir : root.listFiles()) { + if (dir.isDirectory()) + dirs.add(dir); + } + return dirs.toArray(new File[dirs.size()]); + } + + /** + * Loads a skin configuration file. + * If 'skin.ini' is not found, or if any fields are not specified, the + * default values will be used. + * @param dir the skin directory + * @return the loaded skin + */ + public static Skin loadSkin(File dir) { + File skinFile = new File(dir, CONFIG_FILENAME); + Skin skin = new Skin(dir); + if (!skinFile.isFile()) // missing skin.ini + return skin; + + try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(skinFile), "UTF-8"))) { + String line = in.readLine(); + String tokens[] = null; + while (line != null) { + line = line.trim(); + if (!isValidLine(line)) { + line = in.readLine(); + continue; + } + switch (line) { + case "[General]": + while ((line = in.readLine()) != null) { + line = line.trim(); + if (!isValidLine(line)) + continue; + if (line.charAt(0) == '[') + break; + if ((tokens = tokenize(line)) == null) + continue; + try { + switch (tokens[0]) { + case "Name": + skin.name = tokens[1]; + break; + case "Author": + skin.author = tokens[1]; + break; + case "Version": + if (tokens[1].equalsIgnoreCase("latest")) + skin.version = Skin.LATEST_VERSION; + else + skin.version = Integer.parseInt(tokens[1]); + break; + case "SliderBallFlip": + skin.sliderBallFlip = Utils.parseBoolean(tokens[1]); + break; + case "CursorRotate": + skin.cursorRotate = Utils.parseBoolean(tokens[1]); + break; + case "CursorExpand": + skin.cursorExpand = Utils.parseBoolean(tokens[1]); + break; + case "CursorCentre": + skin.cursorCentre = Utils.parseBoolean(tokens[1]); + break; + case "SliderBallFrames": + skin.sliderBallFrames = Integer.parseInt(tokens[1]); + break; + case "HitCircleOverlayAboveNumber": + skin.hitCircleOverlayAboveNumber = Utils.parseBoolean(tokens[1]); + break; + case "spinnerFrequencyModulate": + skin.spinnerFrequencyModulate = Utils.parseBoolean(tokens[1]); + break; + case "LayeredHitSounds": + skin.layeredHitSounds = Utils.parseBoolean(tokens[1]); + break; + case "SpinnerFadePlayfield": + skin.spinnerFadePlayfield = Utils.parseBoolean(tokens[1]); + break; + case "SpinnerNoBlink": + skin.spinnerNoBlink = Utils.parseBoolean(tokens[1]); + break; + case "AllowSliderBallTint": + skin.allowSliderBallTint = Utils.parseBoolean(tokens[1]); + break; + case "AnimationFramerate": + skin.animationFramerate = Integer.parseInt(tokens[1]); + break; + case "CursorTrailRotate": + skin.cursorTrailRotate = Utils.parseBoolean(tokens[1]); + break; + case "CustomComboBurstSounds": + String[] split = tokens[1].split(","); + int[] customComboBurstSounds = new int[split.length]; + for (int i = 0; i < split.length; i++) + customComboBurstSounds[i] = Integer.parseInt(split[i]); + skin.customComboBurstSounds = customComboBurstSounds; + break; + case "ComboBurstRandom": + skin.comboBurstRandom = Utils.parseBoolean(tokens[1]); + break; + case "SliderStyle": + skin.sliderStyle = Byte.parseByte(tokens[1]); + break; + default: + break; + } + } catch (Exception e) { + Log.warn(String.format("Failed to read line '%s' for file '%s'.", + line, skinFile.getAbsolutePath()), e); + } + } + break; + case "[Colours]": + LinkedList colors = new LinkedList(); + while ((line = in.readLine()) != null) { + line = line.trim(); + if (!isValidLine(line)) + continue; + if (line.charAt(0) == '[') + break; + if ((tokens = tokenize(line)) == null) + continue; + try { + String[] rgb = tokens[1].split(","); + Color color = new Color( + Integer.parseInt(rgb[0]), + Integer.parseInt(rgb[1]), + Integer.parseInt(rgb[2]) + ); + switch (tokens[0]) { + case "Combo1": + case "Combo2": + case "Combo3": + case "Combo4": + case "Combo5": + case "Combo6": + case "Combo7": + case "Combo8": + colors.add(color); + break; + case "MenuGlow": + skin.menuGlow = color; + break; + case "SliderBorder": + skin.sliderBorder = color; + break; + case "SliderBall": + skin.sliderBall = color; + break; + case "SpinnerApproachCircle": + skin.spinnerApproachCircle = color; + break; + case "SongSelectActiveText": + skin.songSelectActiveText = color; + break; + case "SongSelectInactiveText": + skin.songSelectInactiveText = color; + break; + case "StarBreakAdditive": + skin.starBreakAdditive = color; + break; + default: + break; + } + } catch (Exception e) { + Log.warn(String.format("Failed to read color '%s' for file '%s'.", + line, skinFile.getAbsolutePath()), e); + } + } + if (!colors.isEmpty()) + skin.combo = colors.toArray(new Color[colors.size()]); + break; + case "[Fonts]": + while ((line = in.readLine()) != null) { + line = line.trim(); + if (!isValidLine(line)) + continue; + if (line.charAt(0) == '[') + break; + if ((tokens = tokenize(line)) == null) + continue; + try { + switch (tokens[0]) { + case "HitCirclePrefix": + skin.hitCirclePrefix = tokens[1]; + break; + case "HitCircleOverlap": + skin.hitCircleOverlap = Integer.parseInt(tokens[1]); + break; + case "ScorePrefix": + skin.scorePrefix = tokens[1]; + break; + case "ScoreOverlap": + skin.scoreOverlap = Integer.parseInt(tokens[1]); + break; + case "ComboPrefix": + skin.comboPrefix = tokens[1]; + break; + case "ComboOverlap": + skin.comboOverlap = Integer.parseInt(tokens[1]); + break; + default: + break; + } + } catch (Exception e) { + Log.warn(String.format("Failed to read color '%s' for file '%s'.", + line, skinFile.getAbsolutePath()), e); + } + } + break; + default: + line = in.readLine(); + break; + } + } + } catch (IOException e) { + ErrorHandler.error(String.format("Failed to read file '%s'.", skinFile.getAbsolutePath()), e, false); + } + + return skin; + } + + /** + * Returns false if the line is too short or commented. + */ + private static boolean isValidLine(String line) { + return (line.length() > 1 && !line.startsWith("//")); + } + + /** + * Splits line into two strings: tag, value. + * If no ':' character is present, null will be returned. + */ + private static String[] tokenize(String line) { + int index = line.indexOf(':'); + if (index == -1) { + Log.debug(String.format("Failed to tokenize line: '%s'.", line)); + return null; + } + + String[] tokens = new String[2]; + tokens[0] = line.substring(0, index).trim(); + tokens[1] = line.substring(index + 1).trim(); + return tokens; + } +} diff --git a/src/itdelatrisu/opsu/states/OptionsMenu.java b/src/itdelatrisu/opsu/states/OptionsMenu.java index ba6ff7f1..29cd42ee 100644 --- a/src/itdelatrisu/opsu/states/OptionsMenu.java +++ b/src/itdelatrisu/opsu/states/OptionsMenu.java @@ -52,6 +52,7 @@ public class OptionsMenu extends BasicGameState { DISPLAY ("Display", new GameOption[] { GameOption.SCREEN_RESOLUTION, // GameOption.FULLSCREEN, + GameOption.SKIN, GameOption.TARGET_FPS, GameOption.SHOW_FPS, GameOption.SHOW_UNICODE,