diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index cbcf242e..c5b8ddf1 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -18,6 +18,8 @@ package itdelatrisu.opsu; +import itdelatrisu.opsu.states.SongMenu; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -381,7 +383,7 @@ public enum GameImage { MENU_BUTTON_BG ("menu-button-background", "png", false, false) { @Override protected Image process_sub(Image img, int w, int h) { - return img.getScaledCopy(w / 2, h / 6); + return img.getScaledCopy(w / 2, h / SongMenu.MAX_SONG_BUTTONS); } }, MENU_TAB ("selection-tab", "png", false, false) { diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index 07383df6..5d261d33 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -393,6 +393,7 @@ public class Options { RES_1440_900 (1440, 900), RES_1600_900 (1600, 900), RES_1680_1050 (1680, 1050), + RES_1600_1200 (1600, 1200), RES_1920_1080 (1920, 1080), RES_1920_1200 (1920, 1200), RES_2560_1440 (2560, 1440), diff --git a/src/itdelatrisu/opsu/OsuFile.java b/src/itdelatrisu/opsu/OsuFile.java index cc24149b..01c1a9f9 100644 --- a/src/itdelatrisu/opsu/OsuFile.java +++ b/src/itdelatrisu/opsu/OsuFile.java @@ -34,6 +34,9 @@ public class OsuFile implements Comparable { /** Map of all loaded background images. */ private static HashMap bgImageMap = new HashMap(); + /** 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; @@ -250,19 +253,34 @@ public class OsuFile implements Comparable { * @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) { + 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) { - bgImage = new Image(bg).getScaledCopy(width, height); + bgImage = new Image(bg); + if (bgImageMap.size() > MAX_CACHE_SIZE) + clearImageCache(); bgImageMap.put(this, bgImage); } + + // fit image to screen + int swidth = width; + int sheight = height; + if (!stretch) { + 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()); + } + bgImage = bgImage.getScaledCopy(swidth, sheight); + bgImage.setAlpha(alpha); - bgImage.draw(); + 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 diff --git a/src/itdelatrisu/opsu/OsuGroupNode.java b/src/itdelatrisu/opsu/OsuGroupNode.java index d8436e40..69e8eb39 100644 --- a/src/itdelatrisu/opsu/OsuGroupNode.java +++ b/src/itdelatrisu/opsu/OsuGroupNode.java @@ -150,7 +150,9 @@ public class OsuGroupNode { // search: title, artist, creator, source, version, tags (first OsuFile) if (osu.title.toLowerCase().contains(query) || + osu.titleUnicode.toLowerCase().contains(query) || osu.artist.toLowerCase().contains(query) || + osu.artistUnicode.toLowerCase().contains(query) || osu.creator.toLowerCase().contains(query) || osu.source.toLowerCase().contains(query) || osu.version.toLowerCase().contains(query) || diff --git a/src/itdelatrisu/opsu/OsuHitObject.java b/src/itdelatrisu/opsu/OsuHitObject.java index 9b8d077b..fec96502 100644 --- a/src/itdelatrisu/opsu/OsuHitObject.java +++ b/src/itdelatrisu/opsu/OsuHitObject.java @@ -148,8 +148,8 @@ public class OsuHitObject { String tokens[] = line.split(","); // common fields - this.x = Integer.parseInt(tokens[0]) * xMultiplier + xOffset; - this.y = Integer.parseInt(tokens[1]) * yMultiplier + yOffset; + this.x = Float.parseFloat(tokens[0]) * xMultiplier + xOffset; + this.y = Float.parseFloat(tokens[1]) * yMultiplier + yOffset; this.time = Integer.parseInt(tokens[2]); this.type = Integer.parseInt(tokens[3]); this.hitSound = Byte.parseByte(tokens[4]); diff --git a/src/itdelatrisu/opsu/OsuTimingPoint.java b/src/itdelatrisu/opsu/OsuTimingPoint.java index 31f610c1..eedca9c6 100644 --- a/src/itdelatrisu/opsu/OsuTimingPoint.java +++ b/src/itdelatrisu/opsu/OsuTimingPoint.java @@ -65,7 +65,8 @@ public class OsuTimingPoint { this.sampleTypeCustom = Byte.parseByte(tokens[4]); this.sampleVolume = Integer.parseInt(tokens[5]); // this.inherited = (Integer.parseInt(tokens[6]) == 1); - this.kiai = (Integer.parseInt(tokens[7]) == 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)); } diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index 763fb945..81c47ee5 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -44,6 +44,9 @@ public class Download { /** Read timeout, in ms. */ public static final int READ_TIMEOUT = 10000; + /** Time between download speed and ETA updates, in ms. */ + private static final int UPDATE_INTERVAL = 1000; + /** Download statuses. */ public enum Status { WAITING ("Waiting"), @@ -90,6 +93,18 @@ public class Download { /** The download status. */ private Status status = Status.WAITING; + /** Time when lastReadSoFar was updated. */ + private long lastReadSoFarTime = -1; + + /** Last readSoFar amount. */ + private long lastReadSoFar = -1; + + /** Last calculated download speed string. */ + private String lastDownloadSpeed; + + /** Last calculated ETA string. */ + private String lastTimeRemaining; + /** * Constructor. * @param remoteURL the download URL @@ -234,6 +249,60 @@ public class Download { } } + /** + * Returns the last calculated download speed, or null if not downloading. + */ + public String getDownloadSpeed() { + updateReadSoFar(); + return lastDownloadSpeed; + } + + /** + * Returns the last calculated ETA, or null if not downloading. + */ + public String getTimeRemaining() { + updateReadSoFar(); + return lastTimeRemaining; + } + + /** + * Updates the last readSoFar and related fields. + */ + private void updateReadSoFar() { + // only update while downloading + if (status != Status.DOWNLOADING) { + this.lastDownloadSpeed = null; + this.lastTimeRemaining = null; + return; + } + + // update download speed and ETA + if (System.currentTimeMillis() > lastReadSoFarTime + UPDATE_INTERVAL) { + long readSoFar = readSoFar(); + long readSoFarTime = System.currentTimeMillis(); + long dlspeed = (readSoFar - lastReadSoFar) * 1000 / (readSoFarTime - lastReadSoFarTime); + if (dlspeed > 0) { + this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(dlspeed)); + long t = (contentLength - readSoFar) / dlspeed; + if (t >= 3600) + this.lastTimeRemaining = String.format("%dh%dm%ds", t / 3600, (t / 60) % 60, t % 60); + else + this.lastTimeRemaining = String.format("%dm%ds", t / 60, t % 60); + } else { + this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(0)); + this.lastTimeRemaining = "?"; + } + this.lastReadSoFarTime = readSoFarTime; + this.lastReadSoFar = readSoFar; + } + + // first call + else if (lastReadSoFarTime <= 0) { + this.lastReadSoFar = readSoFar(); + this.lastReadSoFarTime = System.currentTimeMillis(); + } + } + /** * Cancels the download, if running. */ diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 5ebd1070..6cf06890 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -348,9 +348,13 @@ public class DownloadNode { info = status.getName(); else if (status == Download.Status.WAITING) info = String.format("%s...", status.getName()); - else - info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress, - Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength())); + else { + if (hover) + info = String.format("%s: %s left (%s)", status.getName(), download.getTimeRemaining(), download.getDownloadSpeed()); + else + info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress, + Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength())); + } Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white); Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white); diff --git a/src/itdelatrisu/opsu/objects/Circle.java b/src/itdelatrisu/opsu/objects/Circle.java index 57783813..9a49a3af 100644 --- a/src/itdelatrisu/opsu/objects/Circle.java +++ b/src/itdelatrisu/opsu/objects/Circle.java @@ -56,7 +56,7 @@ public class Circle implements HitObject { */ public static void init(GameContainer container, float circleSize) { int diameter = (int) (96 - (circleSize * 8)); - diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480) + diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter)); GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter)); GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter)); diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index ddb9f09c..1e5de886 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -99,7 +99,7 @@ public class Slider implements HitObject { */ public static void init(GameContainer container, float circleSize, OsuFile osu) { int diameter = (int) (96 - (circleSize * 8)); - diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480) + diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) // slider ball Image[] sliderBallImages; @@ -211,9 +211,13 @@ public class Slider implements HitObject { Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color); } else { float[] c = curve.pointAt(getT(trackPosition, false)); + float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f); // slider ball - Utils.drawCentered(sliderBall, c[0], c[1]); + Image sliderBallFrame = sliderBall.getCurrentFrame(); + float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI); + sliderBallFrame.setRotation(angle); + sliderBallFrame.drawCentered(c[0], c[1]); // follow circle if (followCircleActive) diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index bfae5428..0a7b2dfa 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -660,6 +660,12 @@ public class DownloadsMenu extends BasicGameState { pageDir = Page.RESET; } + @Override + public void leave(GameContainer container, StateBasedGame game) + throws SlickException { + search.setFocus(false); + } + /** * Resets the search timer, but respects the minimum request interval. */ diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 7daf16ac..d55d9543 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -173,7 +173,7 @@ public class Game extends BasicGameState { // background g.setBackground(Color.black); float dimLevel = Options.getBackgroundDim(); - if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel)) { + if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) { Image playfield = GameImage.PLAYFIELD.getImage(); playfield.setAlpha(dimLevel); playfield.draw(); diff --git a/src/itdelatrisu/opsu/states/GameRanking.java b/src/itdelatrisu/opsu/states/GameRanking.java index 73db38cc..07c67333 100644 --- a/src/itdelatrisu/opsu/states/GameRanking.java +++ b/src/itdelatrisu/opsu/states/GameRanking.java @@ -95,7 +95,7 @@ public class GameRanking extends BasicGameState { OsuFile osu = MusicController.getOsuFile(); // background - if (!osu.drawBG(width, height, 0.7f)) + if (!osu.drawBG(width, height, 0.7f, true)) g.setBackground(Utils.COLOR_BLACK_ALPHA); // ranking screen elements diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index e0d52c78..09efc648 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -168,7 +168,7 @@ public class MainMenu extends BasicGameState { // draw background OsuFile osu = MusicController.getOsuFile(); if (Options.isDynamicBackgroundEnabled() && - osu != null && osu.drawBG(width, height, bgAlpha)) + osu != null && osu.drawBG(width, height, bgAlpha, true)) ; else { Image bg = GameImage.MENU_BG.getImage(); diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index d8a6c1b6..2537b738 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -68,7 +68,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition; */ public class SongMenu extends BasicGameState { /** The max number of song buttons to be shown on each screen. */ - private static final int MAX_SONG_BUTTONS = 6; + public static final int MAX_SONG_BUTTONS = 6; /** The max number of score buttons to be shown at a time. */ public static final int MAX_SCORE_BUTTONS = 7; @@ -239,7 +239,7 @@ public class SongMenu extends BasicGameState { // background if (focusNode != null) - focusNode.osuFiles.get(focusNode.osuFileIndex).drawBG(width, height, 1.0f); + focusNode.osuFiles.get(focusNode.osuFileIndex).drawBG(width, height, 1.0f, true); // header setup float lowerBound = height * 0.15f;