/* * 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 <http://www.gnu.org/licenses/>. */ package itdelatrisu.opsu.skins; import itdelatrisu.opsu.GameImage; 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; import yugecin.opsudance.core.events.EventBus; import yugecin.opsudance.events.BubbleNotificationEvent; /** * 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<File> dirs = new ArrayList<File>(); 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<Color> colors = new LinkedList<Color>(); 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": GameImage.DEFAULT_0.updatePrefix(tokens[1]); GameImage.DEFAULT_1.updatePrefix(tokens[1]); GameImage.DEFAULT_2.updatePrefix(tokens[1]); GameImage.DEFAULT_3.updatePrefix(tokens[1]); GameImage.DEFAULT_4.updatePrefix(tokens[1]); GameImage.DEFAULT_5.updatePrefix(tokens[1]); GameImage.DEFAULT_6.updatePrefix(tokens[1]); GameImage.DEFAULT_7.updatePrefix(tokens[1]); GameImage.DEFAULT_8.updatePrefix(tokens[1]); GameImage.DEFAULT_9.updatePrefix(tokens[1]); break; case "HitCircleOverlap": skin.hitCircleOverlap = Integer.parseInt(tokens[1]); break; case "ScorePrefix": GameImage.SCORE_0.updatePrefix(tokens[1]); GameImage.SCORE_1.updatePrefix(tokens[1]); GameImage.SCORE_2.updatePrefix(tokens[1]); GameImage.SCORE_3.updatePrefix(tokens[1]); GameImage.SCORE_4.updatePrefix(tokens[1]); GameImage.SCORE_5.updatePrefix(tokens[1]); GameImage.SCORE_6.updatePrefix(tokens[1]); GameImage.SCORE_7.updatePrefix(tokens[1]); GameImage.SCORE_8.updatePrefix(tokens[1]); GameImage.SCORE_9.updatePrefix(tokens[1]); GameImage.SCORE_COMMA.updatePrefix(tokens[1]); GameImage.SCORE_PERCENT.updatePrefix(tokens[1]); GameImage.SCORE_X.updatePrefix(tokens[1]); GameImage.SCORE_DOT.updatePrefix(tokens[1]); break; case "ScoreOverlap": skin.scoreOverlap = Integer.parseInt(tokens[1]); break; case "ComboPrefix": // TODO: seems like this uses the score images 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) { String err = String.format("Failed to read file '%s'.", skinFile.getAbsolutePath()); Log.error(err, e); EventBus.instance.post(new BubbleNotificationEvent(err, BubbleNotificationEvent.COMMONCOLOR_RED)); } 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; } }