Major refactoring - now using far more logical class names.
- Renamed "OsuFile" to "Beatmap". All related variables and methods with "osu" have also been renamed to "beatmap" (or variants of each). - Renamed "OsuGroupNode" to "BeatmapSetNode". Avoids confusion since groups are identified by a "set ID", not a "group ID". - Renamed "OsuGroupList" to "BeatmapSetList", for the same reason as above. - Renamed "OsuDB" to "BeatmapDB", for the same reason as above. - Moved classes directly related to parsed beatmaps (Beatmap, BeatmapSetList, BeatmapSetNode, OsuHitObject, and TimingPoint) into a new "beatmap" package. Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
439
src/itdelatrisu/opsu/beatmap/Beatmap.java
Normal file
439
src/itdelatrisu/opsu/beatmap/Beatmap.java
Normal file
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Beatmap structure storing data parsed from OSU files.
|
||||
*/
|
||||
public class Beatmap implements Comparable<Beatmap> {
|
||||
/** 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<Beatmap, Image> bgImageMap = new HashMap<Beatmap, 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 beatmap. */
|
||||
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<TimingPoint> timingPoints;
|
||||
|
||||
/** Song BPM range. */
|
||||
public int bpmMin = 0, bpmMax = 0;
|
||||
|
||||
/**
|
||||
* [Colours]
|
||||
*/
|
||||
|
||||
/** Combo colors (max 8). */
|
||||
public Color[] combo;
|
||||
|
||||
/**
|
||||
* [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<Beatmap, Image>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file associated with this beatmap
|
||||
*/
|
||||
public Beatmap(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 beatmap background.
|
||||
* @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 Beatmap objects first by overall difficulty, then by total objects.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Beatmap 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 (TimingPoint 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<TimingPoint>();
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
String[] tokens = s.split("\\|");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
try {
|
||||
timingPoints.add(new TimingPoint(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()]);
|
||||
}
|
||||
}
|
||||
504
src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
Normal file
504
src/itdelatrisu/opsu/beatmap/BeatmapSetList.java
Normal file
@@ -0,0 +1,504 @@
|
||||
/*
|
||||
* 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.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.SongSort;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.db.BeatmapDB;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
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 BeatmapSetList {
|
||||
/** Song group structure (each group contains a list of beatmaps). */
|
||||
private static BeatmapSetList 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<BeatmapSetNode> parsedNodes;
|
||||
|
||||
/** Total number of beatmaps (i.e. Beatmap objects). */
|
||||
private int mapCount = 0;
|
||||
|
||||
/** Current list of nodes (subset of parsedNodes, used for searches). */
|
||||
private ArrayList<BeatmapSetNode> nodes;
|
||||
|
||||
/** Set of all beatmap set IDs for the parsed beatmaps. */
|
||||
private HashSet<Integer> MSIDdb;
|
||||
|
||||
/** Index of current expanded node (-1 if no node is expanded). */
|
||||
private int expandedIndex;
|
||||
|
||||
/** Start and end nodes of expanded group. */
|
||||
private BeatmapSetNode 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 BeatmapSetList(); }
|
||||
|
||||
/**
|
||||
* Returns the single instance of this class.
|
||||
*/
|
||||
public static BeatmapSetList get() { return list; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private BeatmapSetList() {
|
||||
parsedNodes = new ArrayList<BeatmapSetNode>();
|
||||
MSIDdb = new HashSet<Integer>();
|
||||
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 beatmaps the list of beatmaps in the group
|
||||
* @return the new BeatmapSetNode
|
||||
*/
|
||||
public BeatmapSetNode addSongGroup(ArrayList<Beatmap> beatmaps) {
|
||||
BeatmapSetNode node = new BeatmapSetNode(beatmaps);
|
||||
parsedNodes.add(node);
|
||||
mapCount += beatmaps.size();
|
||||
|
||||
// add beatmap set ID to set
|
||||
int msid = beatmaps.get(0).beatmapSetID;
|
||||
if (msid > 0)
|
||||
MSIDdb.add(msid);
|
||||
|
||||
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(BeatmapSetNode node) {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
// re-link base nodes
|
||||
int index = node.index;
|
||||
BeatmapSetNode 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
|
||||
Beatmap beatmap = node.beatmaps.get(0);
|
||||
nodes.remove(index);
|
||||
parsedNodes.remove(eCur);
|
||||
mapCount -= node.beatmaps.size();
|
||||
if (beatmap.beatmapSetID > 0)
|
||||
MSIDdb.remove(beatmap.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--;
|
||||
BeatmapSetNode expandedNode = expandedStartNode;
|
||||
for (int i = 0, size = expandedNode.beatmaps.size();
|
||||
i < size && expandedNode != null;
|
||||
i++, expandedNode = expandedNode.next)
|
||||
expandedNode.index = expandedIndex;
|
||||
}
|
||||
|
||||
// stop playing the track
|
||||
File dir = beatmap.getFile().getParentFile();
|
||||
if (MusicController.trackExists() || MusicController.isTrackLoading()) {
|
||||
File audioFile = MusicController.getBeatmap().audioFilename;
|
||||
if (audioFile != null && audioFile.equals(beatmap.audioFilename)) {
|
||||
MusicController.reset();
|
||||
System.gc(); // TODO: why can't files be deleted without calling this?
|
||||
}
|
||||
}
|
||||
|
||||
// remove entry from cache
|
||||
BeatmapDB.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(BeatmapSetNode)
|
||||
*/
|
||||
public boolean deleteSong(BeatmapSetNode node) {
|
||||
if (node == null || node.beatmapIndex == -1 || node.index != expandedIndex)
|
||||
return false;
|
||||
|
||||
// last song in group?
|
||||
int size = node.beatmaps.size();
|
||||
if (node.beatmaps.size() == 1)
|
||||
return deleteSongGroup(node);
|
||||
|
||||
// reset indices
|
||||
BeatmapSetNode expandedNode = node.next;
|
||||
for (int i = node.beatmapIndex + 1;
|
||||
i < size && expandedNode != null && expandedNode.index == node.index;
|
||||
i++, expandedNode = expandedNode.next)
|
||||
expandedNode.beatmapIndex--;
|
||||
|
||||
// remove song reference
|
||||
Beatmap beatmap = node.beatmaps.remove(node.beatmapIndex);
|
||||
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 = beatmap.getFile();
|
||||
BeatmapDB.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. Beatmap objects).
|
||||
*/
|
||||
public int getMapCount() { return mapCount; }
|
||||
|
||||
/**
|
||||
* Returns the total number of parsed maps sets.
|
||||
*/
|
||||
public int getMapSetCount() { return parsedNodes.size(); }
|
||||
|
||||
/**
|
||||
* Returns the BeatmapSetNode at an index, disregarding expansions.
|
||||
* @param index the node index
|
||||
*/
|
||||
public BeatmapSetNode getBaseNode(int index) {
|
||||
if (index < 0 || index >= size())
|
||||
return null;
|
||||
|
||||
return nodes.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random base node.
|
||||
*/
|
||||
public BeatmapSetNode getRandomNode() {
|
||||
BeatmapSetNode 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 BeatmapSetNode 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 BeatmapSetNode getNode(BeatmapSetNode node, int shift) {
|
||||
BeatmapSetNode 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 Beatmap
|
||||
* in that node and hiding the group node.
|
||||
* @return the first of the newly-inserted nodes
|
||||
*/
|
||||
public BeatmapSetNode expand(int index) {
|
||||
// undo the previous expansion
|
||||
unexpand();
|
||||
|
||||
BeatmapSetNode node = getBaseNode(index);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
expandedStartNode = expandedEndNode = null;
|
||||
|
||||
// create new nodes
|
||||
ArrayList<Beatmap> beatmaps = node.beatmaps;
|
||||
BeatmapSetNode prevNode = node.prev;
|
||||
BeatmapSetNode nextNode = node.next;
|
||||
for (int i = 0, size = node.beatmaps.size(); i < size; i++) {
|
||||
BeatmapSetNode newNode = new BeatmapSetNode(beatmaps);
|
||||
newNode.index = index;
|
||||
newNode.beatmapIndex = 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
|
||||
BeatmapSetNode
|
||||
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
|
||||
BeatmapSetNode lastNode = nodes.get(0);
|
||||
lastNode.index = 0;
|
||||
lastNode.prev = null;
|
||||
for (int i = 1, size = size(); i < size; i++) {
|
||||
BeatmapSetNode 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<BeatmapSetNode>();
|
||||
if (terms.isEmpty()) {
|
||||
// conditional term
|
||||
String type = condType.remove();
|
||||
String operator = condOperator.remove();
|
||||
float value = condValue.remove();
|
||||
for (BeatmapSetNode node : parsedNodes) {
|
||||
if (node.matches(type, operator, value))
|
||||
nodes.add(node);
|
||||
}
|
||||
} else {
|
||||
// normal term
|
||||
String term = terms.remove();
|
||||
for (BeatmapSetNode 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<BeatmapSetNode> nodeIter = nodes.iterator();
|
||||
while (nodeIter.hasNext()) {
|
||||
BeatmapSetNode 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<BeatmapSetNode> nodeIter = nodes.iterator();
|
||||
while (nodeIter.hasNext()) {
|
||||
BeatmapSetNode 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); }
|
||||
}
|
||||
231
src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
Normal file
231
src/itdelatrisu/opsu/beatmap/BeatmapSetNode.java
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.GameData.Grade;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
|
||||
/**
|
||||
* Node in an BeatmapSetList representing a group of beatmaps.
|
||||
*/
|
||||
public class BeatmapSetNode {
|
||||
/** List of associated beatmaps. */
|
||||
public ArrayList<Beatmap> beatmaps;
|
||||
|
||||
/** Index of this node. */
|
||||
public int index = 0;
|
||||
|
||||
/** Index of the selected beatmap (-1 if not focused). */
|
||||
public int beatmapIndex = -1;
|
||||
|
||||
/** Links to other nodes. */
|
||||
public BeatmapSetNode prev, next;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param beatmaps the beatmaps in this group
|
||||
*/
|
||||
public BeatmapSetNode(ArrayList<Beatmap> beatmaps) {
|
||||
this.beatmaps = beatmaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the button.
|
||||
* @param x the x coordinate
|
||||
* @param y the y coordinate
|
||||
* @param grade the highest grade, if any
|
||||
* @param focus true if this is the focused node
|
||||
*/
|
||||
public void draw(float x, float y, Grade grade, boolean focus) {
|
||||
Image bg = GameImage.MENU_BUTTON_BG.getImage();
|
||||
boolean expanded = (beatmapIndex > -1);
|
||||
Beatmap beatmap;
|
||||
bg.setAlpha(0.9f);
|
||||
Color bgColor;
|
||||
Color textColor = Color.lightGray;
|
||||
|
||||
// get drawing parameters
|
||||
if (expanded) {
|
||||
x -= bg.getWidth() / 10f;
|
||||
if (focus) {
|
||||
bgColor = Color.white;
|
||||
textColor = Color.white;
|
||||
} else
|
||||
bgColor = Utils.COLOR_BLUE_BUTTON;
|
||||
beatmap = beatmaps.get(beatmapIndex);
|
||||
} else {
|
||||
bgColor = Utils.COLOR_ORANGE_BUTTON;
|
||||
beatmap = beatmaps.get(0);
|
||||
}
|
||||
bg.draw(x, y, bgColor);
|
||||
|
||||
float cx = x + (bg.getWidth() * 0.043f);
|
||||
float cy = y + (bg.getHeight() * 0.2f) - 3;
|
||||
|
||||
// draw grade
|
||||
if (grade != Grade.NULL) {
|
||||
Image gradeImg = grade.getMenuImage();
|
||||
gradeImg.drawCentered(cx - bg.getWidth() * 0.01f + gradeImg.getWidth() / 2f, y + bg.getHeight() / 2.2f);
|
||||
cx += gradeImg.getWidth();
|
||||
}
|
||||
|
||||
// draw text
|
||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
|
||||
Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
|
||||
}
|
||||
Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
|
||||
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
|
||||
String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
|
||||
if (expanded || beatmaps.size() == 1)
|
||||
Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 4,
|
||||
beatmap.version, textColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of strings containing song information.
|
||||
* <ul>
|
||||
* <li>0: {Artist} - {Title} [{Version}]
|
||||
* <li>1: Mapped by {Creator}
|
||||
* <li>2: Length: {} BPM: {} Objects: {}
|
||||
* <li>3: Circles: {} Sliders: {} Spinners: {}
|
||||
* <li>4: CS:{} HP:{} AR:{} OD:{}
|
||||
* </ul>
|
||||
*/
|
||||
public String[] getInfo() {
|
||||
if (beatmapIndex < 0)
|
||||
return null;
|
||||
|
||||
Beatmap beatmap = beatmaps.get(beatmapIndex);
|
||||
float speedModifier = GameMod.getSpeedMultiplier();
|
||||
long endTime = (long) (beatmap.endTime / speedModifier);
|
||||
int bpmMin = (int) (beatmap.bpmMin * speedModifier);
|
||||
int bpmMax = (int) (beatmap.bpmMax * speedModifier);
|
||||
float multiplier = GameMod.getDifficultyMultiplier();
|
||||
String[] info = new String[5];
|
||||
info[0] = beatmap.toString();
|
||||
info[1] = String.format("Mapped by %s", beatmap.creator);
|
||||
info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(endTime),
|
||||
TimeUnit.MILLISECONDS.toSeconds(endTime) -
|
||||
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(endTime)),
|
||||
(bpmMax <= 0) ? "--" : ((bpmMin == bpmMax) ? bpmMin : String.format("%d-%d", bpmMin, bpmMax)),
|
||||
(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
|
||||
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
|
||||
beatmap.hitObjectCircle, beatmap.hitObjectSlider, beatmap.hitObjectSpinner);
|
||||
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f",
|
||||
Math.min(beatmap.circleSize * multiplier, 10f),
|
||||
Math.min(beatmap.HPDrainRate * multiplier, 10f),
|
||||
Math.min(beatmap.approachRate * multiplier, 10f),
|
||||
Math.min(beatmap.overallDifficulty * multiplier, 10f));
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string for the beatmap at {@code beatmapIndex}:
|
||||
* "Artist - Title [Version]" (version omitted if {@code beatmapIndex} is invalid)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (beatmapIndex == -1)
|
||||
return String.format("%s - %s", beatmaps.get(0).getArtist(), beatmaps.get(0).getTitle());
|
||||
else
|
||||
return beatmaps.get(beatmapIndex).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the node matches a given search query.
|
||||
* @param query the search term
|
||||
* @return true if title, artist, creator, source, version, or tag matches query
|
||||
*/
|
||||
public boolean matches(String query) {
|
||||
Beatmap beatmap = beatmaps.get(0);
|
||||
|
||||
// search: title, artist, creator, source, version, tags (first beatmap)
|
||||
if (beatmap.title.toLowerCase().contains(query) ||
|
||||
beatmap.titleUnicode.toLowerCase().contains(query) ||
|
||||
beatmap.artist.toLowerCase().contains(query) ||
|
||||
beatmap.artistUnicode.toLowerCase().contains(query) ||
|
||||
beatmap.creator.toLowerCase().contains(query) ||
|
||||
beatmap.source.toLowerCase().contains(query) ||
|
||||
beatmap.version.toLowerCase().contains(query) ||
|
||||
beatmap.tags.contains(query))
|
||||
return true;
|
||||
|
||||
// search: version, tags (remaining beatmaps)
|
||||
for (int i = 1; i < beatmaps.size(); i++) {
|
||||
beatmap = beatmaps.get(i);
|
||||
if (beatmap.version.toLowerCase().contains(query) ||
|
||||
beatmap.tags.contains(query))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the node matches a given condition.
|
||||
* @param type the condition type (ar, cs, od, hp, bpm, length)
|
||||
* @param operator the operator (=/==, >, >=, <, <=)
|
||||
* @param value the value
|
||||
* @return true if the condition is met
|
||||
*/
|
||||
public boolean matches(String type, String operator, float value) {
|
||||
for (Beatmap beatmap : beatmaps) {
|
||||
// get value
|
||||
float v;
|
||||
switch (type) {
|
||||
case "ar": v = beatmap.approachRate; break;
|
||||
case "cs": v = beatmap.circleSize; break;
|
||||
case "od": v = beatmap.overallDifficulty; break;
|
||||
case "hp": v = beatmap.HPDrainRate; break;
|
||||
case "bpm": v = beatmap.bpmMax; break;
|
||||
case "length": v = beatmap.endTime / 1000; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
// get operator
|
||||
boolean met;
|
||||
switch (operator) {
|
||||
case "=":
|
||||
case "==": met = (v == value); break;
|
||||
case ">": met = (v > value); break;
|
||||
case ">=": met = (v >= value); break;
|
||||
case "<": met = (v < value); break;
|
||||
case "<=": met = (v <= value); break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
if (met)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
540
src/itdelatrisu/opsu/beatmap/OsuHitObject.java
Normal file
540
src/itdelatrisu/opsu/beatmap/OsuHitObject.java
Normal file
@@ -0,0 +1,540 @@
|
||||
/*
|
||||
* 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.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Data type representing a parsed 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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
157
src/itdelatrisu/opsu/beatmap/TimingPoint.java
Normal file
157
src/itdelatrisu/opsu/beatmap/TimingPoint.java
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.beatmap;
|
||||
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
* Data type representing a timing point.
|
||||
*/
|
||||
public class TimingPoint {
|
||||
/** Timing point start time/offset (in ms). */
|
||||
private int time = 0;
|
||||
|
||||
/** Time per beat (in ms). [NON-INHERITED] */
|
||||
private float beatLength = 0f;
|
||||
|
||||
/** Slider multiplier. [INHERITED] */
|
||||
private int velocity = 0;
|
||||
|
||||
/** Beats per measure. */
|
||||
private int meter = 4;
|
||||
|
||||
/** Sound sample type. */
|
||||
private byte sampleType = 1;
|
||||
|
||||
/** Custom sound sample type. */
|
||||
private byte sampleTypeCustom = 0;
|
||||
|
||||
/** Volume of samples. [0, 100] */
|
||||
private int sampleVolume = 100;
|
||||
|
||||
/** Whether or not this timing point is inherited. */
|
||||
private boolean inherited = false;
|
||||
|
||||
/** Whether or not Kiai Mode is active. */
|
||||
private boolean kiai = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param line the line to be parsed
|
||||
*/
|
||||
public TimingPoint(String line) {
|
||||
// TODO: better support for old formats
|
||||
String[] tokens = line.split(",");
|
||||
try {
|
||||
this.time = (int) Float.parseFloat(tokens[0]); // rare float
|
||||
this.meter = Integer.parseInt(tokens[2]);
|
||||
this.sampleType = Byte.parseByte(tokens[3]);
|
||||
this.sampleTypeCustom = Byte.parseByte(tokens[4]);
|
||||
this.sampleVolume = Integer.parseInt(tokens[5]);
|
||||
// this.inherited = (Integer.parseInt(tokens[6]) == 1);
|
||||
if (tokens.length > 7)
|
||||
this.kiai = (Integer.parseInt(tokens[7]) == 1);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Log.debug(String.format("Error parsing timing point: '%s'", line));
|
||||
}
|
||||
|
||||
// tokens[1] is either beatLength (positive) or velocity (negative)
|
||||
float beatLength = Float.parseFloat(tokens[1]);
|
||||
if (beatLength > 0)
|
||||
this.beatLength = beatLength;
|
||||
else {
|
||||
this.velocity = (int) beatLength;
|
||||
this.inherited = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timing point start time/offset.
|
||||
* @return the start time (in ms)
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the beat length. [NON-INHERITED]
|
||||
* @return the time per beat (in ms)
|
||||
*/
|
||||
public float getBeatLength() { return beatLength; }
|
||||
|
||||
/**
|
||||
* Returns the slider multiplier. [INHERITED]
|
||||
*/
|
||||
public float getSliderMultiplier() { return velocity / -100f; }
|
||||
|
||||
/**
|
||||
* Returns the meter.
|
||||
* @return the number of beats per measure
|
||||
*/
|
||||
public int getMeter() { return meter; }
|
||||
|
||||
/**
|
||||
* Returns the sample type.
|
||||
* <ul>
|
||||
* <li>0: none
|
||||
* <li>1: normal
|
||||
* <li>2: soft
|
||||
* <li>3: drum
|
||||
* </ul>
|
||||
*/
|
||||
public byte getSampleType() { return sampleType; }
|
||||
|
||||
/**
|
||||
* Returns the custom sample type.
|
||||
* <ul>
|
||||
* <li>0: default
|
||||
* <li>1: custom 1
|
||||
* <li>2: custom 2
|
||||
* </ul>
|
||||
*/
|
||||
public byte getSampleTypeCustom() { return sampleTypeCustom; }
|
||||
|
||||
/**
|
||||
* Returns the sample volume.
|
||||
* @return the sample volume [0, 1]
|
||||
*/
|
||||
public float getSampleVolume() { return sampleVolume / 100f; }
|
||||
|
||||
/**
|
||||
* Returns whether or not this timing point is inherited.
|
||||
* @return the inherited
|
||||
*/
|
||||
public boolean isInherited() { return inherited; }
|
||||
|
||||
/**
|
||||
* Returns whether or not Kiai Time is active.
|
||||
* @return true if active
|
||||
*/
|
||||
public boolean isKiaiTimeActive() { return kiai; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (inherited)
|
||||
return String.format("%d,%d,%d,%d,%d,%d,%d,%d",
|
||||
time, velocity, meter, (int) sampleType,
|
||||
(int) sampleTypeCustom, sampleVolume, 1, (kiai) ? 1: 0);
|
||||
else
|
||||
return String.format("%d,%g,%d,%d,%d,%d,%d,%d",
|
||||
time, beatLength, meter, (int) sampleType,
|
||||
(int) sampleTypeCustom, sampleVolume, 0, (kiai) ? 1: 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user