Major track length-related updates.

- Parse and store the end time for every beatmap (i.e. end time of last hit object).
- Added a 'length' sorting tab.
- Added 'length' search condition.
- Removed 'getTrackLength()' and 'getTrackLengthString()' methods, as they are no longer needed.
- Added a loader spritesheet animation to render during MP3 conversions (in place of track length rendering upon completion).

Other changes:
- Added a yellow progress circle during lead-in time.
- Fixed sorting tab positioning.
- Slightly increased button animation speed in "Main Menu Exit" state.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2014-07-17 21:16:15 -04:00
parent 4ecd50f488
commit b0c0b44ef1
12 changed files with 91 additions and 83 deletions

BIN
res/loader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -478,11 +478,10 @@ public class GameScore {
* scorebar, score, score percentage, map progress circle,
* mod icons, combo count, combo burst, and grade.
* @param g the graphics context
* @param mapLength the length of the beatmap (in ms)
* @param breakPeriod if true, will not draw scorebar and combo elements, and will draw grade
* @param firstObject true if the first hit object's start time has not yet passed
*/
public void drawGameElements(Graphics g, int mapLength, boolean breakPeriod, boolean firstObject) {
public void drawGameElements(Graphics g, boolean breakPeriod, boolean firstObject) {
// score
drawSymbolString((scoreDisplay < 100000000) ? String.format("%08d", scoreDisplay) : Long.toString(scoreDisplay),
width - 2, 0, 1.0f, true);
@ -500,11 +499,19 @@ public class GameScore {
float circleDiameter = symbolHeight * 0.75f;
g.drawOval(circleX, symbolHeight, circleDiameter, circleDiameter);
int firstObjectTime = MusicController.getOsuFile().objects[0].time;
OsuFile osu = MusicController.getOsuFile();
int firstObjectTime = osu.objects[0].time;
int trackPosition = MusicController.getPosition();
if (trackPosition > firstObjectTime) {
// map progress (white)
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / mapLength)
-90, -90 + (int) (360f * (trackPosition - firstObjectTime) / (osu.endTime - firstObjectTime))
);
} else {
// lead-in time (yellow)
g.setColor(Utils.COLOR_YELLOW_ALPHA);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
-90 + (int) (360f * trackPosition / firstObjectTime), -90
);
}

View File

@ -23,7 +23,6 @@ import itdelatrisu.opsu.states.Options;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.IntBuffer;
import java.util.concurrent.TimeUnit;
import javazoom.jl.converter.Converter;
@ -249,48 +248,6 @@ public class MusicController {
return (trackExists() && player.setPosition(position / 1000f));
}
/**
* Gets the length of the track, in milliseconds.
* Returns 0 if no file is loaded or a track is currently being loaded.
* @author bdk (http://slick.ninjacave.com/forum/viewtopic.php?t=2699)
*/
public static int getTrackLength() {
if (!trackExists() || isTrackLoading())
return 0;
float duration = 0f;
try {
// get Music object's (private) Audio object reference
Field sound = player.getClass().getDeclaredField("sound");
sound.setAccessible(true);
Audio audio = (Audio) (sound.get(player));
// access Audio object's (private)'length' field
Field length = audio.getClass().getDeclaredField("length");
length.setAccessible(true);
duration = (float) (length.get(audio));
} catch (Exception e) {
Log.debug("Could not get track length.");
return 0;
}
return (int) (duration * 1000);
}
/**
* Gets the length of the track as a formatted string (M:SS).
* Returns "--" if a track is currently being loaded.
*/
public static String getTrackLengthString() {
if (isTrackLoading())
return "...";
int duration = getTrackLength();
return String.format("%d:%02d",
TimeUnit.MILLISECONDS.toMinutes(duration),
TimeUnit.MILLISECONDS.toSeconds(duration) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration)));
}
/**
* Stops and releases all sources, clears each of the specified Audio
* buffers, destroys the OpenAL context, and resets SoundStore for future use.

View File

@ -96,6 +96,7 @@ public class OsuFile implements Comparable<OsuFile> {
public int hitObjectCircle = 0; // number of circles
public int hitObjectSlider = 0; // number of sliders
public int hitObjectSpinner = 0; // number of spinners
public int endTime = -1; // last object end time (in ms)
/**
* Constructor.

View File

@ -38,13 +38,14 @@ public class OsuGroupList {
SORT_ARTIST = 1,
SORT_CREATOR = 2,
SORT_BPM = 3,
SORT_MAX = 4; // not a sort
SORT_LENGTH = 4,
SORT_MAX = 5; // not a sort
/**
* Sorting order names (indexed by SORT_* constants).
*/
public static final String[] SORT_NAMES = {
"Title", "Artist", "Creator", "BPM"
"Title", "Artist", "Creator", "BPM", "Length"
};
/**
@ -240,6 +241,9 @@ public class OsuGroupList {
case SORT_BPM:
Collections.sort(nodes, new OsuGroupNode.BPMOrder());
break;
case SORT_LENGTH:
Collections.sort(nodes, new OsuGroupNode.LengthOrder());
break;
}
expandedIndex = -1;

View File

@ -20,6 +20,7 @@ package itdelatrisu.opsu;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
@ -100,6 +101,26 @@ public class OsuGroupNode implements Comparable<OsuGroupNode> {
}
}
/**
* Compares two OsuGroupNode objects by length.
* Uses the longest beatmap in each set for comparison.
*/
public static class LengthOrder implements Comparator<OsuGroupNode> {
@Override
public int compare(OsuGroupNode v, OsuGroupNode w) {
int vMax = 0, wMax = 0;
for (OsuFile osu : v.osuFiles) {
if (osu.endTime > vMax)
vMax = osu.endTime;
}
for (OsuFile osu : w.osuFiles) {
if (osu.endTime > wMax)
wMax = osu.endTime;
}
return Integer.compare(vMax, wMax);
}
}
/**
* Sets a button background image.
*/
@ -162,8 +183,10 @@ public class OsuGroupNode implements Comparable<OsuGroupNode> {
info[0] = osu.toString();
info[1] = String.format("Mapped by %s",
osu.creator);
info[2] = String.format("Length: %s BPM: %s Objects: %d",
MusicController.getTrackLengthString(),
info[2] = String.format("Length: %d:%02d BPM: %s Objects: %d",
TimeUnit.MILLISECONDS.toMinutes(osu.endTime),
TimeUnit.MILLISECONDS.toSeconds(osu.endTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(osu.endTime)),
(osu.bpmMax <= 0) ? "--" :
((osu.bpmMin == osu.bpmMax) ? osu.bpmMin : String.format("%d-%d", osu.bpmMin, osu.bpmMax)),
(osu.hitObjectCircle + osu.hitObjectSlider + osu.hitObjectSpinner));
@ -232,7 +255,7 @@ public class OsuGroupNode implements Comparable<OsuGroupNode> {
case "od": osuValue = osu.overallDifficulty; break;
case "hp": osuValue = osu.HPDrainRate; break;
case "bpm": osuValue = osu.bpmMax; break;
// case "length": /* not implemented */ break;
case "length": osuValue = osu.endTime / 1000; break;
default: return false;
}

View File

@ -131,7 +131,7 @@ public class OsuParser {
osu.timingPoints = new ArrayList<OsuTimingPoint>();
String line = in.readLine();
String tokens[];
String tokens[] = null;
while (line != null) {
line = line.trim();
if (!isValidLine(line)) {
@ -386,6 +386,7 @@ public class OsuParser {
osu.combo = colors.toArray(new Color[colors.size()]);
break;
case "[HitObjects]":
int type = -1;
while ((line = in.readLine()) != null) {
line = line.trim();
if (!isValidLine(line))
@ -394,7 +395,7 @@ public class OsuParser {
break;
/* Only type counts parsed at this time. */
tokens = line.split(",");
int type = Integer.parseInt(tokens[3]);
type = Integer.parseInt(tokens[3]);
if ((type & OsuHitObject.TYPE_CIRCLE) > 0)
osu.hitObjectCircle++;
else if ((type & OsuHitObject.TYPE_SLIDER) > 0)
@ -402,6 +403,16 @@ public class OsuParser {
else //if ((type & OsuHitObject.TYPE_SPINNER) > 0)
osu.hitObjectSpinner++;
}
// 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
osu.endTime = Integer.parseInt(tokens[2]);
break;
default:
line = in.readLine();

View File

@ -59,7 +59,8 @@ public class Utils {
COLOR_GREEN_OBJECT = new Color(26, 207, 26),
COLOR_BLUE_OBJECT = new Color(46, 136, 248),
COLOR_RED_OBJECT = new Color(243, 48, 77),
COLOR_ORANGE_OBJECT = new Color(255, 200, 32);
COLOR_ORANGE_OBJECT = new Color(255, 200, 32),
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f);
/**
* The default map colors, used when a map does not provide custom colors.

View File

@ -114,11 +114,6 @@ public class Game extends BasicGameState {
*/
private int[] hitResultOffset;
/**
* Time, in milliseconds, between the first and last hit object.
*/
private int mapLength;
/**
* Current break index in breaks ArrayList.
*/
@ -274,7 +269,7 @@ public class Game extends BasicGameState {
g.fillRect(0, height * 0.875f, width, height * 0.125f);
}
score.drawGameElements(g, mapLength, true, objectIndex == 0);
score.drawGameElements(g, true, objectIndex == 0);
if (breakLength >= 8000 &&
trackPosition - breakTime > 2000 &&
@ -317,7 +312,7 @@ public class Game extends BasicGameState {
}
// game elements
score.drawGameElements(g, mapLength, false, objectIndex == 0);
score.drawGameElements(g, false, objectIndex == 0);
// skip beginning
if (objectIndex == 0 &&
@ -624,7 +619,7 @@ public class Game extends BasicGameState {
// load checkpoint
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
int checkpoint = Options.getCheckpoint();
if (checkpoint == 0 || checkpoint > MusicController.getTrackLength())
if (checkpoint == 0 || checkpoint > osu.endTime)
break; // invalid checkpoint
try {
restart = RESTART_MANUAL;
@ -728,15 +723,6 @@ public class Game extends BasicGameState {
if (restart == RESTART_NEW) {
loadImages();
setMapModifiers();
// calculate map length (TODO: end on slider?)
OsuHitObject lastObject = osu.objects[osu.objects.length - 1];
int endTime;
if ((lastObject.type & OsuHitObject.TYPE_SPINNER) > 0)
endTime = lastObject.endTime;
else
endTime = lastObject.time;
mapLength = endTime - osu.objects[0].time;
}
// initialize object maps

View File

@ -190,7 +190,7 @@ public class MainMenu extends BasicGameState {
g.setColor(Color.white);
if (!MusicController.isTrackLoading())
g.fillRoundRect(width - 168, 54,
148f * MusicController.getPosition() / MusicController.getTrackLength(), 5, 4);
148f * MusicController.getPosition() / osu.endTime, 5, 4);
// draw text
g.setFont(Utils.FONT_MEDIUM);

View File

@ -120,9 +120,9 @@ public class MainMenuExit extends BasicGameState {
float yesX = yesButton.getX(), noX = noButton.getX();
float center = container.getWidth() / 2f;
if (yesX < center)
yesButton.setX(Math.min(yesX + (delta / 6f), center));
yesButton.setX(Math.min(yesX + (delta / 5f), center));
if (noX > center)
noButton.setX(Math.max(noX - (delta / 6f), center));
noButton.setX(Math.max(noX - (delta / 5f), center));
}
@Override

View File

@ -29,12 +29,14 @@ import itdelatrisu.opsu.SoundController;
import itdelatrisu.opsu.Utils;
import org.lwjgl.opengl.Display;
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.SpriteSheet;
import org.newdawn.slick.gui.TextField;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
@ -128,6 +130,11 @@ public class SongMenu extends BasicGameState {
*/
private Image musicNote;
/**
* Loader animation.
*/
private Animation loader;
// game-related variables
private GameContainer container;
private StateBasedGame game;
@ -164,7 +171,7 @@ public class SongMenu extends BasicGameState {
Image tab = Utils.getTabImage();
float tabX = buttonX + (tab.getWidth() / 2f);
float tabY = (height * 0.15f) - (tab.getHeight() / 2f) - 2f;
float tabOffset = (width - buttonX) / sortTabs.length;
float tabOffset = (width - buttonX - tab.getWidth()) / (sortTabs.length - 1);
for (int i = 0; i < sortTabs.length; i++)
sortTabs[i] = new GUIMenuButton(tab, tabX + (i * tabOffset), tabY);
@ -178,7 +185,7 @@ public class SongMenu extends BasicGameState {
search = new TextField(
container, Utils.FONT_DEFAULT,
(int) tabX + searchIcon.getWidth(), (int) ((height * 0.15f) - (tab.getHeight() * 5 / 2f)),
(int) tabX + searchIcon.getWidth(), (int) ((height * 0.15f) - (tab.getHeight() * 2.5f)),
(int) (buttonWidth / 2), Utils.FONT_DEFAULT.getHeight()
);
search.setBackgroundColor(Color.transparent);
@ -191,8 +198,16 @@ public class SongMenu extends BasicGameState {
Image optionsIcon = new Image("options.png").getScaledCopy(iconScale);
optionsButton = new GUIMenuButton(optionsIcon, search.getX() - (optionsIcon.getWidth() * 1.5f), search.getY());
// music note
int musicNoteDim = (int) (Utils.FONT_LARGE.getHeight() * 0.75f + Utils.FONT_DEFAULT.getHeight());
musicNote = new Image("music-note.png").getScaledCopy(musicNoteDim, musicNoteDim);
// loader
SpriteSheet spr = new SpriteSheet(
new Image("loader.png").getScaledCopy(musicNoteDim / 48f),
musicNoteDim, musicNoteDim
);
loader = new Animation(spr, 50);
}
@Override
@ -218,16 +233,19 @@ public class SongMenu extends BasicGameState {
// header
if (focusNode != null) {
musicNote.draw();
int musicNoteWidth = musicNote.getWidth();
int musicNoteHeight = musicNote.getHeight();
if (MusicController.isTrackLoading())
loader.draw();
else
musicNote.draw();
int iconWidth = musicNote.getWidth();
int iconHeight = musicNote.getHeight();
String[] info = focusNode.getInfo();
g.setColor(Color.white);
Utils.FONT_LARGE.drawString(musicNoteWidth + 5, -3, info[0]);
Utils.FONT_LARGE.drawString(iconWidth + 5, -3, info[0]);
Utils.FONT_DEFAULT.drawString(
musicNoteWidth + 5, -3 + Utils.FONT_LARGE.getHeight() * 0.75f, info[1]);
int headerY = musicNoteHeight - 3;
iconWidth + 5, -3 + Utils.FONT_LARGE.getHeight() * 0.75f, info[1]);
int headerY = iconHeight - 3;
Utils.FONT_BOLD.drawString(5, headerY, info[2]);
headerY += Utils.FONT_BOLD.getLineHeight() - 6;
Utils.FONT_DEFAULT.drawString(5, headerY, info[3]);