WIPTest Merge Fixes
This commit is contained in:
parent
fd3f7fdabe
commit
038a5d5779
|
@ -1241,11 +1241,12 @@ public class GameData {
|
|||
}
|
||||
}
|
||||
public void sliderFinalResult(int time, int hitSlider30, float x, float y,
|
||||
OsuHitObject hitObject, int currentRepeats) {
|
||||
HitObject hitObject, int currentRepeats) {
|
||||
score += 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://osu.ppy.sh/wiki/Score
|
||||
* Returns the score for a hit based on the following score formula:
|
||||
* <p>
|
||||
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
|
||||
|
@ -1256,10 +1257,15 @@ public class GameData {
|
|||
* <li><strong>Mod:</strong> mod multipliers
|
||||
* </ul>
|
||||
* @param hitValue the hit value
|
||||
* @param hitObject
|
||||
* @return the score value
|
||||
*/
|
||||
private int getScoreForHit(int hitValue) {
|
||||
return hitValue + (int) (hitValue * (Math.max(combo - 1, 0) * difficulty * GameMod.getScoreMultiplier()) / 25);
|
||||
private int getScoreForHit(int hitValue, HitObject hitObject) {
|
||||
int comboMulti = Math.max(combo - 1, 0);
|
||||
if(hitObject.isSlider()){
|
||||
comboMulti += 1;
|
||||
}
|
||||
return (hitValue + (int)(hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1310,25 +1316,9 @@ public class GameData {
|
|||
hitObject.getEdgeHitSoundType(repeat),
|
||||
hitObject.getSampleSet(repeat),
|
||||
hitObject.getAdditionSampleSet(repeat));
|
||||
//TODO merge conflict
|
||||
|
||||
/**
|
||||
* https://osu.ppy.sh/wiki/Score
|
||||
* [SCORE FORMULA]
|
||||
* Score = Hit Value + Hit Value * (Combo * Difficulty * Mod) / 25
|
||||
* - Hit Value: hit result (50, 100, 300), slider ticks, spinner bonus
|
||||
* - Combo: combo before this hit - 1 (minimum 0)
|
||||
* - Difficulty: the beatmap difficulty
|
||||
* - Mod: mod multipliers
|
||||
*/
|
||||
int comboMulti = Math.max(combo - 1, 0);
|
||||
if(hitObject.isSlider()){
|
||||
comboMulti += 1;
|
||||
}
|
||||
score += (hitValue + (hitValue * (comboMulti * getDifficultyMultiplier() * GameMod.getScoreMultiplier()) / 25));
|
||||
|
||||
// calculate score and increment combo streak
|
||||
changeScore(getScoreForHit(hitValue));
|
||||
changeScore(getScoreForHit(hitValue, hitObject));
|
||||
incrementComboStreak();
|
||||
//merge conflict end
|
||||
}
|
||||
|
@ -1398,8 +1388,10 @@ public class GameData {
|
|||
return difficultyMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier
|
||||
*/
|
||||
public void calculateDifficultyMultiplier() {
|
||||
//https://osu.ppy.sh/wiki/Score#How_to_calculate_the_Difficulty_multiplier
|
||||
//TODO THE LIES ( difficultyMultiplier )
|
||||
/*
|
||||
924 3x1/4 beat notes 0.14stars
|
||||
|
@ -1408,8 +1400,9 @@ public class GameData {
|
|||
|
||||
seems to be based on hitobject density? (Total Objects/Time)
|
||||
*/
|
||||
|
||||
difficultyMultiplier = (int)((circleSize + difficulty + drainRate) / 6) + 2;
|
||||
float mult = ((circleSize + difficulty + drainRate) / 6) + 1.5f;
|
||||
System.out.println("diffuculty Multiplier : "+ mult);
|
||||
difficultyMultiplier = (int)mult;
|
||||
}
|
||||
/**
|
||||
* Returns a ScoreData object encapsulating all game data.
|
||||
|
|
|
@ -1,440 +0,0 @@
|
|||
//TODO rename
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Data type storing parsed data from OSU files.
|
||||
*/
|
||||
public class OsuFile implements Comparable<OsuFile> {
|
||||
/** Game modes. */
|
||||
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
|
||||
|
||||
/** Map of all loaded background images. */
|
||||
private static HashMap<OsuFile, Image> bgImageMap = new HashMap<OsuFile, Image>();
|
||||
|
||||
/** Maximum number of cached images before all get erased. */
|
||||
private static final int MAX_CACHE_SIZE = 10;
|
||||
|
||||
/** The OSU File object associated with this OsuFile. */
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* [General]
|
||||
*/
|
||||
|
||||
/** Audio file object. */
|
||||
public File audioFilename;
|
||||
|
||||
/** Delay time before music starts (in ms). */
|
||||
public int audioLeadIn = 0;
|
||||
|
||||
/** Audio hash (deprecated). */
|
||||
// public String audioHash = "";
|
||||
|
||||
/** Start position of music preview (in ms). */
|
||||
public int previewTime = -1;
|
||||
|
||||
/** Countdown type (0:disabled, 1:normal, 2:half, 3:double). */
|
||||
public byte countdown = 0;
|
||||
|
||||
/** Sound samples ("None", "Normal", "Soft"). */
|
||||
public String sampleSet = "";
|
||||
|
||||
/** How often closely placed hit objects will be stacked together. */
|
||||
public float stackLeniency = 0.7f;
|
||||
|
||||
/** Game mode (MODE_* constants). */
|
||||
public byte mode = MODE_OSU;
|
||||
|
||||
/** Whether the letterbox (top/bottom black bars) appears during breaks. */
|
||||
public boolean letterboxInBreaks = false;
|
||||
|
||||
/** Whether the storyboard should be widescreen. */
|
||||
public boolean widescreenStoryboard = false;
|
||||
|
||||
/** Whether to show an epilepsy warning. */
|
||||
public boolean epilepsyWarning = false;
|
||||
|
||||
/**
|
||||
* [Editor]
|
||||
*/
|
||||
|
||||
/** List of editor bookmarks (in ms). */
|
||||
// public int[] bookmarks;
|
||||
|
||||
/** Multiplier for "Distance Snap". */
|
||||
// public float distanceSpacing = 0f;
|
||||
|
||||
/** Beat division. */
|
||||
// public byte beatDivisor = 0;
|
||||
|
||||
/** Size of grid for "Grid Snap". */
|
||||
// public int gridSize = 0;
|
||||
|
||||
/** Zoom in the editor timeline. */
|
||||
// public int timelineZoom = 0;
|
||||
|
||||
/**
|
||||
* [Metadata]
|
||||
*/
|
||||
|
||||
/** Song title. */
|
||||
public String title = "", titleUnicode = "";
|
||||
|
||||
/** Song artist. */
|
||||
public String artist = "", artistUnicode = "";
|
||||
|
||||
/** Beatmap creator. */
|
||||
public String creator = "";
|
||||
|
||||
/** Beatmap difficulty. */
|
||||
public String version = "";
|
||||
|
||||
/** Song source. */
|
||||
public String source = "";
|
||||
|
||||
/** Song tags (for searching). */
|
||||
public String tags = "";
|
||||
|
||||
/** Beatmap ID. */
|
||||
public int beatmapID = 0;
|
||||
|
||||
/** Beatmap set ID. */
|
||||
public int beatmapSetID = 0;
|
||||
|
||||
/**
|
||||
* [Difficulty]
|
||||
*/
|
||||
|
||||
/** HP: Health drain rate (0:easy ~ 10:hard) */
|
||||
public float HPDrainRate = 5f;
|
||||
|
||||
/** CS: Size of circles and sliders (0:large ~ 10:small). */
|
||||
public float circleSize = 4f;
|
||||
|
||||
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
|
||||
public float overallDifficulty = 5f;
|
||||
|
||||
/** AR: How long circles stay on the screen (0:long ~ 10:short). */
|
||||
public float approachRate = -1f;
|
||||
|
||||
/** Slider movement speed multiplier. */
|
||||
public float sliderMultiplier = 1f;
|
||||
|
||||
/** Rate at which slider ticks are placed (x per beat). */
|
||||
public float sliderTickRate = 1f;
|
||||
|
||||
/**
|
||||
* [Events]
|
||||
*/
|
||||
|
||||
/** Background image file name. */
|
||||
public String bg;
|
||||
|
||||
/** Background video file name. */
|
||||
// public String video;
|
||||
|
||||
/** All break periods (start time, end time, ...). */
|
||||
public ArrayList<Integer> breaks;
|
||||
|
||||
/**
|
||||
* [TimingPoints]
|
||||
*/
|
||||
|
||||
/** All timing points. */
|
||||
public ArrayList<OsuTimingPoint> timingPoints;
|
||||
|
||||
/** Song BPM range. */
|
||||
public int bpmMin = 0, bpmMax = 0;
|
||||
|
||||
/**
|
||||
* [Colours]
|
||||
*/
|
||||
|
||||
/** Combo colors (max 8). */
|
||||
public Color[] combo;
|
||||
|
||||
/** md5 hash of this file */
|
||||
public String md5Hash;
|
||||
|
||||
/**
|
||||
* [HitObjects]
|
||||
*/
|
||||
|
||||
/** All hit objects. */
|
||||
public OsuHitObject[] objects;
|
||||
|
||||
/** Number of individual objects. */
|
||||
public int
|
||||
hitObjectCircle = 0,
|
||||
hitObjectSlider = 0,
|
||||
hitObjectSpinner = 0;
|
||||
|
||||
/** Last object end time (in ms). */
|
||||
public int endTime = -1;
|
||||
|
||||
/**
|
||||
* Destroys all cached background images and resets the cache.
|
||||
*/
|
||||
public static void clearImageCache() {
|
||||
for (Image img : bgImageMap.values()) {
|
||||
if (img != null && !img.isDestroyed()) {
|
||||
try {
|
||||
img.destroy();
|
||||
} catch (SlickException e) {
|
||||
Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
resetImageCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the image cache.
|
||||
* This does NOT destroy images, so be careful of memory leaks!
|
||||
*/
|
||||
public static void resetImageCache() {
|
||||
bgImageMap = new HashMap<OsuFile, Image>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file associated with this OsuFile
|
||||
*/
|
||||
public OsuFile(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated file object.
|
||||
* @return the File object
|
||||
*/
|
||||
public File getFile() { return file; }
|
||||
|
||||
/**
|
||||
* Returns the song title.
|
||||
* If configured, the Unicode string will be returned instead.
|
||||
* @return the song title
|
||||
*/
|
||||
public String getTitle() {
|
||||
return (Options.useUnicodeMetadata() && !titleUnicode.isEmpty()) ? titleUnicode : title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the song artist.
|
||||
* If configured, the Unicode string will be returned instead.
|
||||
* @return the song artist
|
||||
*/
|
||||
public String getArtist() {
|
||||
return (Options.useUnicodeMetadata() && !artistUnicode.isEmpty()) ? artistUnicode : artist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the background associated with the OsuFile.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param alpha the alpha value
|
||||
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
||||
* @return true if successful, false if any errors were produced
|
||||
*/
|
||||
public boolean drawBG(int width, int height, float alpha, boolean stretch) {
|
||||
if (bg == null)
|
||||
return false;
|
||||
try {
|
||||
Image bgImage = bgImageMap.get(this);
|
||||
if (bgImage == null) {
|
||||
if (bgImageMap.size() > MAX_CACHE_SIZE)
|
||||
clearImageCache();
|
||||
bgImage = new Image(new File(file.getParentFile(), bg).getAbsolutePath());
|
||||
bgImageMap.put(this, bgImage);
|
||||
}
|
||||
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (!stretch) {
|
||||
// fit image to screen
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
else
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
} else {
|
||||
// fill screen while maintaining aspect ratio
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
else
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
}
|
||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.drawCentered(width / 2, height / 2);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
||||
bg = null; // don't try to load the file again until a restart
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two OsuFile objects first by overall difficulty, then by total objects.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(OsuFile that) {
|
||||
int cmp = Float.compare(this.overallDifficulty, that.overallDifficulty);
|
||||
if (cmp == 0)
|
||||
cmp = Integer.compare(
|
||||
this.hitObjectCircle + this.hitObjectSlider + this.hitObjectSpinner,
|
||||
that.hitObjectCircle + that.hitObjectSlider + that.hitObjectSpinner
|
||||
);
|
||||
return cmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string: "Artist - Title [Version]"
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - %s [%s]", getArtist(), getTitle(), version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #breaks} field formatted as a string,
|
||||
* or null if the field is null.
|
||||
*/
|
||||
public String breaksToString() {
|
||||
if (breaks == null)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i : breaks) {
|
||||
sb.append(i);
|
||||
sb.append(',');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #breaks} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void breaksFromString(String s) {
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
this.breaks = new ArrayList<Integer>();
|
||||
String[] tokens = s.split(",");
|
||||
for (int i = 0; i < tokens.length; i++)
|
||||
breaks.add(Integer.parseInt(tokens[i]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #timingPoints} field formatted as a string,
|
||||
* or null if the field is null.
|
||||
*/
|
||||
public String timingPointsToString() {
|
||||
if (timingPoints == null)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (OsuTimingPoint p : timingPoints) {
|
||||
sb.append(p.toString());
|
||||
sb.append('|');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #timingPoints} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void timingPointsFromString(String s) {
|
||||
this.timingPoints = new ArrayList<OsuTimingPoint>();
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
String[] tokens = s.split("\\|");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
try {
|
||||
timingPoints.add(new OsuTimingPoint(tokens[i]));
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read timing point '%s'.", tokens[i]), e);
|
||||
}
|
||||
}
|
||||
timingPoints.trimToSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #combo} field formatted as a string,
|
||||
* or null if the field is null or the default combo.
|
||||
*/
|
||||
public String comboToString() {
|
||||
if (combo == null || combo == Utils.DEFAULT_COMBO)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < combo.length; i++) {
|
||||
Color c = combo[i];
|
||||
sb.append(c.getRed());
|
||||
sb.append(',');
|
||||
sb.append(c.getGreen());
|
||||
sb.append(',');
|
||||
sb.append(c.getBlue());
|
||||
sb.append('|');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #combo} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void comboFromString(String s) {
|
||||
this.combo = Utils.DEFAULT_COMBO;
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
LinkedList<Color> colors = new LinkedList<Color>();
|
||||
String[] tokens = s.split("\\|");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
String[] rgb = tokens[i].split(",");
|
||||
colors.add(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
this.combo = colors.toArray(new Color[colors.size()]);
|
||||
}
|
||||
}
|
|
@ -1,516 +0,0 @@
|
|||
//TODO rename
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.db.OsuDB;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Indexed, expanding, doubly-linked list data type for song groups.
|
||||
*/
|
||||
public class OsuGroupList {
|
||||
/** Song group structure (each group contains of an ArrayList of OsuFiles). */
|
||||
private static OsuGroupList list;
|
||||
|
||||
/** Search pattern for conditional expressions. */
|
||||
private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
|
||||
"(ar|cs|od|hp|bpm|length)(=|==|>|>=|<|<=)((\\d*\\.)?\\d+)"
|
||||
);
|
||||
|
||||
/** List containing all parsed nodes. */
|
||||
private ArrayList<OsuGroupNode> parsedNodes;
|
||||
|
||||
/** Total number of beatmaps (i.e. OsuFile objects). */
|
||||
private int mapCount = 0;
|
||||
|
||||
/** Current list of nodes (subset of parsedNodes, used for searches). */
|
||||
private ArrayList<OsuGroupNode> nodes;
|
||||
|
||||
/** Set of all beatmap set IDs for the parsed beatmaps. */
|
||||
private HashSet<Integer> MSIDdb;
|
||||
|
||||
/** Map of all hash to OsuFile . */
|
||||
public HashMap<String, OsuFile> beatmapHashesToFile;
|
||||
|
||||
|
||||
/** Index of current expanded node (-1 if no node is expanded). */
|
||||
private int expandedIndex;
|
||||
|
||||
/** Start and end nodes of expanded group. */
|
||||
private OsuGroupNode expandedStartNode, expandedEndNode;
|
||||
|
||||
/** The last search query. */
|
||||
private String lastQuery;
|
||||
|
||||
/**
|
||||
* Creates a new instance of this class (overwriting any previous instance).
|
||||
*/
|
||||
public static void create() { list = new OsuGroupList(); }
|
||||
|
||||
/**
|
||||
* Returns the single instance of this class.
|
||||
*/
|
||||
public static OsuGroupList get() { return list; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private OsuGroupList() {
|
||||
parsedNodes = new ArrayList<OsuGroupNode>();
|
||||
MSIDdb = new HashSet<Integer>();
|
||||
beatmapHashesToFile = new HashMap<String, OsuFile>();
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the list's fields.
|
||||
* This does not erase any parsed nodes.
|
||||
*/
|
||||
public void reset() {
|
||||
nodes = parsedNodes;
|
||||
expandedIndex = -1;
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
lastQuery = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements.
|
||||
*/
|
||||
public int size() { return nodes.size(); }
|
||||
|
||||
/**
|
||||
* Adds a song group.
|
||||
* @param osuFiles the list of OsuFile objects in the group
|
||||
* @return the new OsuGroupNode
|
||||
*/
|
||||
public OsuGroupNode addSongGroup(ArrayList<OsuFile> osuFiles) {
|
||||
OsuGroupNode node = new OsuGroupNode(osuFiles);
|
||||
parsedNodes.add(node);
|
||||
mapCount += osuFiles.size();
|
||||
|
||||
// add beatmap set ID to set
|
||||
int msid = osuFiles.get(0).beatmapSetID;
|
||||
if (msid > 0)
|
||||
MSIDdb.add(msid);
|
||||
for(OsuFile f : osuFiles) {
|
||||
beatmapHashesToFile.put(f.md5Hash, f);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a song group from the list, and also deletes the beatmap
|
||||
* directory associated with the node.
|
||||
* @param node the node containing the song group to delete
|
||||
* @return true if the song group was deleted, false otherwise
|
||||
*/
|
||||
public boolean deleteSongGroup(OsuGroupNode node) {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
// re-link base nodes
|
||||
int index = node.index;
|
||||
OsuGroupNode ePrev = getBaseNode(index - 1), eCur = getBaseNode(index), eNext = getBaseNode(index + 1);
|
||||
if (ePrev != null) {
|
||||
if (ePrev.index == expandedIndex)
|
||||
expandedEndNode.next = eNext;
|
||||
else if (eNext != null && eNext.index == expandedIndex)
|
||||
ePrev.next = expandedStartNode;
|
||||
else
|
||||
ePrev.next = eNext;
|
||||
}
|
||||
if (eNext != null) {
|
||||
if (eNext.index == expandedIndex)
|
||||
expandedStartNode.prev = ePrev;
|
||||
else if (ePrev != null && ePrev.index == expandedIndex)
|
||||
eNext.prev = expandedEndNode;
|
||||
else
|
||||
eNext.prev = ePrev;
|
||||
}
|
||||
|
||||
// remove all node references
|
||||
OsuFile osu = node.osuFiles.get(0);
|
||||
nodes.remove(index);
|
||||
parsedNodes.remove(eCur);
|
||||
mapCount -= node.osuFiles.size();
|
||||
if (osu.beatmapSetID > 0)
|
||||
MSIDdb.remove(osu.beatmapSetID);
|
||||
|
||||
// reset indices
|
||||
for (int i = index, size = size(); i < size; i++)
|
||||
nodes.get(i).index = i;
|
||||
if (index == expandedIndex) {
|
||||
expandedIndex = -1;
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
} else if (expandedIndex > index) {
|
||||
expandedIndex--;
|
||||
OsuGroupNode expandedNode = expandedStartNode;
|
||||
for (int i = 0, size = expandedNode.osuFiles.size();
|
||||
i < size && expandedNode != null;
|
||||
i++, expandedNode = expandedNode.next)
|
||||
expandedNode.index = expandedIndex;
|
||||
}
|
||||
|
||||
// stop playing the track
|
||||
File dir = osu.getFile().getParentFile();
|
||||
if (MusicController.trackExists() || MusicController.isTrackLoading()) {
|
||||
File audioFile = MusicController.getOsuFile().audioFilename;
|
||||
if (audioFile != null && audioFile.equals(osu.audioFilename)) {
|
||||
MusicController.reset();
|
||||
System.gc(); // TODO: why can't files be deleted without calling this?
|
||||
}
|
||||
}
|
||||
|
||||
// remove entry from cache
|
||||
OsuDB.delete(dir.getName());
|
||||
|
||||
// delete the associated directory
|
||||
try {
|
||||
Utils.deleteToTrash(dir);
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not delete song group.", e, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a song from a song group, and also deletes the beatmap file.
|
||||
* If this causes the song group to be empty, then the song group and
|
||||
* beatmap directory will be deleted altogether.
|
||||
* @param node the node containing the song group to delete (expanded only)
|
||||
* @return true if the song or song group was deleted, false otherwise
|
||||
* @see #deleteSongGroup(OsuGroupNode)
|
||||
*/
|
||||
public boolean deleteSong(OsuGroupNode node) {
|
||||
if (node == null || node.osuFileIndex == -1 || node.index != expandedIndex)
|
||||
return false;
|
||||
|
||||
// last song in group?
|
||||
int size = node.osuFiles.size();
|
||||
if (node.osuFiles.size() == 1)
|
||||
return deleteSongGroup(node);
|
||||
|
||||
// reset indices
|
||||
OsuGroupNode expandedNode = node.next;
|
||||
for (int i = node.osuFileIndex + 1;
|
||||
i < size && expandedNode != null && expandedNode.index == node.index;
|
||||
i++, expandedNode = expandedNode.next)
|
||||
expandedNode.osuFileIndex--;
|
||||
|
||||
// remove song reference
|
||||
OsuFile osu = node.osuFiles.remove(node.osuFileIndex);
|
||||
mapCount--;
|
||||
|
||||
// re-link nodes
|
||||
if (node.prev != null)
|
||||
node.prev.next = node.next;
|
||||
if (node.next != null)
|
||||
node.next.prev = node.prev;
|
||||
|
||||
// remove entry from cache
|
||||
File file = osu.getFile();
|
||||
OsuDB.delete(file.getParentFile().getName(), file.getName());
|
||||
|
||||
// delete the associated file
|
||||
try {
|
||||
Utils.deleteToTrash(file);
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not delete song.", e, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of parsed maps (i.e. OsuFile objects).
|
||||
*/
|
||||
public int getMapCount() { return mapCount; }
|
||||
|
||||
/**
|
||||
* Returns the total number of parsed maps sets.
|
||||
*/
|
||||
public int getMapSetCount() { return parsedNodes.size(); }
|
||||
|
||||
/**
|
||||
* Returns the OsuGroupNode at an index, disregarding expansions.
|
||||
* @param index the node index
|
||||
*/
|
||||
public OsuGroupNode getBaseNode(int index) {
|
||||
if (index < 0 || index >= size())
|
||||
return null;
|
||||
|
||||
return nodes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random base node.
|
||||
*/
|
||||
public OsuGroupNode getRandomNode() {
|
||||
OsuGroupNode node = getBaseNode((int) (Math.random() * size()));
|
||||
if (node != null && node.index == expandedIndex) // don't choose an expanded group node
|
||||
node = node.next;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OsuGroupNode a given number of positions forward or backwards.
|
||||
* @param node the starting node
|
||||
* @param shift the number of nodes to shift forward (+) or backward (-).
|
||||
*/
|
||||
public OsuGroupNode getNode(OsuGroupNode node, int shift) {
|
||||
OsuGroupNode startNode = node;
|
||||
if (shift > 0) {
|
||||
for (int i = 0; i < shift && startNode != null; i++)
|
||||
startNode = startNode.next;
|
||||
} else {
|
||||
for (int i = 0; i < shift && startNode != null; i++)
|
||||
startNode = startNode.prev;
|
||||
}
|
||||
return startNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the expanded node (or -1 if nothing is expanded).
|
||||
*/
|
||||
public int getExpandedIndex() { return expandedIndex; }
|
||||
|
||||
/**
|
||||
* Expands the node at an index by inserting a new node for each OsuFile
|
||||
* in that node and hiding the group node.
|
||||
* @return the first of the newly-inserted nodes
|
||||
*/
|
||||
public OsuGroupNode expand(int index) {
|
||||
// undo the previous expansion
|
||||
unexpand();
|
||||
|
||||
OsuGroupNode node = getBaseNode(index);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
|
||||
// create new nodes
|
||||
ArrayList<OsuFile> osuFiles = node.osuFiles;
|
||||
OsuGroupNode prevNode = node.prev;
|
||||
OsuGroupNode nextNode = node.next;
|
||||
for (int i = 0, size = node.osuFiles.size(); i < size; i++) {
|
||||
OsuGroupNode newNode = new OsuGroupNode(osuFiles);
|
||||
newNode.index = index;
|
||||
newNode.osuFileIndex = i;
|
||||
newNode.prev = node;
|
||||
|
||||
// unlink the group node
|
||||
if (i == 0) {
|
||||
expandedStartNode = newNode;
|
||||
newNode.prev = prevNode;
|
||||
if (prevNode != null)
|
||||
prevNode.next = newNode;
|
||||
}
|
||||
|
||||
node.next = newNode;
|
||||
node = node.next;
|
||||
}
|
||||
if (nextNode != null) {
|
||||
node.next = nextNode;
|
||||
nextNode.prev = node;
|
||||
}
|
||||
expandedEndNode = node;
|
||||
|
||||
expandedIndex = index;
|
||||
return expandedStartNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undoes the current expansion, if any.
|
||||
*/
|
||||
private void unexpand() {
|
||||
if (expandedIndex < 0 || expandedIndex >= size())
|
||||
return;
|
||||
|
||||
// recreate surrounding links
|
||||
OsuGroupNode
|
||||
ePrev = getBaseNode(expandedIndex - 1),
|
||||
eCur = getBaseNode(expandedIndex),
|
||||
eNext = getBaseNode(expandedIndex + 1);
|
||||
if (ePrev != null)
|
||||
ePrev.next = eCur;
|
||||
eCur.prev = ePrev;
|
||||
eCur.index = expandedIndex;
|
||||
eCur.next = eNext;
|
||||
if (eNext != null)
|
||||
eNext.prev = eCur;
|
||||
|
||||
expandedIndex = -1;
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the links in the list.
|
||||
*/
|
||||
public void init() {
|
||||
if (size() < 1)
|
||||
return;
|
||||
|
||||
// sort the list
|
||||
Collections.sort(nodes, SongSort.getSort().getComparator());
|
||||
expandedIndex = -1;
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
|
||||
// create links
|
||||
OsuGroupNode lastNode = nodes.get(0);
|
||||
lastNode.index = 0;
|
||||
lastNode.prev = null;
|
||||
for (int i = 1, size = size(); i < size; i++) {
|
||||
OsuGroupNode node = nodes.get(i);
|
||||
lastNode.next = node;
|
||||
node.index = i;
|
||||
node.prev = lastNode;
|
||||
|
||||
lastNode = node;
|
||||
}
|
||||
lastNode.next = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new list of song groups in which each group contains a match to a search query.
|
||||
* @param query the search query (terms separated by spaces)
|
||||
* @return false if query is the same as the previous one, true otherwise
|
||||
*/
|
||||
public boolean search(String query) {
|
||||
if (query == null)
|
||||
return false;
|
||||
|
||||
// don't redo the same search
|
||||
query = query.trim().toLowerCase();
|
||||
if (lastQuery != null && query.equals(lastQuery))
|
||||
return false;
|
||||
lastQuery = query;
|
||||
LinkedList<String> terms = new LinkedList<String>(Arrays.asList(query.split("\\s+")));
|
||||
|
||||
// if empty query, reset to original list
|
||||
if (query.isEmpty() || terms.isEmpty()) {
|
||||
nodes = parsedNodes;
|
||||
return true;
|
||||
}
|
||||
|
||||
// find and remove any conditional search terms
|
||||
LinkedList<String> condType = new LinkedList<String>();
|
||||
LinkedList<String> condOperator = new LinkedList<String>();
|
||||
LinkedList<Float> condValue = new LinkedList<Float>();
|
||||
|
||||
Iterator<String> termIter = terms.iterator();
|
||||
while (termIter.hasNext()) {
|
||||
String term = termIter.next();
|
||||
Matcher m = SEARCH_CONDITION_PATTERN.matcher(term);
|
||||
if (m.find()) {
|
||||
condType.add(m.group(1));
|
||||
condOperator.add(m.group(2));
|
||||
condValue.add(Float.parseFloat(m.group(3)));
|
||||
termIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// build an initial list from first search term
|
||||
nodes = new ArrayList<OsuGroupNode>();
|
||||
if (terms.isEmpty()) {
|
||||
// conditional term
|
||||
String type = condType.remove();
|
||||
String operator = condOperator.remove();
|
||||
float value = condValue.remove();
|
||||
for (OsuGroupNode node : parsedNodes) {
|
||||
if (node.matches(type, operator, value))
|
||||
nodes.add(node);
|
||||
}
|
||||
} else {
|
||||
// normal term
|
||||
String term = terms.remove();
|
||||
for (OsuGroupNode node : parsedNodes) {
|
||||
if (node.matches(term))
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through remaining normal search terms
|
||||
while (!terms.isEmpty()) {
|
||||
if (nodes.isEmpty())
|
||||
return true;
|
||||
|
||||
String term = terms.remove();
|
||||
|
||||
// remove nodes from list if they don't match all terms
|
||||
Iterator<OsuGroupNode> nodeIter = nodes.iterator();
|
||||
while (nodeIter.hasNext()) {
|
||||
OsuGroupNode node = nodeIter.next();
|
||||
if (!node.matches(term))
|
||||
nodeIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through remaining conditional terms
|
||||
while (!condType.isEmpty()) {
|
||||
if (nodes.isEmpty())
|
||||
return true;
|
||||
|
||||
String type = condType.remove();
|
||||
String operator = condOperator.remove();
|
||||
float value = condValue.remove();
|
||||
|
||||
// remove nodes from list if they don't match all terms
|
||||
Iterator<OsuGroupNode> nodeIter = nodes.iterator();
|
||||
while (nodeIter.hasNext()) {
|
||||
OsuGroupNode node = nodeIter.next();
|
||||
if (!node.matches(type, operator, value))
|
||||
nodeIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the list contains the given beatmap set ID.
|
||||
* <p>
|
||||
* Note that IDs for older maps might have been improperly parsed, so
|
||||
* there is no guarantee that this method will return an accurate value.
|
||||
* @param id the beatmap set ID to check
|
||||
* @return true if id is in the list
|
||||
*/
|
||||
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
|
||||
|
||||
public OsuFile getFileFromBeatmapHash(String beatmapHash) {
|
||||
return beatmapHashesToFile.get(beatmapHash);
|
||||
}
|
||||
}
|
|
@ -1,543 +0,0 @@
|
|||
//TODO rename
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Data type representing a hit object.
|
||||
*/
|
||||
public class OsuHitObject {
|
||||
/** Hit object types (bits). */
|
||||
public static final int
|
||||
TYPE_CIRCLE = 1,
|
||||
TYPE_SLIDER = 2,
|
||||
TYPE_NEWCOMBO = 4, // not an object
|
||||
TYPE_SPINNER = 8;
|
||||
|
||||
/** Hit object type names. */
|
||||
private static final String
|
||||
CIRCLE = "circle",
|
||||
SLIDER = "slider",
|
||||
SPINNER = "spinner",
|
||||
UNKNOWN = "unknown object";
|
||||
|
||||
/** Hit sound types (bits). */
|
||||
public static final byte
|
||||
SOUND_NORMAL = 0,
|
||||
SOUND_WHISTLE = 2,
|
||||
SOUND_FINISH = 4,
|
||||
SOUND_CLAP = 8;
|
||||
|
||||
/**
|
||||
* Slider curve types.
|
||||
* (Deprecated: only Beziers are currently used.)
|
||||
*/
|
||||
public static final char
|
||||
SLIDER_CATMULL = 'C',
|
||||
SLIDER_BEZIER = 'B',
|
||||
SLIDER_LINEAR = 'L',
|
||||
SLIDER_PASSTHROUGH = 'P';
|
||||
|
||||
/** Max hit object coordinates. */
|
||||
private static final int
|
||||
MAX_X = 512,
|
||||
MAX_Y = 384;
|
||||
|
||||
/** The x and y multipliers for hit object coordinates. */
|
||||
private static float xMultiplier, yMultiplier;
|
||||
|
||||
/** The x and y offsets for hit object coordinates. */
|
||||
private static int
|
||||
xOffset, // offset right of border
|
||||
yOffset; // offset below health bar
|
||||
|
||||
/** The container height. */
|
||||
private static int containerHeight;
|
||||
|
||||
/** The offset per stack. */
|
||||
private static float stackOffset;
|
||||
|
||||
/**
|
||||
* Returns the stack position modifier, in pixels.
|
||||
* @return stack position modifier
|
||||
*/
|
||||
public static float getStackOffset() { return stackOffset; }
|
||||
|
||||
/**
|
||||
* Sets the stack position modifier.
|
||||
* @param offset stack position modifier, in pixels
|
||||
*/
|
||||
public static void setStackOffset(float offset) { stackOffset = offset; }
|
||||
|
||||
/** Starting coordinates. */
|
||||
private float x, y;
|
||||
|
||||
/** Start time (in ms). */
|
||||
private int time;
|
||||
|
||||
/** Hit object type (TYPE_* bitmask). */
|
||||
private int type;
|
||||
|
||||
/** Hit sound type (SOUND_* bitmask). */
|
||||
private byte hitSound;
|
||||
|
||||
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
|
||||
private byte[] addition;
|
||||
|
||||
/** Slider curve type (SLIDER_* constant). */
|
||||
private char sliderType;
|
||||
|
||||
/** Slider coordinate lists. */
|
||||
private float[] sliderX, sliderY;
|
||||
|
||||
/** Slider repeat count. */
|
||||
private int repeat;
|
||||
|
||||
/** Slider pixel length. */
|
||||
private float pixelLength;
|
||||
|
||||
/** Spinner end time (in ms). */
|
||||
private int endTime;
|
||||
|
||||
/** Slider edge hit sound type (SOUND_* bitmask). */
|
||||
private byte[] edgeHitSound;
|
||||
|
||||
/** Slider edge hit sound addition (sampleSet, AdditionSampleSet). */
|
||||
private byte[][] edgeAddition;
|
||||
|
||||
/** Current index in combo color array. */
|
||||
private int comboIndex;
|
||||
|
||||
/** Number to display in hit object. */
|
||||
private int comboNumber;
|
||||
|
||||
/** Hit object index in the current stack. */
|
||||
private int stack;
|
||||
|
||||
/**
|
||||
* Initializes the OsuHitObject data type with container dimensions.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
*/
|
||||
public static void init(int width, int height) {
|
||||
containerHeight = height;
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (swidth * 3 > sheight * 4)
|
||||
swidth = sheight * 4 / 3;
|
||||
else
|
||||
sheight = swidth * 3 / 4;
|
||||
xMultiplier = swidth / 640f;
|
||||
yMultiplier = sheight / 480f;
|
||||
|
||||
//xMultiplier = swidth / MAX_X;
|
||||
//yMultiplier = sheight / MAX_Y;
|
||||
xOffset = (int) (width - MAX_X * xMultiplier) / 2;
|
||||
yOffset = (int) (height - MAX_Y * yMultiplier) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X multiplier for coordinates.
|
||||
*/
|
||||
public static float getXMultiplier() { return xMultiplier; }
|
||||
|
||||
/**
|
||||
* Returns the Y multiplier for coordinates.
|
||||
*/
|
||||
public static float getYMultiplier() { return yMultiplier; }
|
||||
|
||||
/**
|
||||
* Returns the X offset for coordinates.
|
||||
*/
|
||||
public static int getXOffset() { return xOffset; }
|
||||
|
||||
/**
|
||||
* Returns the Y offset for coordinates.
|
||||
*/
|
||||
public static int getYOffset() { return yOffset; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param line the line to be parsed
|
||||
*/
|
||||
public OsuHitObject(String line) {
|
||||
/**
|
||||
* [OBJECT FORMATS]
|
||||
* Circles:
|
||||
* x,y,time,type,hitSound,addition
|
||||
* 256,148,9466,1,2,0:0:0:0:
|
||||
*
|
||||
* Sliders:
|
||||
* x,y,time,type,hitSound,sliderType|curveX:curveY|...,repeat,pixelLength,edgeHitsound,edgeAddition,addition
|
||||
* 300,68,4591,2,0,B|372:100|332:172|420:192,2,180,2|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
*
|
||||
* Spinners:
|
||||
* x,y,time,type,hitSound,endTime,addition
|
||||
* 256,192,654,12,0,4029,0:0:0:0:
|
||||
*
|
||||
* NOTE: 'addition' -> sampl:add:cust:vol:hitsound (optional, defaults to "0:0:0:0:")
|
||||
*/
|
||||
String tokens[] = line.split(",");
|
||||
|
||||
// common fields
|
||||
this.x = Float.parseFloat(tokens[0]);
|
||||
this.y = Float.parseFloat(tokens[1]);
|
||||
this.time = Integer.parseInt(tokens[2]);
|
||||
this.type = Integer.parseInt(tokens[3]);
|
||||
this.hitSound = Byte.parseByte(tokens[4]);
|
||||
|
||||
// type-specific fields
|
||||
int additionIndex;
|
||||
if ((type & OsuHitObject.TYPE_CIRCLE) > 0)
|
||||
additionIndex = 5;
|
||||
else if ((type & OsuHitObject.TYPE_SLIDER) > 0) {
|
||||
additionIndex = 10;
|
||||
|
||||
// slider curve type and coordinates
|
||||
String[] sliderTokens = tokens[5].split("\\|");
|
||||
this.sliderType = sliderTokens[0].charAt(0);
|
||||
this.sliderX = new float[sliderTokens.length - 1];
|
||||
this.sliderY = new float[sliderTokens.length - 1];
|
||||
for (int j = 1; j < sliderTokens.length; j++) {
|
||||
String[] sliderXY = sliderTokens[j].split(":");
|
||||
this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]);
|
||||
this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]);
|
||||
}
|
||||
this.repeat = Integer.parseInt(tokens[6]);
|
||||
this.pixelLength = Float.parseFloat(tokens[7]);
|
||||
if (tokens.length > 8) {
|
||||
String[] edgeHitSoundTokens = tokens[8].split("\\|");
|
||||
this.edgeHitSound = new byte[edgeHitSoundTokens.length];
|
||||
for (int j = 0; j < edgeHitSoundTokens.length; j++)
|
||||
edgeHitSound[j] = Byte.parseByte(edgeHitSoundTokens[j]);
|
||||
}
|
||||
if (tokens.length > 9) {
|
||||
String[] edgeAdditionTokens = tokens[9].split("\\|");
|
||||
this.edgeAddition = new byte[edgeAdditionTokens.length][2];
|
||||
for (int j = 0; j < edgeAdditionTokens.length; j++) {
|
||||
String[] tedgeAddition = edgeAdditionTokens[j].split(":");
|
||||
edgeAddition[j][0] = Byte.parseByte(tedgeAddition[0]);
|
||||
edgeAddition[j][1] = Byte.parseByte(tedgeAddition[1]);
|
||||
}
|
||||
}
|
||||
} else { //if ((type & OsuHitObject.TYPE_SPINNER) > 0) {
|
||||
additionIndex = 6;
|
||||
|
||||
// some 'endTime' fields contain a ':' character (?)
|
||||
int index = tokens[5].indexOf(':');
|
||||
if (index != -1)
|
||||
tokens[5] = tokens[5].substring(0, index);
|
||||
this.endTime = Integer.parseInt(tokens[5]);
|
||||
}
|
||||
|
||||
// addition
|
||||
if (tokens.length > additionIndex) {
|
||||
String[] additionTokens = tokens[additionIndex].split(":");
|
||||
this.addition = new byte[additionTokens.length];
|
||||
for (int j = 0; j < additionTokens.length; j++)
|
||||
this.addition[j] = Byte.parseByte(additionTokens[j]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw starting x coordinate.
|
||||
*/
|
||||
public float getX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the raw starting y coordinate.
|
||||
*/
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting x coordinate.
|
||||
*/
|
||||
public float getScaledX() { return (x - stack * stackOffset) * xMultiplier + xOffset; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting y coordinate.
|
||||
*/
|
||||
public float getScaledY() {
|
||||
if (GameMod.HARD_ROCK.isActive())
|
||||
return containerHeight - ((y - stack * stackOffset) * yMultiplier + yOffset);
|
||||
else
|
||||
return (y - stack * stackOffset) * yMultiplier + yOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start time.
|
||||
* @return the start time (in ms)
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the hit object type.
|
||||
* @return the object type (TYPE_* bitmask)
|
||||
*/
|
||||
public int getType() { return type; }
|
||||
|
||||
/**
|
||||
* Returns the name of the hit object type.
|
||||
*/
|
||||
public String getTypeName() {
|
||||
if (isCircle())
|
||||
return CIRCLE;
|
||||
else if (isSlider())
|
||||
return SLIDER;
|
||||
else if (isSpinner())
|
||||
return SPINNER;
|
||||
else
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hit sound type.
|
||||
* @return the sound type (SOUND_* bitmask)
|
||||
*/
|
||||
public byte getHitSoundType() { return hitSound; }
|
||||
|
||||
/**
|
||||
* Returns the edge hit sound type.
|
||||
* @param index the slider edge index (ignored for non-sliders)
|
||||
* @return the sound type (SOUND_* bitmask)
|
||||
*/
|
||||
public byte getEdgeHitSoundType(int index) {
|
||||
if (edgeHitSound != null)
|
||||
return edgeHitSound[index];
|
||||
else
|
||||
return hitSound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider type.
|
||||
* @return the slider type (SLIDER_* constant)
|
||||
*/
|
||||
public char getSliderType() { return sliderType; }
|
||||
|
||||
/**
|
||||
* Returns a list of raw slider x coordinates.
|
||||
*/
|
||||
public float[] getSliderX() { return sliderX; }
|
||||
|
||||
/**
|
||||
* Returns a list of raw slider y coordinates.
|
||||
*/
|
||||
public float[] getSliderY() { return sliderY; }
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider x coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderX() {
|
||||
if (sliderX == null)
|
||||
return null;
|
||||
|
||||
float[] x = new float[sliderX.length];
|
||||
for (int i = 0; i < x.length; i++)
|
||||
x[i] = (sliderX[i] - stack * stackOffset) * xMultiplier + xOffset;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider y coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderY() {
|
||||
if (sliderY == null)
|
||||
return null;
|
||||
|
||||
float[] y = new float[sliderY.length];
|
||||
if (GameMod.HARD_ROCK.isActive()) {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = containerHeight - ((sliderY[i] - stack * stackOffset) * yMultiplier + yOffset);
|
||||
} else {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = (sliderY[i] - stack * stackOffset) * yMultiplier + yOffset;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider repeat count.
|
||||
* @return the repeat count
|
||||
*/
|
||||
public int getRepeatCount() { return repeat; }
|
||||
|
||||
/**
|
||||
* Returns the slider pixel length.
|
||||
* @return the pixel length
|
||||
*/
|
||||
public float getPixelLength() { return pixelLength; }
|
||||
|
||||
/**
|
||||
* Returns the spinner end time.
|
||||
* @return the end time (in ms)
|
||||
*/
|
||||
public int getEndTime() { return endTime; }
|
||||
|
||||
/**
|
||||
* Sets the current index in the combo color array.
|
||||
* @param comboIndex the combo index
|
||||
*/
|
||||
public void setComboIndex(int comboIndex) { this.comboIndex = comboIndex; }
|
||||
|
||||
/**
|
||||
* Returns the current index in the combo color array.
|
||||
* @return the combo index
|
||||
*/
|
||||
public int getComboIndex() { return comboIndex; }
|
||||
|
||||
/**
|
||||
* Sets the number to display in the hit object.
|
||||
* @param comboNumber the combo number
|
||||
*/
|
||||
public void setComboNumber(int comboNumber) { this.comboNumber = comboNumber; }
|
||||
|
||||
/**
|
||||
* Returns the number to display in the hit object.
|
||||
* @return the combo number
|
||||
*/
|
||||
public int getComboNumber() { return comboNumber; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a circle.
|
||||
* @return true if circle
|
||||
*/
|
||||
public boolean isCircle() { return (type & TYPE_CIRCLE) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a slider.
|
||||
* @return true if slider
|
||||
*/
|
||||
public boolean isSlider() { return (type & TYPE_SLIDER) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a spinner.
|
||||
* @return true if spinner
|
||||
*/
|
||||
public boolean isSpinner() { return (type & TYPE_SPINNER) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object starts a new combo.
|
||||
* @return true if new combo
|
||||
*/
|
||||
public boolean isNewCombo() { return (type & TYPE_NEWCOMBO) > 0; }
|
||||
|
||||
/**
|
||||
* Returns the number of extra skips on the combo colors.
|
||||
*/
|
||||
public int getComboSkip() { return (type >> TYPE_NEWCOMBO); }
|
||||
|
||||
/**
|
||||
* Returns the sample set at the given index.
|
||||
* @param index the index (for sliders, ignored otherwise)
|
||||
* @return the sample set, or 0 if none available
|
||||
*/
|
||||
public byte getSampleSet(int index) {
|
||||
if (edgeAddition != null)
|
||||
return edgeAddition[index][0];
|
||||
if (addition != null)
|
||||
return addition[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'addition' sample set at the given index.
|
||||
* @param index the index (for sliders, ignored otherwise)
|
||||
* @return the sample set, or 0 if none available
|
||||
*/
|
||||
public byte getAdditionSampleSet(int index) {
|
||||
if (edgeAddition != null)
|
||||
return edgeAddition[index][1];
|
||||
if (addition != null)
|
||||
return addition[1];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hit object index in the current stack.
|
||||
* @param stack index in the stack
|
||||
*/
|
||||
public void setStack(int stack) { this.stack = stack; }
|
||||
|
||||
/**
|
||||
* Returns the hit object index in the current stack.
|
||||
* @return index in the stack
|
||||
*/
|
||||
public int getStack() { return stack; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
NumberFormat nf = new DecimalFormat("###.#####");
|
||||
|
||||
// common fields
|
||||
sb.append(nf.format(x)); sb.append(',');
|
||||
sb.append(nf.format(y)); sb.append(',');
|
||||
sb.append(time); sb.append(',');
|
||||
sb.append(type); sb.append(',');
|
||||
sb.append(hitSound); sb.append(',');
|
||||
|
||||
// type-specific fields
|
||||
if (isCircle())
|
||||
;
|
||||
else if (isSlider()) {
|
||||
sb.append(getSliderType());
|
||||
sb.append('|');
|
||||
for (int i = 0; i < sliderX.length; i++) {
|
||||
sb.append(nf.format(sliderX[i])); sb.append(':');
|
||||
sb.append(nf.format(sliderY[i])); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
sb.append(repeat); sb.append(',');
|
||||
sb.append(pixelLength); sb.append(',');
|
||||
if (edgeHitSound != null) {
|
||||
for (int i = 0; i < edgeHitSound.length; i++) {
|
||||
sb.append(edgeHitSound[i]); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
}
|
||||
if (edgeAddition != null) {
|
||||
for (int i = 0; i < edgeAddition.length; i++) {
|
||||
sb.append(edgeAddition[i][0]); sb.append(':');
|
||||
sb.append(edgeAddition[i][1]); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
}
|
||||
} else if (isSpinner()) {
|
||||
sb.append(endTime);
|
||||
sb.append(',');
|
||||
}
|
||||
|
||||
// addition
|
||||
if (addition != null) {
|
||||
for (int i = 0; i < addition.length; i++) {
|
||||
sb.append(addition[i]);
|
||||
sb.append(':');
|
||||
}
|
||||
} else
|
||||
sb.setLength(sb.length() - 1);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -1,742 +0,0 @@
|
|||
//TODO rename
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import itdelatrisu.opsu.db.OsuDB;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Parser for OSU files.
|
||||
*/
|
||||
public class OsuParser {
|
||||
/** The string lookup database. */
|
||||
private static HashMap<String, String> stringdb = new HashMap<String, String>();
|
||||
|
||||
/** The expected pattern for beatmap directories, used to find beatmap set IDs. */
|
||||
private static final String DIR_MSID_PATTERN = "^\\d+ .*";
|
||||
|
||||
/** The current file being parsed. */
|
||||
private static File currentFile;
|
||||
|
||||
/** The current directory number while parsing. */
|
||||
private static int currentDirectoryIndex = -1;
|
||||
|
||||
/** The total number of directories to parse. */
|
||||
private static int totalDirectories = -1;
|
||||
|
||||
/** Parser statuses. */
|
||||
public enum Status { NONE, PARSING, CACHE, INSERTING };
|
||||
|
||||
/** The current status. */
|
||||
private static Status status = Status.NONE;
|
||||
|
||||
// This class should not be instantiated.
|
||||
private OsuParser() {}
|
||||
|
||||
/**
|
||||
* Invokes parser for each OSU file in a root directory and
|
||||
* adds the OsuFiles to a new OsuGroupList.
|
||||
* @param root the root directory (search has depth 1)
|
||||
*/
|
||||
public static void parseAllFiles(File root) {
|
||||
// create a new OsuGroupList
|
||||
OsuGroupList.create();
|
||||
|
||||
// parse all directories
|
||||
parseDirectories(root.listFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes parser for each directory in the given array and
|
||||
* adds the OsuFiles to the existing OsuGroupList.
|
||||
* @param dirs the array of directories to parse
|
||||
* @return the last OsuGroupNode parsed, or null if none
|
||||
*/
|
||||
public static OsuGroupNode parseDirectories(File[] dirs) {
|
||||
if (dirs == null)
|
||||
return null;
|
||||
|
||||
// progress tracking
|
||||
status = Status.PARSING;
|
||||
currentDirectoryIndex = 0;
|
||||
totalDirectories = dirs.length;
|
||||
|
||||
// get last modified map from database
|
||||
Map<String, Long> map = OsuDB.getLastModifiedMap();
|
||||
|
||||
// OsuFile lists
|
||||
List<ArrayList<OsuFile>> allOsuFiles = new LinkedList<ArrayList<OsuFile>>();
|
||||
List<OsuFile> cachedOsuFiles = new LinkedList<OsuFile>(); // loaded from database
|
||||
List<OsuFile> parsedOsuFiles = new LinkedList<OsuFile>(); // loaded from parser
|
||||
|
||||
// parse directories
|
||||
OsuGroupNode lastNode = null;
|
||||
for (File dir : dirs) {
|
||||
currentDirectoryIndex++;
|
||||
if (!dir.isDirectory())
|
||||
continue;
|
||||
|
||||
// find all OSU files
|
||||
File[] files = dir.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return name.toLowerCase().endsWith(".osu");
|
||||
}
|
||||
});
|
||||
if (files.length < 1)
|
||||
continue;
|
||||
|
||||
// create a new group entry
|
||||
ArrayList<OsuFile> osuFiles = new ArrayList<OsuFile>();
|
||||
for (File file : files) {
|
||||
currentFile = file;
|
||||
|
||||
// check if beatmap is cached
|
||||
String path = String.format("%s/%s", dir.getName(), file.getName());
|
||||
if (map.containsKey(path)) {
|
||||
// check last modified times
|
||||
long lastModified = map.get(path);
|
||||
if (lastModified == file.lastModified()) {
|
||||
// add to cached beatmap list
|
||||
OsuFile osu = new OsuFile(file);
|
||||
osuFiles.add(osu);
|
||||
cachedOsuFiles.add(osu);
|
||||
continue;
|
||||
} else
|
||||
OsuDB.delete(dir.getName(), file.getName());
|
||||
}
|
||||
|
||||
// Parse hit objects only when needed to save time/memory.
|
||||
// Change boolean to 'true' to parse them immediately.
|
||||
OsuFile osu = parseFile(file, dir, osuFiles, false);
|
||||
|
||||
// add to parsed beatmap list
|
||||
if (osu != null) {
|
||||
osuFiles.add(osu);
|
||||
parsedOsuFiles.add(osu);
|
||||
}
|
||||
}
|
||||
|
||||
// add group entry if non-empty
|
||||
if (!osuFiles.isEmpty()) {
|
||||
osuFiles.trimToSize();
|
||||
allOsuFiles.add(osuFiles);
|
||||
}
|
||||
|
||||
// stop parsing files (interrupted)
|
||||
if (Thread.interrupted())
|
||||
break;
|
||||
}
|
||||
|
||||
// load cached entries from database
|
||||
if (!cachedOsuFiles.isEmpty()) {
|
||||
status = Status.CACHE;
|
||||
|
||||
// Load array fields only when needed to save time/memory.
|
||||
// Change flag to 'LOAD_ALL' to load them immediately.
|
||||
OsuDB.load(cachedOsuFiles, OsuDB.LOAD_NONARRAY);
|
||||
}
|
||||
|
||||
// add group entries to OsuGroupList
|
||||
for (ArrayList<OsuFile> osuFiles : allOsuFiles) {
|
||||
Collections.sort(osuFiles);
|
||||
lastNode = OsuGroupList.get().addSongGroup(osuFiles);
|
||||
}
|
||||
|
||||
// clear string DB
|
||||
stringdb = new HashMap<String, String>();
|
||||
|
||||
// add beatmap entries to database
|
||||
if (!parsedOsuFiles.isEmpty()) {
|
||||
status = Status.INSERTING;
|
||||
OsuDB.insert(parsedOsuFiles);
|
||||
}
|
||||
|
||||
status = Status.NONE;
|
||||
currentFile = null;
|
||||
currentDirectoryIndex = -1;
|
||||
totalDirectories = -1;
|
||||
return lastNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an OSU file.
|
||||
* @param file the file to parse
|
||||
* @param dir the directory containing the beatmap
|
||||
* @param osuFiles the song group
|
||||
* @param parseObjects if true, hit objects will be fully parsed now
|
||||
* @return the new OsuFile object
|
||||
*/
|
||||
private static OsuFile parseFile(File file, File dir, ArrayList<OsuFile> osuFiles, boolean parseObjects) {
|
||||
OsuFile osu = new OsuFile(file);
|
||||
osu.timingPoints = new ArrayList<OsuTimingPoint>();
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "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 "AudioFilename":
|
||||
File audioFileName = new File(dir, tokens[1]);
|
||||
if (!osuFiles.isEmpty()) {
|
||||
// if possible, reuse the same File object from another OsuFile in the group
|
||||
File groupAudioFileName = osuFiles.get(0).audioFilename;
|
||||
if (groupAudioFileName != null &&
|
||||
tokens[1].equalsIgnoreCase(groupAudioFileName.getName()))
|
||||
audioFileName = groupAudioFileName;
|
||||
}
|
||||
if (!audioFileName.isFile()) {
|
||||
// try to find the file with a case-insensitive match
|
||||
boolean match = false;
|
||||
for (String s : dir.list()) {
|
||||
if (s.equalsIgnoreCase(tokens[1])) {
|
||||
audioFileName = new File(dir, s);
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
Log.error(String.format("File not found: '%s'", tokens[1]));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
osu.audioFilename = audioFileName;
|
||||
break;
|
||||
case "AudioLeadIn":
|
||||
osu.audioLeadIn = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
// case "AudioHash": // deprecated
|
||||
// osu.audioHash = tokens[1];
|
||||
// break;
|
||||
case "PreviewTime":
|
||||
osu.previewTime = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "Countdown":
|
||||
osu.countdown = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "SampleSet":
|
||||
osu.sampleSet = getDBString(tokens[1]);
|
||||
break;
|
||||
case "StackLeniency":
|
||||
osu.stackLeniency = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "Mode":
|
||||
osu.mode = Byte.parseByte(tokens[1]);
|
||||
|
||||
/* Non-Opsu! standard files not implemented (obviously). */
|
||||
if (osu.mode != OsuFile.MODE_OSU)
|
||||
return null;
|
||||
|
||||
break;
|
||||
case "LetterboxInBreaks":
|
||||
osu.letterboxInBreaks = (Integer.parseInt(tokens[1]) == 1);
|
||||
break;
|
||||
case "WidescreenStoryboard":
|
||||
osu.widescreenStoryboard = (Integer.parseInt(tokens[1]) == 1);
|
||||
break;
|
||||
case "EpilepsyWarning":
|
||||
osu.epilepsyWarning = (Integer.parseInt(tokens[1]) == 1);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read line '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Editor]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Not implemented. */
|
||||
// if ((tokens = tokenize(line)) == null)
|
||||
// continue;
|
||||
// try {
|
||||
// switch (tokens[0]) {
|
||||
// case "Bookmarks":
|
||||
// String[] bookmarks = tokens[1].split(",");
|
||||
// osu.bookmarks = new int[bookmarks.length];
|
||||
// for (int i = 0; i < bookmarks.length; i++)
|
||||
// osu.bookmarks[i] = Integer.parseInt(bookmarks[i]);
|
||||
// break;
|
||||
// case "DistanceSpacing":
|
||||
// osu.distanceSpacing = Float.parseFloat(tokens[1]);
|
||||
// break;
|
||||
// case "BeatDivisor":
|
||||
// osu.beatDivisor = Byte.parseByte(tokens[1]);
|
||||
// break;
|
||||
// case "GridSize":
|
||||
// osu.gridSize = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// case "TimelineZoom":
|
||||
// osu.timelineZoom = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// Log.warn(String.format("Failed to read editor line '%s' for file '%s'.",
|
||||
// line, file.getAbsolutePath()), e);
|
||||
// }
|
||||
}
|
||||
break;
|
||||
case "[Metadata]":
|
||||
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 "Title":
|
||||
osu.title = getDBString(tokens[1]);
|
||||
break;
|
||||
case "TitleUnicode":
|
||||
osu.titleUnicode = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Artist":
|
||||
osu.artist = getDBString(tokens[1]);
|
||||
break;
|
||||
case "ArtistUnicode":
|
||||
osu.artistUnicode = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Creator":
|
||||
osu.creator = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Version":
|
||||
osu.version = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Source":
|
||||
osu.source = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Tags":
|
||||
osu.tags = getDBString(tokens[1].toLowerCase());
|
||||
break;
|
||||
case "BeatmapID":
|
||||
osu.beatmapID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "BeatmapSetID":
|
||||
osu.beatmapSetID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read metadata '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
if (osu.beatmapSetID <= 0) { // try to determine MSID from directory name
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
String dirName = dir.getName();
|
||||
if (!dirName.isEmpty() && dirName.matches(DIR_MSID_PATTERN))
|
||||
osu.beatmapSetID = Integer.parseInt(dirName.substring(0, dirName.indexOf(' ')));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Difficulty]":
|
||||
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 "HPDrainRate":
|
||||
osu.HPDrainRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "CircleSize":
|
||||
osu.circleSize = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "OverallDifficulty":
|
||||
osu.overallDifficulty = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "ApproachRate":
|
||||
osu.approachRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "SliderMultiplier":
|
||||
osu.sliderMultiplier = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "SliderTickRate":
|
||||
osu.sliderTickRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read difficulty '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
if (osu.approachRate == -1f) // not in old format
|
||||
osu.approachRate = osu.overallDifficulty;
|
||||
break;
|
||||
case "[Events]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = line.split(",");
|
||||
switch (tokens[0]) {
|
||||
case "0": // background
|
||||
tokens[2] = tokens[2].replaceAll("^\"|\"$", "");
|
||||
String ext = OsuParser.getExtension(tokens[2]);
|
||||
if (ext.equals("jpg") || ext.equals("png"))
|
||||
osu.bg = getDBString(tokens[2]);
|
||||
break;
|
||||
case "2": // break periods
|
||||
try {
|
||||
if (osu.breaks == null) // optional, create if needed
|
||||
osu.breaks = new ArrayList<Integer>();
|
||||
osu.breaks.add(Integer.parseInt(tokens[1]));
|
||||
osu.breaks.add(Integer.parseInt(tokens[2]));
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read break period '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* Not implemented. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (osu.breaks != null)
|
||||
osu.breaks.trimToSize();
|
||||
break;
|
||||
case "[TimingPoints]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
|
||||
try {
|
||||
// parse timing point
|
||||
OsuTimingPoint timingPoint = new OsuTimingPoint(line);
|
||||
|
||||
// calculate BPM
|
||||
if (!timingPoint.isInherited()) {
|
||||
int bpm = Math.round(60000 / timingPoint.getBeatLength());
|
||||
if (osu.bpmMin == 0)
|
||||
osu.bpmMin = osu.bpmMax = bpm;
|
||||
else if (bpm < osu.bpmMin)
|
||||
osu.bpmMin = bpm;
|
||||
else if (bpm > osu.bpmMax)
|
||||
osu.bpmMax = bpm;
|
||||
}
|
||||
|
||||
osu.timingPoints.add(timingPoint);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
osu.timingPoints.trimToSize();
|
||||
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 {
|
||||
switch (tokens[0]) {
|
||||
case "Combo1":
|
||||
case "Combo2":
|
||||
case "Combo3":
|
||||
case "Combo4":
|
||||
case "Combo5":
|
||||
case "Combo6":
|
||||
case "Combo7":
|
||||
case "Combo8":
|
||||
String[] rgb = tokens[1].split(",");
|
||||
colors.add(new Color(
|
||||
Integer.parseInt(rgb[0]),
|
||||
Integer.parseInt(rgb[1]),
|
||||
Integer.parseInt(rgb[2])
|
||||
));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read color '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
osu.combo = colors.toArray(new Color[colors.size()]);
|
||||
break;
|
||||
case "[HitObjects]":
|
||||
int type = 0;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Only type counts parsed at this time. */
|
||||
tokens = line.split(",");
|
||||
try {
|
||||
type = Integer.parseInt(tokens[3]);
|
||||
if ((type & OsuHitObject.TYPE_CIRCLE) > 0)
|
||||
osu.hitObjectCircle++;
|
||||
else if ((type & OsuHitObject.TYPE_SLIDER) > 0)
|
||||
osu.hitObjectSlider++;
|
||||
else //if ((type & OsuHitObject.TYPE_SPINNER) > 0)
|
||||
osu.hitObjectSpinner++;
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// map length = last object end time (TODO: end on slider?)
|
||||
if ((type & OsuHitObject.TYPE_SPINNER) > 0) {
|
||||
// some 'endTime' fields contain a ':' character (?)
|
||||
int index = tokens[5].indexOf(':');
|
||||
if (index != -1)
|
||||
tokens[5] = tokens[5].substring(0, index);
|
||||
osu.endTime = Integer.parseInt(tokens[5]);
|
||||
} else if (type != 0)
|
||||
osu.endTime = Integer.parseInt(tokens[2]);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object end time '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
line = in.readLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false);
|
||||
}
|
||||
|
||||
// no associated audio file?
|
||||
if (osu.audioFilename == null)
|
||||
return null;
|
||||
|
||||
// if no custom colors, use the default color scheme
|
||||
if (osu.combo == null)
|
||||
osu.combo = Utils.DEFAULT_COMBO;
|
||||
|
||||
// parse hit objects now?
|
||||
if (parseObjects)
|
||||
parseHitObjects(osu);
|
||||
|
||||
osu.md5Hash = Utils.getMD5(file);
|
||||
|
||||
return osu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all hit objects in an OSU file.
|
||||
* @param osu the OsuFile to parse
|
||||
*/
|
||||
public static void parseHitObjects(OsuFile osu) {
|
||||
if (osu.objects != null) // already parsed
|
||||
return;
|
||||
|
||||
osu.objects = new OsuHitObject[(osu.hitObjectCircle
|
||||
+ osu.hitObjectSlider + osu.hitObjectSpinner)];
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(osu.getFile()))) {
|
||||
String line = in.readLine();
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!line.equals("[HitObjects]"))
|
||||
line = in.readLine();
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (line == null) {
|
||||
Log.warn(String.format("No hit objects found in OsuFile '%s'.", osu.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// combo info
|
||||
int comboIndex = 0; // color index
|
||||
int comboNumber = 1; // combo number
|
||||
|
||||
int objectIndex = 0;
|
||||
boolean first = true;
|
||||
while ((line = in.readLine()) != null && objectIndex < osu.objects.length) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
|
||||
// lines must have at minimum 5 parameters
|
||||
int tokenCount = line.length() - line.replace(",", "").length();
|
||||
if (tokenCount < 4)
|
||||
continue;
|
||||
|
||||
try {
|
||||
// create a new OsuHitObject for each line
|
||||
OsuHitObject hitObject = new OsuHitObject(line);
|
||||
|
||||
// set combo info
|
||||
// - new combo: get next combo index, reset combo number
|
||||
// - else: maintain combo index, increase combo number
|
||||
if (hitObject.isNewCombo() || first) {
|
||||
int skip = (hitObject.isSpinner() ? 0 : 1) + hitObject.getComboSkip();
|
||||
for (int i = 0; i < skip; i++) {
|
||||
comboIndex = (comboIndex + 1) % osu.combo.length;
|
||||
comboNumber = 1;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
hitObject.setComboIndex(comboIndex);
|
||||
hitObject.setComboNumber(comboNumber++);
|
||||
|
||||
osu.objects[objectIndex++] = hitObject;
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object '%s' for OsuFile '%s'.",
|
||||
line, osu.toString()), e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error(String.format("Failed to read file '%s'.", osu.getFile().getAbsolutePath()), e, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension of a file.
|
||||
*/
|
||||
public static String getExtension(String file) {
|
||||
int i = file.lastIndexOf('.');
|
||||
return (i != -1) ? file.substring(i + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the current file being parsed, or null if none.
|
||||
*/
|
||||
public static String getCurrentFileName() {
|
||||
if (status == Status.PARSING)
|
||||
return (currentFile != null) ? currentFile.getName() : null;
|
||||
else
|
||||
return (status == Status.NONE) ? null : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the progress of file parsing, or -1 if not parsing.
|
||||
* @return the completion percent [0, 100] or -1
|
||||
*/
|
||||
public static int getParserProgress() {
|
||||
if (currentDirectoryIndex == -1 || totalDirectories == -1)
|
||||
return -1;
|
||||
|
||||
return currentDirectoryIndex * 100 / totalDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current parser status.
|
||||
*/
|
||||
public static Status getStatus() { return status; }
|
||||
|
||||
/**
|
||||
* Returns the String object in the database for the given String.
|
||||
* If none, insert the String into the database and return the original String.
|
||||
* @param s the string to retrieve
|
||||
* @return the string object
|
||||
*/
|
||||
public static String getDBString(String s) {
|
||||
String DBString = stringdb.get(s);
|
||||
if (DBString == null) {
|
||||
stringdb.put(s, s);
|
||||
return s;
|
||||
} else
|
||||
return DBString;
|
||||
}
|
||||
}
|
|
@ -1,722 +0,0 @@
|
|||
//TODO rename?
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.LWJGLException;
|
||||
import org.lwjgl.input.Cursor;
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
|
||||
/**
|
||||
* Class primarily used for drawing UI components.
|
||||
*/
|
||||
public class UI {
|
||||
/** Back button. */
|
||||
private static MenuButton backButton;
|
||||
|
||||
/** Empty cursor. */
|
||||
private static Cursor emptyCursor;
|
||||
|
||||
/** Last cursor coordinates. */
|
||||
private static int lastX = -1, lastY = -1;
|
||||
|
||||
/** Cursor rotation angle. */
|
||||
private static float cursorAngle = 0f;
|
||||
|
||||
/** Stores all previous cursor locations to display a trail. */
|
||||
private static LinkedList<Integer>
|
||||
cursorX = new LinkedList<Integer>(),
|
||||
cursorY = new LinkedList<Integer>();
|
||||
|
||||
/** Time to show volume image, in milliseconds. */
|
||||
private static final int VOLUME_DISPLAY_TIME = 1500;
|
||||
|
||||
/** Volume display elapsed time. */
|
||||
private static int volumeDisplay = -1;
|
||||
|
||||
/** The current bar notification string. */
|
||||
private static String barNotif;
|
||||
|
||||
/** The current bar notification timer. */
|
||||
private static int barNotifTimer = -1;
|
||||
|
||||
/** Duration, in milliseconds, to display bar notifications. */
|
||||
private static final int BAR_NOTIFICATION_TIME = 1250;
|
||||
|
||||
/** The current tooltip. */
|
||||
private static String tooltip;
|
||||
|
||||
/** Whether or not to check the current tooltip for line breaks. */
|
||||
private static boolean tooltipNewlines;
|
||||
|
||||
/** The current tooltip timer. */
|
||||
private static int tooltipTimer = -1;
|
||||
|
||||
/** Duration, in milliseconds, to fade tooltips. */
|
||||
private static final int TOOLTIP_FADE_TIME = 200;
|
||||
|
||||
// game-related variables
|
||||
private static GameContainer container;
|
||||
private static StateBasedGame game;
|
||||
private static Input input;
|
||||
|
||||
// This class should not be instantiated.
|
||||
private UI() {}
|
||||
|
||||
/**
|
||||
* Initializes UI data.
|
||||
* @param container the game container
|
||||
* @param game the game object
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
UI.container = container;
|
||||
UI.game = game;
|
||||
UI.input = container.getInput();
|
||||
|
||||
// hide native cursor
|
||||
try {
|
||||
int min = Cursor.getMinCursorSize();
|
||||
IntBuffer tmp = BufferUtils.createIntBuffer(min * min);
|
||||
emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null);
|
||||
hideCursor();
|
||||
} catch (LWJGLException e) {
|
||||
ErrorHandler.error("Failed to create hidden cursor.", e, true);
|
||||
}
|
||||
|
||||
// back button
|
||||
if (GameImage.MENU_BACK.getImages() != null) {
|
||||
Animation back = GameImage.MENU_BACK.getAnimation(120);
|
||||
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
|
||||
} else {
|
||||
Image back = GameImage.MENU_BACK.getImage();
|
||||
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
|
||||
}
|
||||
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all UI components by a delta interval.
|
||||
* @param delta the delta interval since the last call.
|
||||
*/
|
||||
public static void update(int delta) {
|
||||
updateCursor(delta);
|
||||
updateVolumeDisplay(delta);
|
||||
updateBarNotification(delta);
|
||||
if (tooltipTimer > 0)
|
||||
tooltipTimer -= delta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void draw(Graphics g) {
|
||||
drawBarNotification(g);
|
||||
drawVolume(g);
|
||||
drawFPS();
|
||||
drawCursor();
|
||||
drawTooltip(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
|
||||
* @param g the graphics context
|
||||
* @param mouseX the mouse x coordinate
|
||||
* @param mouseY the mouse y coordinate
|
||||
* @param mousePressed whether or not the mouse button is pressed
|
||||
*/
|
||||
public static void draw(Graphics g, int mouseX, int mouseY, boolean mousePressed) {
|
||||
drawBarNotification(g);
|
||||
drawVolume(g);
|
||||
drawFPS();
|
||||
drawCursor(mouseX, mouseY, mousePressed);
|
||||
drawTooltip(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the necessary UI components upon entering a state.
|
||||
*/
|
||||
public static void enter() {
|
||||
backButton.resetHover();
|
||||
resetBarNotification();
|
||||
resetCursorLocations();
|
||||
resetTooltip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'menu-back' MenuButton.
|
||||
*/
|
||||
public static MenuButton getBackButton() { return backButton; }
|
||||
|
||||
/**
|
||||
* Draws a tab image and text centered at a location.
|
||||
* @param x the center x coordinate
|
||||
* @param y the center y coordinate
|
||||
* @param text the text to draw inside the tab
|
||||
* @param selected whether the tab is selected (white) or not (red)
|
||||
* @param isHover whether to include a hover effect (unselected only)
|
||||
*/
|
||||
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
|
||||
Image tabImage = GameImage.MENU_TAB.getImage();
|
||||
float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
|
||||
float tabTextY = y - (tabImage.getHeight() / 2.5f);
|
||||
Color filter, textColor;
|
||||
if (selected) {
|
||||
filter = Color.white;
|
||||
textColor = Color.black;
|
||||
} else {
|
||||
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
|
||||
textColor = Color.white;
|
||||
}
|
||||
tabImage.drawCentered(x, y, filter);
|
||||
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the cursor.
|
||||
*/
|
||||
public static void drawCursor() {
|
||||
int state = game.getCurrentStateID();
|
||||
boolean mousePressed =
|
||||
(((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && Utils.isGameKeyPressed()) ||
|
||||
((input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) &&
|
||||
!(state == Opsu.STATE_GAME && Options.isMouseDisabled())));
|
||||
drawCursor(input.getMouseX(), input.getMouseY(), mousePressed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the cursor.
|
||||
* @param mouseX the mouse x coordinate
|
||||
* @param mouseY the mouse y coordinate
|
||||
* @param mousePressed whether or not the mouse button is pressed
|
||||
*/
|
||||
public static void drawCursor(int mouseX, int mouseY, boolean mousePressed) {
|
||||
// determine correct cursor image
|
||||
// TODO: most beatmaps don't skin CURSOR_MIDDLE, so how to determine style?
|
||||
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
||||
boolean skinned = GameImage.CURSOR.hasSkinImage();
|
||||
boolean newStyle = (skinned) ? true : Options.isNewCursorEnabled();
|
||||
if (skinned || newStyle) {
|
||||
cursor = GameImage.CURSOR.getImage();
|
||||
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
|
||||
} else {
|
||||
cursor = GameImage.CURSOR_OLD.getImage();
|
||||
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
|
||||
}
|
||||
if (newStyle)
|
||||
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
||||
|
||||
int removeCount = 0;
|
||||
int FPSmod = (Options.getTargetFPS() / 60);
|
||||
|
||||
// TODO: use an image buffer
|
||||
/*
|
||||
if (newStyle) {
|
||||
// new style: add all points between cursor movements
|
||||
if (lastX < 0) {
|
||||
lastX = mouseX;
|
||||
lastY = mouseY;
|
||||
return;
|
||||
}
|
||||
addCursorPoints(lastX, lastY, mouseX, mouseY);
|
||||
lastX = mouseX;
|
||||
lastY = mouseY;
|
||||
|
||||
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
|
||||
} else {
|
||||
// old style: sample one point at a time
|
||||
cursorX.add(mouseX);
|
||||
cursorY.add(mouseY);
|
||||
|
||||
int max = 10 * FPSmod;
|
||||
if (cursorX.size() > max)
|
||||
removeCount = cursorX.size() - max;
|
||||
}*/
|
||||
|
||||
// remove points from the lists
|
||||
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
|
||||
cursorX.remove();
|
||||
cursorY.remove();
|
||||
}
|
||||
|
||||
// draw a fading trail
|
||||
float alpha = 0f;
|
||||
float t = 2f / cursorX.size();
|
||||
Iterator<Integer> iterX = cursorX.iterator();
|
||||
Iterator<Integer> iterY = cursorY.iterator();
|
||||
while (iterX.hasNext()) {
|
||||
int cx = iterX.next();
|
||||
int cy = iterY.next();
|
||||
alpha += t;
|
||||
cursorTrail.setAlpha(alpha);
|
||||
// if (cx != x || cy != y)
|
||||
cursorTrail.drawCentered(cx, cy);
|
||||
}
|
||||
cursorTrail.drawCentered(mouseX, mouseY);
|
||||
|
||||
// increase the cursor size if pressed
|
||||
final float scale = 1.25f;
|
||||
if (mousePressed) {
|
||||
cursor = cursor.getScaledCopy(scale);
|
||||
if (newStyle)
|
||||
cursorMiddle = cursorMiddle.getScaledCopy(scale);
|
||||
}
|
||||
|
||||
// draw the other components
|
||||
if (newStyle)
|
||||
cursor.setRotation(cursorAngle);
|
||||
cursor.drawCentered(mouseX, mouseY);
|
||||
if (newStyle)
|
||||
cursorMiddle.drawCentered(mouseX, mouseY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all points between (x1, y1) and (x2, y2) to the cursor point lists.
|
||||
* @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java
|
||||
*/
|
||||
private static void addCursorPoints(int x1, int y1, int x2, int y2) {
|
||||
// delta of exact value and rounded value of the dependent variable
|
||||
int d = 0;
|
||||
int dy = Math.abs(y2 - y1);
|
||||
int dx = Math.abs(x2 - x1);
|
||||
|
||||
int dy2 = (dy << 1); // slope scaling factors to avoid floating
|
||||
int dx2 = (dx << 1); // point
|
||||
int ix = x1 < x2 ? 1 : -1; // increment direction
|
||||
int iy = y1 < y2 ? 1 : -1;
|
||||
|
||||
int k = 5; // sample size
|
||||
if (dy <= dx) {
|
||||
for (int i = 0; ; i++) {
|
||||
if (i == k) {
|
||||
cursorX.add(x1);
|
||||
cursorY.add(y1);
|
||||
i = 0;
|
||||
}
|
||||
if (x1 == x2)
|
||||
break;
|
||||
x1 += ix;
|
||||
d += dy2;
|
||||
if (d > dx) {
|
||||
y1 += iy;
|
||||
d -= dx2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; ; i++) {
|
||||
if (i == k) {
|
||||
cursorX.add(x1);
|
||||
cursorY.add(y1);
|
||||
i = 0;
|
||||
}
|
||||
if (y1 == y2)
|
||||
break;
|
||||
y1 += iy;
|
||||
d += dx2;
|
||||
if (d > dy) {
|
||||
x1 += ix;
|
||||
d -= dy2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the cursor by a degree determined by a delta interval.
|
||||
* If the old style cursor is being used, this will do nothing.
|
||||
* @param delta the delta interval since the last call
|
||||
*/
|
||||
private static void updateCursor(int delta) {
|
||||
cursorAngle += delta / 40f;
|
||||
cursorAngle %= 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all cursor data and skins.
|
||||
*/
|
||||
public static void resetCursor() {
|
||||
GameImage.CURSOR.destroySkinImage();
|
||||
GameImage.CURSOR_MIDDLE.destroySkinImage();
|
||||
GameImage.CURSOR_TRAIL.destroySkinImage();
|
||||
cursorAngle = 0f;
|
||||
GameImage.CURSOR.getImage().setRotation(0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all cursor location data.
|
||||
*/
|
||||
private static void resetCursorLocations() {
|
||||
lastX = lastY = -1;
|
||||
cursorX.clear();
|
||||
cursorY.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the cursor, if possible.
|
||||
*/
|
||||
public static void hideCursor() {
|
||||
if (emptyCursor != null) {
|
||||
try {
|
||||
container.setMouseCursor(emptyCursor, 0, 0);
|
||||
} catch (SlickException e) {
|
||||
ErrorHandler.error("Failed to hide the cursor.", e, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unhides the cursor.
|
||||
*/
|
||||
public static void showCursor() {
|
||||
container.setDefaultMouseCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the FPS at the bottom-right corner of the game container.
|
||||
* If the option is not activated, this will do nothing.
|
||||
*/
|
||||
public static void drawFPS() {
|
||||
if (!Options.isFPSCounterEnabled())
|
||||
return;
|
||||
|
||||
String fps = String.format("%dFPS", container.getFPS());
|
||||
Utils.FONT_BOLD.drawString(
|
||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
|
||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
|
||||
Integer.toString(container.getFPS()), Color.white
|
||||
);
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
|
||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
|
||||
"FPS", Color.white
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the volume bar on the middle right-hand side of the game container.
|
||||
* Only draws if the volume has recently been changed using with {@link #changeVolume(int)}.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void drawVolume(Graphics g) {
|
||||
if (volumeDisplay == -1)
|
||||
return;
|
||||
|
||||
int width = container.getWidth(), height = container.getHeight();
|
||||
Image img = GameImage.VOLUME.getImage();
|
||||
|
||||
// move image in/out
|
||||
float xOffset = 0;
|
||||
float ratio = (float) volumeDisplay / VOLUME_DISPLAY_TIME;
|
||||
if (ratio <= 0.1f)
|
||||
xOffset = img.getWidth() * (1 - (ratio * 10f));
|
||||
else if (ratio >= 0.9f)
|
||||
xOffset = img.getWidth() * (1 - ((1 - ratio) * 10f));
|
||||
|
||||
img.drawCentered(width - img.getWidth() / 2f + xOffset, height / 2f);
|
||||
float barHeight = img.getHeight() * 0.9f;
|
||||
float volume = Options.getMasterVolume();
|
||||
g.setColor(Color.white);
|
||||
g.fillRoundRect(
|
||||
width - (img.getWidth() * 0.368f) + xOffset,
|
||||
(height / 2f) - (img.getHeight() * 0.47f) + (barHeight * (1 - volume)),
|
||||
img.getWidth() * 0.15f, barHeight * volume, 3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates volume display by a delta interval.
|
||||
* @param delta the delta interval since the last call
|
||||
*/
|
||||
private static void updateVolumeDisplay(int delta) {
|
||||
if (volumeDisplay == -1)
|
||||
return;
|
||||
|
||||
volumeDisplay += delta;
|
||||
if (volumeDisplay > VOLUME_DISPLAY_TIME)
|
||||
volumeDisplay = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the master volume by a unit (positive or negative).
|
||||
* @param units the number of units
|
||||
*/
|
||||
public static void changeVolume(int units) {
|
||||
final float UNIT_OFFSET = 0.05f;
|
||||
Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
|
||||
if (volumeDisplay == -1)
|
||||
volumeDisplay = 0;
|
||||
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
|
||||
volumeDisplay = VOLUME_DISPLAY_TIME / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws loading progress (OSZ unpacking, OsuFile parsing, sound loading)
|
||||
* at the bottom of the screen.
|
||||
*/
|
||||
public static void drawLoadingProgress(Graphics g) {
|
||||
String text, file;
|
||||
int progress;
|
||||
|
||||
// determine current action
|
||||
if ((file = OszUnpacker.getCurrentFileName()) != null) {
|
||||
text = "Unpacking new beatmaps...";
|
||||
progress = OszUnpacker.getUnpackerProgress();
|
||||
} else if ((file = OsuParser.getCurrentFileName()) != null) {
|
||||
text = (OsuParser.getStatus() == OsuParser.Status.INSERTING) ?
|
||||
"Updating database..." : "Loading beatmaps...";
|
||||
progress = OsuParser.getParserProgress();
|
||||
} else if ((file = SoundController.getCurrentFileName()) != null) {
|
||||
text = "Loading sounds...";
|
||||
progress = SoundController.getLoadingProgress();
|
||||
} else
|
||||
return;
|
||||
|
||||
// draw loading info
|
||||
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
|
||||
float lineY = container.getHeight() - marginY;
|
||||
int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
|
||||
if (Options.isLoadVerbose()) {
|
||||
// verbose: display percentages and file names
|
||||
Utils.FONT_MEDIUM.drawString(
|
||||
marginX, lineY - (lineOffsetY * 2),
|
||||
String.format("%s (%d%%)", text, progress), Color.white);
|
||||
Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
|
||||
} else {
|
||||
// draw loading bar
|
||||
Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
|
||||
g.setColor(Color.white);
|
||||
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
|
||||
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a scroll bar.
|
||||
* @param g the graphics context
|
||||
* @param unitIndex the unit index
|
||||
* @param totalUnits the total number of units
|
||||
* @param maxShown the maximum number of units shown at one time
|
||||
* @param unitBaseX the base x coordinate of the units
|
||||
* @param unitBaseY the base y coordinate of the units
|
||||
* @param unitWidth the width of a unit
|
||||
* @param unitHeight the height of a unit
|
||||
* @param unitOffsetY the y offset between units
|
||||
* @param bgColor the scroll bar area background color (null if none)
|
||||
* @param scrollbarColor the scroll bar color
|
||||
* @param right whether or not to place the scroll bar on the right side of the unit
|
||||
*/
|
||||
public static void drawScrollbar(
|
||||
Graphics g, int unitIndex, int totalUnits, int maxShown,
|
||||
float unitBaseX, float unitBaseY, float unitWidth, float unitHeight, float unitOffsetY,
|
||||
Color bgColor, Color scrollbarColor, boolean right
|
||||
) {
|
||||
float scrollbarWidth = container.getWidth() * 0.00347f;
|
||||
float heightRatio = (float) (2.6701f * Math.exp(-0.81 * Math.log(totalUnits)));
|
||||
float scrollbarHeight = container.getHeight() * heightRatio;
|
||||
float scrollAreaHeight = unitHeight + unitOffsetY * (maxShown - 1);
|
||||
float offsetY = (scrollAreaHeight - scrollbarHeight) * ((float) unitIndex / (totalUnits - maxShown));
|
||||
float scrollbarX = unitBaseX + unitWidth - ((right) ? scrollbarWidth : 0);
|
||||
if (bgColor != null) {
|
||||
g.setColor(bgColor);
|
||||
g.fillRect(scrollbarX, unitBaseY, scrollbarWidth, scrollAreaHeight);
|
||||
}
|
||||
g.setColor(scrollbarColor);
|
||||
g.fillRect(scrollbarX, unitBaseY + offsetY, scrollbarWidth, scrollbarHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates a tooltip for drawing.
|
||||
* Must be called with {@link #drawTooltip(Graphics)}.
|
||||
* @param delta the delta interval since the last call
|
||||
* @param s the tooltip text
|
||||
* @param newlines whether to check for line breaks ('\n')
|
||||
*/
|
||||
public static void updateTooltip(int delta, String s, boolean newlines) {
|
||||
if (s != null) {
|
||||
tooltip = s;
|
||||
tooltipNewlines = newlines;
|
||||
if (tooltipTimer <= 0)
|
||||
tooltipTimer = delta;
|
||||
else
|
||||
tooltipTimer += delta * 2;
|
||||
if (tooltipTimer > TOOLTIP_FADE_TIME)
|
||||
tooltipTimer = TOOLTIP_FADE_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a tooltip, if any, near the current mouse coordinates,
|
||||
* bounded by the container dimensions.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void drawTooltip(Graphics g) {
|
||||
if (tooltipTimer <= 0 || tooltip == null)
|
||||
return;
|
||||
|
||||
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
|
||||
int margin = containerWidth / 100, textMarginX = 2;
|
||||
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
|
||||
int lineHeight = Utils.FONT_SMALL.getLineHeight();
|
||||
int textWidth = textMarginX * 2, textHeight = lineHeight;
|
||||
if (tooltipNewlines) {
|
||||
String[] lines = tooltip.split("\\n");
|
||||
int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
|
||||
for (int i = 1; i < lines.length; i++) {
|
||||
int w = Utils.FONT_SMALL.getWidth(lines[i]);
|
||||
if (w > maxWidth)
|
||||
maxWidth = w;
|
||||
}
|
||||
textWidth += maxWidth;
|
||||
textHeight += lineHeight * (lines.length - 1);
|
||||
} else
|
||||
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
|
||||
|
||||
// get drawing coordinates
|
||||
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
|
||||
if (x + textWidth > containerWidth - margin)
|
||||
x = containerWidth - margin - textWidth;
|
||||
else if (x < margin)
|
||||
x = margin;
|
||||
if (y + textHeight > containerHeight - margin)
|
||||
y = containerHeight - margin - textHeight;
|
||||
else if (y < margin)
|
||||
y = margin;
|
||||
|
||||
// draw tooltip text inside a filled rectangle
|
||||
float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
|
||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a = alpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
g.fillRect(x, y, textWidth, textHeight);
|
||||
oldAlpha = Utils.COLOR_DARK_GRAY.a;
|
||||
Utils.COLOR_DARK_GRAY.a = alpha;
|
||||
g.setColor(Utils.COLOR_DARK_GRAY);
|
||||
g.setLineWidth(1);
|
||||
g.drawRect(x, y, textWidth, textHeight);
|
||||
Utils.COLOR_DARK_GRAY.a = oldAlpha;
|
||||
oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
|
||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
||||
Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
|
||||
Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tooltip.
|
||||
*/
|
||||
public static void resetTooltip() {
|
||||
tooltipTimer = -1;
|
||||
tooltip = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a bar notification for drawing.
|
||||
* Must be called with {@link #drawBarNotification(Graphics)}.
|
||||
* @param s the notification string
|
||||
*/
|
||||
public static void sendBarNotification(String s) {
|
||||
if (s != null) {
|
||||
barNotif = s;
|
||||
barNotifTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bar notification by a delta interval.
|
||||
* @param delta the delta interval since the last call
|
||||
*/
|
||||
private static void updateBarNotification(int delta) {
|
||||
if (barNotifTimer > -1 && barNotifTimer < BAR_NOTIFICATION_TIME) {
|
||||
barNotifTimer += delta;
|
||||
if (barNotifTimer > BAR_NOTIFICATION_TIME)
|
||||
barNotifTimer = BAR_NOTIFICATION_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the bar notification.
|
||||
*/
|
||||
public static void resetBarNotification() {
|
||||
barNotifTimer = -1;
|
||||
barNotif = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the notification sent from {@link #sendBarNotification(String)}.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void drawBarNotification(Graphics g) {
|
||||
if (barNotifTimer <= 0 || barNotifTimer >= BAR_NOTIFICATION_TIME)
|
||||
return;
|
||||
|
||||
float alpha = 1f;
|
||||
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
|
||||
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
|
||||
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
|
||||
float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
|
||||
float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a *= alpha;
|
||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
|
||||
Utils.FONT_LARGE.drawString(
|
||||
midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
|
||||
midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
|
||||
barNotif, Utils.COLOR_WHITE_ALPHA);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
|
||||
Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog (used before exiting the game).
|
||||
* @param message the message to display
|
||||
* @return true if user selects "yes", false otherwise
|
||||
*/
|
||||
public static boolean showExitConfirmation(String message) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Exception e) {
|
||||
ErrorHandler.error("Could not set system look and feel for exit confirmation.", e, true);
|
||||
}
|
||||
int n = JOptionPane.showConfirmDialog(null, message, "Warning",
|
||||
JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
return (n != JOptionPane.YES_OPTION);
|
||||
}
|
||||
}
|
|
@ -185,6 +185,9 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||
/** Slider border color. If null, the skin value is used. */
|
||||
public Color sliderBorder;
|
||||
|
||||
/** md5 hash of this file */
|
||||
public String md5Hash;
|
||||
|
||||
/**
|
||||
* [HitObjects]
|
||||
*/
|
||||
|
|
|
@ -590,6 +590,8 @@ public class BeatmapParser {
|
|||
if (parseObjects)
|
||||
parseHitObjects(beatmap);
|
||||
|
||||
beatmap.md5Hash = Utils.getMD5(file);
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
@ -58,6 +59,10 @@ public class BeatmapSetList {
|
|||
/** Set of all beatmap set IDs for the parsed beatmaps. */
|
||||
private HashSet<Integer> MSIDdb;
|
||||
|
||||
/** Map of all hash to Beatmap . */
|
||||
public HashMap<String, Beatmap> beatmapHashesToFile;
|
||||
|
||||
|
||||
/** Index of current expanded node (-1 if no node is expanded). */
|
||||
private int expandedIndex;
|
||||
|
||||
|
@ -83,6 +88,7 @@ public class BeatmapSetList {
|
|||
private BeatmapSetList() {
|
||||
parsedNodes = new ArrayList<BeatmapSetNode>();
|
||||
MSIDdb = new HashSet<Integer>();
|
||||
beatmapHashesToFile = new HashMap<String, Beatmap>();
|
||||
reset();
|
||||
}
|
||||
|
||||
|
@ -117,6 +123,10 @@ public class BeatmapSetList {
|
|||
int msid = beatmaps.get(0).beatmapSetID;
|
||||
if (msid > 0)
|
||||
MSIDdb.add(msid);
|
||||
for(Beatmap f : beatmaps) {
|
||||
beatmapHashesToFile.put(f.md5Hash, f);
|
||||
}
|
||||
|
||||
|
||||
return node;
|
||||
}
|
||||
|
@ -501,4 +511,8 @@ public class BeatmapSetList {
|
|||
* @return true if id is in the list
|
||||
*/
|
||||
public boolean containsBeatmapSetID(int id) { return MSIDdb.contains(id); }
|
||||
|
||||
public Beatmap getFileFromBeatmapHash(String beatmapHash) {
|
||||
return beatmapHashesToFile.get(beatmapHash);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ public class BeatmapDB {
|
|||
* Current database version.
|
||||
* This value should be changed whenever the database format changes.
|
||||
*/
|
||||
private static final String DATABASE_VERSION = "2014-06-08";
|
||||
private static final String DATABASE_VERSION = "2015-06-11";
|
||||
|
||||
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
|
||||
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
|
||||
|
@ -96,7 +96,7 @@ public class BeatmapDB {
|
|||
insertStmt = connection.prepareStatement(
|
||||
"INSERT INTO beatmaps VALUES (" +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
|
@ -122,7 +122,8 @@ public class BeatmapDB {
|
|||
"bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
|
||||
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
||||
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
||||
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT" +
|
||||
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT," +
|
||||
"md5hash TEXT" +
|
||||
"); " +
|
||||
"CREATE TABLE IF NOT EXISTS info (" +
|
||||
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
||||
|
@ -340,6 +341,7 @@ public class BeatmapDB {
|
|||
stmt.setString(38, beatmap.timingPointsToString());
|
||||
stmt.setString(39, beatmap.breaksToString());
|
||||
stmt.setString(40, beatmap.comboToString());
|
||||
stmt.setString(41, beatmap.md5Hash);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
@ -481,6 +483,7 @@ public class BeatmapDB {
|
|||
if (bg != null)
|
||||
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
||||
beatmap.sliderBorderFromString(rs.getString(37));
|
||||
beatmap.md5Hash = BeatmapParser.getDBString(rs.getString(41));
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -1,584 +0,0 @@
|
|||
//todo rename
|
||||
|
||||
/*
|
||||
* 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.db;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuParser;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Handles connections and queries with the cached beatmap database.
|
||||
*/
|
||||
public class OsuDB {
|
||||
/**
|
||||
* Current database version.
|
||||
* This value should be changed whenever the database format changes.
|
||||
*/
|
||||
private static final String DATABASE_VERSION = "2015-03-30";
|
||||
|
||||
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
|
||||
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
|
||||
|
||||
/** Minimum batch size to invoke batch insertion. */
|
||||
private static final int INSERT_BATCH_MIN = 100;
|
||||
|
||||
/** OsuFile loading flags. */
|
||||
public static final int LOAD_NONARRAY = 1, LOAD_ARRAY = 2, LOAD_ALL = 3;
|
||||
|
||||
/** Database connection. */
|
||||
private static Connection connection;
|
||||
|
||||
/** Query statements. */
|
||||
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, updateSizeStmt;
|
||||
|
||||
/** Current size of beatmap cache table. */
|
||||
private static int cacheSize = -1;
|
||||
|
||||
// This class should not be instantiated.
|
||||
private OsuDB() {}
|
||||
|
||||
/**
|
||||
* Initializes the database connection.
|
||||
*/
|
||||
public static void init() {
|
||||
// create a database connection
|
||||
connection = DBController.createConnection(Options.OSU_DB.getPath());
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
// create the database
|
||||
createDatabase();
|
||||
|
||||
// prepare sql statements
|
||||
try {
|
||||
insertStmt = connection.prepareStatement(
|
||||
"INSERT INTO beatmaps VALUES (" +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
|
||||
updateSizeStmt = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('size', ?)");
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
|
||||
}
|
||||
|
||||
// retrieve the cache size
|
||||
getCacheSize();
|
||||
|
||||
// check the database version
|
||||
checkVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the database, if it does not exist.
|
||||
*/
|
||||
private static void createDatabase() {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
String sql =
|
||||
"CREATE TABLE IF NOT EXISTS beatmaps (" +
|
||||
"dir TEXT, file TEXT, lastModified INTEGER, " +
|
||||
"MID INTEGER, MSID INTEGER, " +
|
||||
"title TEXT, titleUnicode TEXT, artist TEXT, artistUnicode TEXT, " +
|
||||
"creator TEXT, version TEXT, source TEXT, tags TEXT, " +
|
||||
"circles INTEGER, sliders INTEGER, spinners INTEGER, " +
|
||||
"hp REAL, cs REAL, od REAL, ar REAL, sliderMultiplier REAL, sliderTickRate REAL, " +
|
||||
"bpmMin INTEGER, bpmMax INTEGER, endTime INTEGER, " +
|
||||
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
||||
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
||||
"bg TEXT, timingPoints TEXT, breaks TEXT, combo TEXT," +
|
||||
"md5hash TEXT" +
|
||||
"); " +
|
||||
"CREATE TABLE IF NOT EXISTS info (" +
|
||||
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
||||
"); " +
|
||||
"CREATE INDEX IF NOT EXISTS idx ON beatmaps (dir, file); " +
|
||||
|
||||
// extra optimizations
|
||||
"PRAGMA locking_mode = EXCLUSIVE; " +
|
||||
"PRAGMA journal_mode = WAL;";
|
||||
stmt.executeUpdate(sql);
|
||||
|
||||
// set the version key, if empty
|
||||
sql = String.format("INSERT OR IGNORE INTO info(key, value) VALUES('version', '%s')", DATABASE_VERSION);
|
||||
stmt.executeUpdate(sql);
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not create beatmap database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the stored table version, clears the beatmap database if different
|
||||
* from the current version, then updates the version field.
|
||||
*/
|
||||
private static void checkVersion() {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// get the stored version
|
||||
String sql = "SELECT value FROM info WHERE key = 'version'";
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
String version = (rs.next()) ? rs.getString(1) : "";
|
||||
rs.close();
|
||||
|
||||
// if different from current version, clear the database
|
||||
if (!version.equals(DATABASE_VERSION)) {
|
||||
clearDatabase();
|
||||
|
||||
// update version
|
||||
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
|
||||
ps.setString(1, DATABASE_VERSION);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Beatmap database version checks failed.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the size of the beatmap cache from the 'info' table.
|
||||
*/
|
||||
private static void getCacheSize() {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
String sql = "SELECT value FROM info WHERE key = 'size'";
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
try {
|
||||
cacheSize = (rs.next()) ? Integer.parseInt(rs.getString(1)) : 0;
|
||||
} catch (NumberFormatException e) {
|
||||
cacheSize = 0;
|
||||
}
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not get beatmap cache size.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the size of the beatmap cache in the 'info' table.
|
||||
*/
|
||||
private static void updateCacheSize() {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
updateSizeStmt.setString(1, Integer.toString(Math.max(cacheSize, 0)));
|
||||
updateSizeStmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not update beatmap cache size.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the database.
|
||||
*/
|
||||
public static void clearDatabase() {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
// drop the table, then recreate it
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
String sql = "DROP TABLE beatmaps";
|
||||
stmt.executeUpdate(sql);
|
||||
cacheSize = 0;
|
||||
updateCacheSize();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Could not drop beatmap database.", e, true);
|
||||
}
|
||||
createDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the OsuFile to the database.
|
||||
* @param osu the OsuFile object
|
||||
*/
|
||||
public static void insert(OsuFile osu) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
setStatementFields(insertStmt, osu);
|
||||
cacheSize += insertStmt.executeUpdate();
|
||||
updateCacheSize();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to add beatmap to database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the OsuFiles to the database in a batch.
|
||||
* @param batch a list of OsuFile objects
|
||||
*/
|
||||
public static void insert(List<OsuFile> batch) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// turn off auto-commit mode
|
||||
boolean autoCommit = connection.getAutoCommit();
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
// drop indexes
|
||||
boolean recreateIndexes = (batch.size() >= INSERT_BATCH_MIN);
|
||||
if (recreateIndexes) {
|
||||
String sql = "DROP INDEX IF EXISTS idx";
|
||||
stmt.executeUpdate(sql);
|
||||
}
|
||||
|
||||
// batch insert
|
||||
for (OsuFile osu : batch) {
|
||||
try {
|
||||
setStatementFields(insertStmt, osu);
|
||||
} catch (SQLException e) {
|
||||
Log.error(String.format("Failed to insert map '%s' into database.", osu.getFile().getPath()), e);
|
||||
continue;
|
||||
}
|
||||
insertStmt.addBatch();
|
||||
}
|
||||
int[] results = insertStmt.executeBatch();
|
||||
for (int i = 0; i < results.length; i++) {
|
||||
if (results[i] > 0)
|
||||
cacheSize += results[i];
|
||||
}
|
||||
|
||||
// re-create indexes
|
||||
if (recreateIndexes) {
|
||||
String sql = "CREATE INDEX idx ON beatmaps (dir, file)";
|
||||
stmt.executeUpdate(sql);
|
||||
}
|
||||
|
||||
// restore previous auto-commit mode
|
||||
connection.commit();
|
||||
connection.setAutoCommit(autoCommit);
|
||||
|
||||
// update cache size
|
||||
updateCacheSize();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to add beatmaps to database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all statement fields using a given OsuFile object.
|
||||
* @param stmt the statement to set fields for
|
||||
* @param osu the OsuFile
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static void setStatementFields(PreparedStatement stmt, OsuFile osu)
|
||||
throws SQLException {
|
||||
try {
|
||||
stmt.setString(1, osu.getFile().getParentFile().getName());
|
||||
stmt.setString(2, osu.getFile().getName());
|
||||
stmt.setLong(3, osu.getFile().lastModified());
|
||||
stmt.setInt(4, osu.beatmapID);
|
||||
stmt.setInt(5, osu.beatmapSetID);
|
||||
stmt.setString(6, osu.title);
|
||||
stmt.setString(7, osu.titleUnicode);
|
||||
stmt.setString(8, osu.artist);
|
||||
stmt.setString(9, osu.artistUnicode);
|
||||
stmt.setString(10, osu.creator);
|
||||
stmt.setString(11, osu.version);
|
||||
stmt.setString(12, osu.source);
|
||||
stmt.setString(13, osu.tags);
|
||||
stmt.setInt(14, osu.hitObjectCircle);
|
||||
stmt.setInt(15, osu.hitObjectSlider);
|
||||
stmt.setInt(16, osu.hitObjectSpinner);
|
||||
stmt.setFloat(17, osu.HPDrainRate);
|
||||
stmt.setFloat(18, osu.circleSize);
|
||||
stmt.setFloat(19, osu.overallDifficulty);
|
||||
stmt.setFloat(20, osu.approachRate);
|
||||
stmt.setFloat(21, osu.sliderMultiplier);
|
||||
stmt.setFloat(22, osu.sliderTickRate);
|
||||
stmt.setInt(23, osu.bpmMin);
|
||||
stmt.setInt(24, osu.bpmMax);
|
||||
stmt.setInt(25, osu.endTime);
|
||||
stmt.setString(26, osu.audioFilename.getName());
|
||||
stmt.setInt(27, osu.audioLeadIn);
|
||||
stmt.setInt(28, osu.previewTime);
|
||||
stmt.setByte(29, osu.countdown);
|
||||
stmt.setString(30, osu.sampleSet);
|
||||
stmt.setFloat(31, osu.stackLeniency);
|
||||
stmt.setByte(32, osu.mode);
|
||||
stmt.setBoolean(33, osu.letterboxInBreaks);
|
||||
stmt.setBoolean(34, osu.widescreenStoryboard);
|
||||
stmt.setBoolean(35, osu.epilepsyWarning);
|
||||
stmt.setString(36, osu.bg);
|
||||
stmt.setString(37, osu.timingPointsToString());
|
||||
stmt.setString(38, osu.breaksToString());
|
||||
stmt.setString(39, osu.comboToString());
|
||||
stmt.setString(40, osu.md5Hash);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads OsuFile fields from the database.
|
||||
* @param osu the OsuFile object
|
||||
* @param flag whether to load all fields (LOAD_ALL), non-array
|
||||
* fields (LOAD_NONARRAY), or array fields (LOAD_ARRAY)
|
||||
*/
|
||||
public static void load(OsuFile osu, int flag) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
selectStmt.setString(1, osu.getFile().getParentFile().getName());
|
||||
selectStmt.setString(2, osu.getFile().getName());
|
||||
ResultSet rs = selectStmt.executeQuery();
|
||||
if (rs.next()) {
|
||||
if ((flag & LOAD_NONARRAY) > 0)
|
||||
setOsuFileFields(rs, osu);
|
||||
if ((flag & LOAD_ARRAY) > 0)
|
||||
setOsuFileArrayFields(rs, osu);
|
||||
}
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to load OsuFile from database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads OsuFile fields from the database in a batch.
|
||||
* @param batch a list of OsuFile objects
|
||||
* @param flag whether to load all fields (LOAD_ALL), non-array
|
||||
* fields (LOAD_NONARRAY), or array fields (LOAD_ARRAY)
|
||||
*/
|
||||
public static void load(List<OsuFile> batch, int flag) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
// batch size too small
|
||||
int size = batch.size();
|
||||
if (size < cacheSize * LOAD_BATCH_MIN_RATIO) {
|
||||
for (OsuFile osu : batch)
|
||||
load(osu, flag);
|
||||
return;
|
||||
}
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// create map
|
||||
HashMap<String, HashMap<String, OsuFile>> map = new HashMap<String, HashMap<String, OsuFile>>();
|
||||
for (OsuFile osu : batch) {
|
||||
String parent = osu.getFile().getParentFile().getName();
|
||||
String name = osu.getFile().getName();
|
||||
HashMap<String, OsuFile> m = map.get(parent);
|
||||
if (m == null) {
|
||||
m = new HashMap<String, OsuFile>();
|
||||
map.put(parent, m);
|
||||
}
|
||||
m.put(name, osu);
|
||||
}
|
||||
|
||||
// iterate through database to load OsuFiles
|
||||
int count = 0;
|
||||
stmt.setFetchSize(100);
|
||||
String sql = "SELECT * FROM beatmaps";
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
while (rs.next()) {
|
||||
String parent = rs.getString(1);
|
||||
HashMap<String, OsuFile> m = map.get(parent);
|
||||
if (m != null) {
|
||||
String name = rs.getString(2);
|
||||
OsuFile osu = m.get(name);
|
||||
if (osu != null) {
|
||||
try {
|
||||
if ((flag & LOAD_NONARRAY) > 0)
|
||||
setOsuFileFields(rs, osu);
|
||||
if ((flag & LOAD_ARRAY) > 0)
|
||||
setOsuFileArrayFields(rs, osu);
|
||||
} catch (SQLException e) {
|
||||
Log.error(String.format("Failed to load map '%s/%s' from database.", parent, name), e);
|
||||
}
|
||||
if (++count >= size)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
rs.close();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to load OsuFiles from database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all OsuFile non-array fields using a given result set.
|
||||
* @param rs the result set containing the fields
|
||||
* @param osu the OsuFile
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static void setOsuFileFields(ResultSet rs, OsuFile osu) throws SQLException {
|
||||
try {
|
||||
osu.beatmapID = rs.getInt(4);
|
||||
osu.beatmapSetID = rs.getInt(5);
|
||||
osu.title = OsuParser.getDBString(rs.getString(6));
|
||||
osu.titleUnicode = OsuParser.getDBString(rs.getString(7));
|
||||
osu.artist = OsuParser.getDBString(rs.getString(8));
|
||||
osu.artistUnicode = OsuParser.getDBString(rs.getString(9));
|
||||
osu.creator = OsuParser.getDBString(rs.getString(10));
|
||||
osu.version = OsuParser.getDBString(rs.getString(11));
|
||||
osu.source = OsuParser.getDBString(rs.getString(12));
|
||||
osu.tags = OsuParser.getDBString(rs.getString(13));
|
||||
osu.hitObjectCircle = rs.getInt(14);
|
||||
osu.hitObjectSlider = rs.getInt(15);
|
||||
osu.hitObjectSpinner = rs.getInt(16);
|
||||
osu.HPDrainRate = rs.getFloat(17);
|
||||
osu.circleSize = rs.getFloat(18);
|
||||
osu.overallDifficulty = rs.getFloat(19);
|
||||
osu.approachRate = rs.getFloat(20);
|
||||
osu.sliderMultiplier = rs.getFloat(21);
|
||||
osu.sliderTickRate = rs.getFloat(22);
|
||||
osu.bpmMin = rs.getInt(23);
|
||||
osu.bpmMax = rs.getInt(24);
|
||||
osu.endTime = rs.getInt(25);
|
||||
osu.audioFilename = new File(osu.getFile().getParentFile(), OsuParser.getDBString(rs.getString(26)));
|
||||
osu.audioLeadIn = rs.getInt(27);
|
||||
osu.previewTime = rs.getInt(28);
|
||||
osu.countdown = rs.getByte(29);
|
||||
osu.sampleSet = OsuParser.getDBString(rs.getString(30));
|
||||
osu.stackLeniency = rs.getFloat(31);
|
||||
osu.mode = rs.getByte(32);
|
||||
osu.letterboxInBreaks = rs.getBoolean(33);
|
||||
osu.widescreenStoryboard = rs.getBoolean(34);
|
||||
osu.epilepsyWarning = rs.getBoolean(35);
|
||||
osu.bg = OsuParser.getDBString(rs.getString(36));
|
||||
osu.md5Hash = OsuParser.getDBString(rs.getString(40));
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all OsuFile array fields using a given result set.
|
||||
* @param rs the result set containing the fields
|
||||
* @param osu the OsuFile
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static void setOsuFileArrayFields(ResultSet rs, OsuFile osu) throws SQLException {
|
||||
try {
|
||||
osu.timingPointsFromString(rs.getString(37));
|
||||
osu.breaksFromString(rs.getString(38));
|
||||
osu.comboFromString(rs.getString(39));
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of file paths ({dir}/{file}) to last modified times, or
|
||||
* null if any error occurred.
|
||||
*/
|
||||
public static Map<String, Long> getLastModifiedMap() {
|
||||
if (connection == null)
|
||||
return null;
|
||||
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
Map<String, Long> map = new HashMap<String, Long>();
|
||||
String sql = "SELECT dir, file, lastModified FROM beatmaps";
|
||||
ResultSet rs = stmt.executeQuery(sql);
|
||||
stmt.setFetchSize(100);
|
||||
while (rs.next()) {
|
||||
String path = String.format("%s/%s", rs.getString(1), rs.getString(2));
|
||||
long lastModified = rs.getLong(3);
|
||||
map.put(path, lastModified);
|
||||
}
|
||||
rs.close();
|
||||
return map;
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to get last modified map from database.", e, true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the beatmap entry from the database.
|
||||
* @param dir the directory
|
||||
* @param file the file
|
||||
*/
|
||||
public static void delete(String dir, String file) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
deleteMapStmt.setString(1, dir);
|
||||
deleteMapStmt.setString(2, file);
|
||||
cacheSize -= deleteMapStmt.executeUpdate();
|
||||
updateCacheSize();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to delete beatmap entry from database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the beatmap group entry from the database.
|
||||
* @param dir the directory
|
||||
*/
|
||||
public static void delete(String dir) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
deleteGroupStmt.setString(1, dir);
|
||||
cacheSize -= deleteGroupStmt.executeUpdate();
|
||||
updateCacheSize();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to delete beatmap group entry from database.", e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection to the database.
|
||||
*/
|
||||
public static void closeConnection() {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
insertStmt.close();
|
||||
selectStmt.close();
|
||||
deleteMapStmt.close();
|
||||
deleteGroupStmt.close();
|
||||
updateSizeStmt.close();
|
||||
connection.close();
|
||||
connection = null;
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to close beatmap database.", e, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,7 +66,7 @@ public class Circle implements GameObject {
|
|||
*/
|
||||
public static void init(GameContainer container, float circleSize) {
|
||||
diameter = (108 - (circleSize * 8));
|
||||
diameter = (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
int diameterInt = (int)diameter;
|
||||
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||
|
|
|
@ -69,4 +69,9 @@ public interface GameObject {
|
|||
* Updates the position of the hit object.
|
||||
*/
|
||||
public void updatePosition();
|
||||
|
||||
/**
|
||||
* Resets the hit object so that it can be reused.
|
||||
*/
|
||||
public void reset();
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
//TODO rename
|
||||
|
||||
/*
|
||||
* 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.objects;
|
||||
|
||||
import org.newdawn.slick.Graphics;
|
||||
|
||||
/**
|
||||
* Hit object interface.
|
||||
*/
|
||||
public interface HitObject {
|
||||
/**
|
||||
* Draws the hit object to the graphics context.
|
||||
* @param g the graphics context
|
||||
* @param trackPosition the current track position
|
||||
*/
|
||||
public void draw(Graphics g, int trackPosition);
|
||||
|
||||
/**
|
||||
* Updates the hit object.
|
||||
* @param overlap true if the next object's start time has already passed
|
||||
* @param delta the delta interval since the last call
|
||||
* @param mouseX the x coordinate of the mouse
|
||||
* @param mouseY the y coordinate of the mouse
|
||||
* @param keyPressed whether or not a game key is currently pressed
|
||||
* @param trackPosition the track position
|
||||
* @return true if object ended
|
||||
*/
|
||||
public boolean update(boolean overlap, int delta, int mouseX, int mouseY, boolean keyPressed, int trackPosition);
|
||||
|
||||
/**
|
||||
* Processes a mouse click.
|
||||
* @param x the x coordinate of the mouse
|
||||
* @param y the y coordinate of the mouse
|
||||
* @param trackPosition the track position
|
||||
* @return true if a hit result was processed
|
||||
*/
|
||||
public boolean mousePressed(int x, int y, int trackPosition);
|
||||
|
||||
/**
|
||||
* Returns the coordinates of the hit object at a given track position.
|
||||
* @param trackPosition the track position
|
||||
* @return the [x,y] coordinates
|
||||
*/
|
||||
public float[] getPointAt(int trackPosition);
|
||||
|
||||
/**
|
||||
* Returns the end time of the hit object.
|
||||
* @return the end time, in milliseconds
|
||||
*/
|
||||
public int getEndTime();
|
||||
|
||||
/**
|
||||
* Updates the position of the hit object.
|
||||
*/
|
||||
public void updatePosition();
|
||||
|
||||
/**
|
||||
* Resets the hit object so that it can be reused.
|
||||
*/
|
||||
public void reset();
|
||||
}
|
|
@ -118,7 +118,7 @@ public class Slider implements GameObject {
|
|||
containerHeight = container.getHeight();
|
||||
|
||||
diameter = (108 - (circleSize * 8));
|
||||
diameter = (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
int diameterInt = (int)diameter;
|
||||
|
||||
followRadius = diameter / 2 * 3f;
|
||||
|
|
|
@ -21,9 +21,9 @@ package itdelatrisu.opsu.replay;
|
|||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.ScoreData;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.io.OsuReader;
|
||||
import itdelatrisu.opsu.io.OsuWriter;
|
||||
|
||||
|
@ -150,7 +150,7 @@ public class Replay {
|
|||
* @param osu the OsuFile
|
||||
* @return the ScoreData object
|
||||
*/
|
||||
public ScoreData getScoreData(OsuFile osu) {
|
||||
public ScoreData getScoreData(Beatmap osu) {
|
||||
if (scoreData != null)
|
||||
return scoreData;
|
||||
|
||||
|
@ -186,7 +186,6 @@ public class Replay {
|
|||
private void loadHeader(OsuReader reader) throws IOException {
|
||||
this.mode = reader.readByte();
|
||||
this.version = reader.readInt();
|
||||
//System.out.println("Header:"+file.getName()+" "+mode+" "+version);
|
||||
this.beatmapHash = reader.readString();
|
||||
this.playerName = reader.readString();
|
||||
this.replayHash = reader.readString();
|
||||
|
@ -208,7 +207,6 @@ public class Replay {
|
|||
* @throws IOException
|
||||
*/
|
||||
private void loadData(OsuReader reader) throws IOException {
|
||||
//System.out.println("Load Data");
|
||||
// life data
|
||||
String[] lifeData = reader.readString().split(",");
|
||||
List<LifeFrame> lifeFrameList = new ArrayList<LifeFrame>(lifeData.length);
|
||||
|
|
|
@ -2,9 +2,9 @@ package itdelatrisu.opsu.replay;
|
|||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OsuFile;
|
||||
import itdelatrisu.opsu.OsuGroupList;
|
||||
import itdelatrisu.opsu.ScoreData;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.db.ScoreDB;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -16,7 +16,7 @@ public class ReplayImporter {
|
|||
try {
|
||||
Replay r = new Replay(replayToImport);
|
||||
r.loadHeader();
|
||||
OsuFile oFile = OsuGroupList.get().getFileFromBeatmapHash(r.beatmapHash);
|
||||
Beatmap oFile = BeatmapSetList.get().getFileFromBeatmapHash(r.beatmapHash);
|
||||
if(oFile != null){
|
||||
File replaydir = Options.getReplayDir();
|
||||
if (!replaydir.isDirectory()) {
|
||||
|
|
|
@ -618,21 +618,21 @@ public class Game extends BasicGameState {
|
|||
if(replayIndex-1 >= 1 && replayIndex < replay.frames.length && trackPosition < replay.frames[replayIndex-1].getTime()){
|
||||
replayIndex = 0;
|
||||
while(objectIndex>=0){
|
||||
hitObjects[objectIndex].reset();
|
||||
gameObjects[objectIndex].reset();
|
||||
objectIndex--;
|
||||
|
||||
}
|
||||
// load the first timingPoint
|
||||
if (!osu.timingPoints.isEmpty()) {
|
||||
OsuTimingPoint timingPoint = osu.timingPoints.get(0);
|
||||
// reset game data
|
||||
resetGameData();
|
||||
|
||||
// load the first timingPoint for stacking
|
||||
if (!beatmap.timingPoints.isEmpty()) {
|
||||
TimingPoint timingPoint = beatmap.timingPoints.get(0);
|
||||
if (!timingPoint.isInherited()) {
|
||||
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
||||
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
|
||||
SoundController.setSampleVolume(timingPoint.getSampleVolume());
|
||||
setBeatLength(timingPoint, true);
|
||||
timingPointIndex++;
|
||||
}
|
||||
}
|
||||
resetGameData();
|
||||
}
|
||||
|
||||
// update and run replay frames
|
||||
|
@ -777,8 +777,8 @@ public class Game extends BasicGameState {
|
|||
boolean keyPressed = keys != ReplayFrame.KEY_NONE;
|
||||
while (objectIndex < gameObjects.length && trackPosition > beatmap.objects[objectIndex].getTime()) {
|
||||
// check if we've already passed the next object's start time
|
||||
boolean overlap = (objectIndex + 1 < hitObjects.length &&
|
||||
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]);
|
||||
boolean overlap = (objectIndex + 1 < gameObjects.length &&
|
||||
trackPosition > beatmap.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_50]);
|
||||
|
||||
// update hit object and check completion status
|
||||
if (gameObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
|
||||
|
@ -915,8 +915,15 @@ public class Game extends BasicGameState {
|
|||
// skip button
|
||||
if (skipButton.contains(x, y))
|
||||
skipIntro();
|
||||
|
||||
// playback speed button
|
||||
else if (playbackSpeed.getButton().contains(x, y)) {
|
||||
playbackSpeed = playbackSpeed.next();
|
||||
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
||||
}
|
||||
|
||||
if(y < 50){
|
||||
float pos = (float)x / width * osu.endTime;
|
||||
float pos = (float)x / width * beatmap.endTime;
|
||||
System.out.println("Seek to"+pos);
|
||||
MusicController.setPosition((int)pos);
|
||||
}
|
||||
|
@ -1067,6 +1074,7 @@ public class Game extends BasicGameState {
|
|||
} else if (restart == Restart.REPLAY)
|
||||
retries = 0;
|
||||
|
||||
gameObjects = new GameObject[beatmap.objects.length];
|
||||
// reset game data
|
||||
resetGameData();
|
||||
|
||||
|
@ -1079,14 +1087,6 @@ public class Game extends BasicGameState {
|
|||
}
|
||||
}
|
||||
|
||||
if (!osu.timingPoints.isEmpty()) {
|
||||
OsuTimingPoint timingPoint = osu.timingPoints.get(0);
|
||||
if (!timingPoint.isInherited()) {
|
||||
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
||||
}
|
||||
}
|
||||
hitObjects = new HitObject[osu.objects.length];
|
||||
|
||||
// initialize object maps
|
||||
Color[] combo = beatmap.getComboColors();
|
||||
for (int i = 0; i < beatmap.objects.length; i++) {
|
||||
|
@ -1094,7 +1094,7 @@ public class Game extends BasicGameState {
|
|||
|
||||
// is this the last note in the combo?
|
||||
boolean comboEnd = false;
|
||||
if (i + 1 >= osu.objects.length || osu.objects[i + 1].isNewCombo())
|
||||
if (i + 1 < beatmap.objects.length && beatmap.objects[i + 1].isNewCombo())
|
||||
comboEnd = true;
|
||||
|
||||
Color color = combo[hitObject.getComboIndex()];
|
||||
|
@ -1145,7 +1145,6 @@ public class Game extends BasicGameState {
|
|||
|
||||
// load replay frames
|
||||
if (isReplay) {
|
||||
//System.out.println(replay.toString());
|
||||
// load mods
|
||||
previousMods = GameMod.getModState();
|
||||
GameMod.loadModState(replay.mods);
|
||||
|
@ -1310,9 +1309,6 @@ public class Game extends BasicGameState {
|
|||
* Resets all game data and structures.
|
||||
*/
|
||||
public void resetGameData() {
|
||||
//conflict
|
||||
gameObjects = new GameObject[beatmap.objects.length];
|
||||
//
|
||||
data.clear();
|
||||
objectIndex = 0;
|
||||
breakIndex = 0;
|
||||
|
|
Loading…
Reference in New Issue
Block a user