diff --git a/.gitignore b/.gitignore index 4ba1441d..6348415d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /Skins/ /SongPacks/ /Songs/ +/Temp/ /.opsu.log /.opsu.cfg /.opsu.db* diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index f462a3e7..00de84dd 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -19,6 +19,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; +import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapWatchService; @@ -61,6 +62,7 @@ public class Container extends AppGameContainer { public void start() throws SlickException { try { setup(); + ErrorHandler.setGlString(); getDelta(); while (running()) gameLoop(); @@ -131,6 +133,9 @@ public class Container extends AppGameContainer { // prevent loading tracks from re-initializing OpenAL MusicController.reset(); + // stop any playing track + SoundController.stopTrack(); + // reset BeatmapSetList data if (BeatmapSetList.get() != null) BeatmapSetList.get().reset(); @@ -142,6 +147,9 @@ public class Container extends AppGameContainer { if (!Options.isWatchServiceEnabled()) BeatmapWatchService.destroy(); BeatmapWatchService.removeListeners(); + + // delete temporary directory + Utils.deleteDirectory(Options.TEMP_DIR); } @Override diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java index 356858b0..1ccead20 100644 --- a/src/itdelatrisu/opsu/ErrorHandler.java +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -33,6 +33,7 @@ import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.UIManager; +import org.lwjgl.opengl.GL11; import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; @@ -73,9 +74,23 @@ public class ErrorHandler { message = { desc, scroll }, messageReport = { descReport, scroll }; + /** OpenGL string (if any). */ + private static String glString = null; + // This class should not be instantiated. private ErrorHandler() {} + /** + * Sets the OpenGL version string. + */ + public static void setGlString() { + try { + String glVersion = GL11.glGetString(GL11.GL_VERSION); + String glVendor = GL11.glGetString(GL11.GL_VENDOR); + glString = String.format("%s (%s)", glVersion, glVendor); + } catch (Exception e) {} + } + /** * Displays an error popup and logs the given error. * @param error a description of the error @@ -197,6 +212,11 @@ public class ErrorHandler { sb.append("**JRE:** "); sb.append(System.getProperty("java.version")); sb.append('\n'); + if (glString != null) { + sb.append("**OpenGL Version:** "); + sb.append(glString); + sb.append('\n'); + } if (error != null) { sb.append("**Error:** `"); sb.append(error); diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 11aac0a3..80852ca5 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -391,7 +391,7 @@ public class GameData { if (hitResultList != null) { for (HitObjectResult hitResult : hitResultList) { if (hitResult.curve != null) - hitResult.curve.discardCache(); + hitResult.curve.discardGeometry(); } } hitResultList = new LinkedBlockingDeque(); @@ -964,7 +964,7 @@ public class GameData { hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME); } else { if (hitResult.curve != null) - hitResult.curve.discardCache(); + hitResult.curve.discardGeometry(); iter.remove(); } } @@ -1436,34 +1436,45 @@ public class GameData { * (i.e. this will not overwrite existing data). * @param beatmap the beatmap * @return the ScoreData object + * @see #getCurrentScoreData(Beatmap, boolean) */ public ScoreData getScoreData(Beatmap beatmap) { - if (scoreData != null) - return scoreData; - - scoreData = new ScoreData(); - scoreData.timestamp = System.currentTimeMillis() / 1000L; - scoreData.MID = beatmap.beatmapID; - scoreData.MSID = beatmap.beatmapSetID; - scoreData.title = beatmap.title; - scoreData.artist = beatmap.artist; - scoreData.creator = beatmap.creator; - scoreData.version = beatmap.version; - scoreData.hit300 = hitResultCount[HIT_300]; - scoreData.hit100 = hitResultCount[HIT_100]; - scoreData.hit50 = hitResultCount[HIT_50]; - scoreData.geki = hitResultCount[HIT_300G]; - scoreData.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K]; - scoreData.miss = hitResultCount[HIT_MISS]; - scoreData.score = score; - scoreData.combo = comboMax; - scoreData.perfect = (comboMax == fullObjectCount); - scoreData.mods = GameMod.getModState(); - scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); - scoreData.playerName = null; // TODO + if (scoreData == null) + scoreData = getCurrentScoreData(beatmap, false); return scoreData; } + /** + * Returns a ScoreData object encapsulating all current game data. + * @param beatmap the beatmap + * @param slidingScore if true, use the display score (might not be actual score) + * @return the ScoreData object + * @see #getScoreData(Beatmap) + */ + public ScoreData getCurrentScoreData(Beatmap beatmap, boolean slidingScore) { + ScoreData sd = new ScoreData(); + sd.timestamp = System.currentTimeMillis() / 1000L; + sd.MID = beatmap.beatmapID; + sd.MSID = beatmap.beatmapSetID; + sd.title = beatmap.title; + sd.artist = beatmap.artist; + sd.creator = beatmap.creator; + sd.version = beatmap.version; + sd.hit300 = hitResultCount[HIT_300]; + sd.hit100 = hitResultCount[HIT_100]; + sd.hit50 = hitResultCount[HIT_50]; + sd.geki = hitResultCount[HIT_300G]; + sd.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K]; + sd.miss = hitResultCount[HIT_MISS]; + sd.score = slidingScore ? scoreDisplay : score; + sd.combo = comboMax; + sd.perfect = (comboMax == fullObjectCount); + sd.mods = GameMod.getModState(); + sd.replayString = (replay == null) ? null : replay.getReplayFilename(); + sd.playerName = null; // TODO + return sd; + } + /** * Returns a Replay object encapsulating all game data. * If a replay already exists and frames is null, the existing object will be returned. diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 379bde4c..64dbe3ae 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -107,10 +107,13 @@ public class Opsu extends StateBasedGame { } catch (FileNotFoundException e) { Log.error(e); } + + // set default exception handler Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { ErrorHandler.error("** Uncaught Exception! **", e, true); + System.exit(1); } }); @@ -124,16 +127,21 @@ public class Opsu extends StateBasedGame { } catch (UnknownHostException e) { // shouldn't happen } catch (IOException e) { - ErrorHandler.error(String.format( - "opsu! could not be launched for one of these reasons:\n" + - "- An instance of opsu! is already running.\n" + - "- Another program is bound to port %d. " + - "You can change the port opsu! uses by editing the \"Port\" field in the configuration file.", - Options.getPort()), null, false); - System.exit(1); + errorAndExit( + null, + String.format( + "opsu! could not be launched for one of these reasons:\n" + + "- An instance of opsu! is already running.\n" + + "- Another program is bound to port %d. " + + "You can change the port opsu! uses by editing the \"Port\" field in the configuration file.", + Options.getPort() + ), + false + ); } } + // load natives File nativeDir; if (!Utils.isJarRunning() && ( (nativeDir = new File("./target/natives/")).isDirectory() || @@ -166,7 +174,7 @@ public class Opsu extends StateBasedGame { try { DBController.init(); } catch (UnsatisfiedLinkError e) { - errorAndExit(e, "The databases could not be initialized."); + errorAndExit(e, "The databases could not be initialized.", true); } // check if just updated @@ -213,7 +221,7 @@ public class Opsu extends StateBasedGame { } } } catch (SlickException e) { - errorAndExit(e, "An error occurred while creating the game container."); + errorAndExit(e, "An error occurred while creating the game container.", true); } } @@ -232,6 +240,7 @@ public class Opsu extends StateBasedGame { } else { if (id == STATE_GAME) { MusicController.pause(); + MusicController.setPitch(1.0f); MusicController.resume(); } else songMenu.resetTrackOnLoad(); @@ -278,15 +287,16 @@ public class Opsu extends StateBasedGame { * Throws an error and exits the application with the given message. * @param e the exception that caused the crash * @param message the message to display + * @param report whether to ask to report the error */ - private static void errorAndExit(Throwable e, String message) { + private static void errorAndExit(Throwable e, String message, boolean report) { // JARs will not run properly inside directories containing '!' // http://bugs.java.com/view_bug.do?bug_id=4523159 if (Utils.isJarRunning() && Utils.getRunningDirectory() != null && Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1) ErrorHandler.error("JARs cannot be run from some paths containing '!'. Please move or rename the file and try again.", null, false); else - ErrorHandler.error(message, e, true); + ErrorHandler.error(message, e, report); System.exit(1); } } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index d97023dc..5759e65b 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -101,6 +101,9 @@ public class Options { /** Directory where natives are unpacked. */ public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/"); + /** Directory where temporary files are stored (deleted on exit). */ + public static final File TEMP_DIR = new File(CACHE_DIR, "Temp/"); + /** Font file name. */ public static final String FONT_NAME = "DroidSansFallback.ttf"; diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index bb14c88e..b873672f 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -30,6 +30,7 @@ import java.sql.SQLException; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; import org.newdawn.slick.Color; import org.newdawn.slick.Graphics; @@ -328,6 +329,69 @@ public class ScoreData implements Comparable { c.a = oldAlpha; } + /** + * Draws the score in-game (smaller and with less information). + * @param g the current graphics context + * @param vPos the base y position of the scoreboard + * @param rank the current rank of this score + * @param position the animated position offset + * @param data an instance of GameData to draw rank number + * @param alpha the transparency of the score + * @param isActive if this score is the one currently played + */ + public void drawSmall(Graphics g, int vPos, int rank, float position, GameData data, float alpha, boolean isActive) { + int rectWidth = (int) (145 * GameImage.getUIscale()); //135 + int rectHeight = data.getScoreSymbolImage('0').getHeight(); + int vertDistance = rectHeight + 10; + int yPos = (int) (vPos + position * vertDistance - rectHeight / 2); + int xPaddingLeft = Math.max(4, (int) (rectWidth * 0.04f)); + int xPaddingRight = Math.max(2, (int) (rectWidth * 0.02f)); + int yPadding = Math.max(2, (int) (rectHeight * 0.02f)); + String scoreString = String.format(Locale.US, "%,d", score); + String comboString = String.format("%dx", combo); + String rankString = String.format("%d", rank); + + Color white = Colors.WHITE_ALPHA, blue = Colors.BLUE_SCOREBOARD, black = Colors.BLACK_ALPHA; + float oldAlphaWhite = white.a, oldAlphaBlue = blue.a, oldAlphaBlack = black.a; + + // rectangle background + Color rectColor = isActive ? white : blue; + rectColor.a = alpha * 0.2f; + g.setColor(rectColor); + g.fillRect(0, yPos, rectWidth, rectHeight); + black.a = alpha * 0.2f; + g.setColor(black); + float oldLineWidth = g.getLineWidth(); + g.setLineWidth(1f); + g.drawRect(0, yPos, rectWidth, rectHeight); + g.setLineWidth(oldLineWidth); + + // rank + data.drawSymbolString(rankString, rectWidth, yPos, 1.0f, alpha * 0.2f, true); + + white.a = blue.a = alpha * 0.75f; + + // player name + if (playerName != null) + Fonts.MEDIUMBOLD.drawString(xPaddingLeft, yPos + yPadding, playerName, white); + + // score + Fonts.DEFAULT.drawString( + xPaddingLeft, yPos + rectHeight - Fonts.DEFAULT.getLineHeight() - yPadding, scoreString, white + ); + + // combo + Fonts.DEFAULT.drawString( + rectWidth - Fonts.DEFAULT.getWidth(comboString) - xPaddingRight, + yPos + rectHeight - Fonts.DEFAULT.getLineHeight() - yPadding, + comboString, blue + ); + + white.a = oldAlphaWhite; + blue.a = oldAlphaBlue; + black.a = oldAlphaBlack; + } + /** * Returns the tooltip string for this score. */ diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index c1b755e8..7ebc1117 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -23,7 +23,6 @@ import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.DownloadNode; -import itdelatrisu.opsu.objects.Circle; import itdelatrisu.opsu.replay.PlaybackSpeed; import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.UI; @@ -44,6 +43,7 @@ import java.net.URL; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -51,6 +51,10 @@ import java.util.Scanner; import java.util.jar.JarFile; import javax.imageio.ImageIO; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import org.json.JSONArray; import org.json.JSONException; @@ -349,7 +353,7 @@ public class Utils { * deletes the directory itself. * @param dir the directory to delete */ - private static void deleteDirectory(File dir) { + public static void deleteDirectory(File dir) { if (dir == null || !dir.isDirectory()) return; @@ -570,6 +574,30 @@ public class Utils { } } + /** + * Switches validation of SSL certificates on or off by installing a default + * all-trusting {@link TrustManager}. + * @param enabled whether to validate SSL certificates + * @author neu242 (http://stackoverflow.com/a/876785) + */ + public static void setSSLCertValidation(boolean enabled) { + // create a trust manager that does not validate certificate chains + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + @Override public void checkClientTrusted(X509Certificate[] certs, String authType) {} + @Override public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + + // install the all-trusting trust manager + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, enabled ? null : trustAllCerts, null); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } catch (Exception e) {} + } + public static int getQuadrant(double x, double y) { if (x < Options.width / 2d) { return y < Options.height / 2d ? 2 : 3; diff --git a/src/itdelatrisu/opsu/audio/SoundController.java b/src/itdelatrisu/opsu/audio/SoundController.java index e9b4f9a6..7db7f608 100644 --- a/src/itdelatrisu/opsu/audio/SoundController.java +++ b/src/itdelatrisu/opsu/audio/SoundController.java @@ -22,6 +22,9 @@ import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.Options; import itdelatrisu.opsu.audio.HitSound.SampleSet; import itdelatrisu.opsu.beatmap.HitObject; +import itdelatrisu.opsu.downloads.Download; +import itdelatrisu.opsu.downloads.Download.DownloadListener; +import itdelatrisu.opsu.ui.UI; import java.io.File; import java.io.IOException; @@ -355,24 +358,58 @@ public class SoundController { } /** - * Plays a track from a URL. + * Plays a track from a remote URL. * If a track is currently playing, it will be stopped. - * @param url the resource URL + * @param url the remote URL + * @param name the track file name * @param isMP3 true if MP3, false if WAV * @param listener the line listener - * @return the MultiClip being played + * @return true if playing, false otherwise * @throws SlickException if any error occurred */ - public static synchronized MultiClip playTrack(URL url, boolean isMP3, LineListener listener) throws SlickException { + public static synchronized boolean playTrack(String url, String name, boolean isMP3, LineListener listener) + throws SlickException { + // stop previous track stopTrack(); - try { - AudioInputStream audioIn = AudioSystem.getAudioInputStream(url); - currentTrack = loadClip(url.getFile(), audioIn, isMP3); - playClip(currentTrack, Options.getMusicVolume() * Options.getMasterVolume(), listener); - return currentTrack; - } catch (Exception e) { - throw new SlickException(String.format("Failed to load clip '%s'.", url.getFile(), e)); + + // download new track + File dir = Options.TEMP_DIR; + if (!dir.isDirectory()) + dir.mkdir(); + String filename = String.format("%s.%s", name, isMP3 ? "mp3" : "wav"); + final File downloadFile = new File(dir, filename); + boolean complete; + if (downloadFile.isFile()) { + complete = true; // file already downloaded + } else { + Download download = new Download(url, downloadFile.getAbsolutePath()); + download.setListener(new DownloadListener() { + @Override + public void completed() {} + + @Override + public void error() { + UI.sendBarNotification("Failed to download track preview."); + } + }); + try { + download.start().join(); + } catch (InterruptedException e) {} + complete = (download.getStatus() == Download.Status.COMPLETE); } + + // play the track + if (complete) { + try { + AudioInputStream audioIn = AudioSystem.getAudioInputStream(downloadFile); + currentTrack = loadClip(filename, audioIn, isMP3); + playClip(currentTrack, Options.getMusicVolume() * Options.getMasterVolume(), listener); + return true; + } catch (Exception e) { + throw new SlickException(String.format("Failed to load clip '%s'.", url)); + } + } + return false; } /** diff --git a/src/itdelatrisu/opsu/db/ScoreDB.java b/src/itdelatrisu/opsu/db/ScoreDB.java index 34891987..8ae37f8c 100644 --- a/src/itdelatrisu/opsu/db/ScoreDB.java +++ b/src/itdelatrisu/opsu/db/ScoreDB.java @@ -304,6 +304,16 @@ public class ScoreDB { * @return all scores for the beatmap, or null if any error occurred */ public static ScoreData[] getMapScores(Beatmap beatmap) { + return getMapScoresExcluding(beatmap, null); + } + + /** + * Retrieves the game scores for a beatmap while excluding a score. + * @param beatmap the beatmap + * @param exclude the filename (replay string) of the score to exclude + * @return all scores for the beatmap except for exclude, or null if any error occurred + */ + public static ScoreData[] getMapScoresExcluding(Beatmap beatmap, String exclude) { if (connection == null) return null; @@ -317,7 +327,11 @@ public class ScoreDB { ResultSet rs = selectMapStmt.executeQuery(); while (rs.next()) { ScoreData s = new ScoreData(rs); - list.add(s); + if (s.replayString != null && s.replayString.equals(exclude)) { + // don't return this score + } else { + list.add(s); + } } rs.close(); } catch (SQLException e) { diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index 09ebe292..58818490 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -167,12 +167,13 @@ public class Download { /** * Starts the download from the "waiting" status. + * @return the started download thread, or {@code null} if none started */ - public void start() { + public Thread start() { if (status != Status.WAITING) - return; + return null; - new Thread() { + Thread t = new Thread() { @Override public void run() { // open connection @@ -274,7 +275,9 @@ public class Download { listener.error(); } } - }.start(); + }; + t.start(); + return t; } /** diff --git a/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java index ce4647be..1c72dff5 100644 --- a/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java @@ -27,8 +27,9 @@ import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; /** * Download server: http://osu.mengsky.net/ @@ -38,13 +39,10 @@ public class MengSkyServer extends DownloadServer { private static final String SERVER_NAME = "MengSky"; /** Formatted download URL: {@code beatmapSetID} */ - private static final String DOWNLOAD_URL = "http://osu.mengsky.net/d.php?id=%d"; + private static final String DOWNLOAD_URL = "http://osu.mengsky.net/api/download/%d"; - /** Formatted search URL: {@code query} */ - private static final String SEARCH_URL = "http://osu.mengsky.net/index.php?search_keywords=%s"; - - /** Formatted home URL: {@code page} */ - private static final String HOME_URL = "http://osu.mengsky.net/index.php?next=1&page=%d"; + /** Formatted search URL: {@code query,page,unranked,approved,qualified} */ + private static final String SEARCH_URL = "http://osu.mengsky.net/api/beatmapinfo?query=%s&page=%d&ranked=1&unrank=%d&approved=%d&qualified=%d"; /** Maximum beatmaps displayed per page. */ private static final int PAGE_LIMIT = 20; @@ -67,127 +65,41 @@ public class MengSkyServer extends DownloadServer { public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { DownloadNode[] nodes = null; try { - // read HTML - String search; - boolean isSearch; - if (query.isEmpty()) { - isSearch = false; - search = String.format(HOME_URL, page - 1); - } else { - isSearch = true; - search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8")); + // read JSON + int rankedOnlyFlag = rankedOnly ? 0 : 1; + String search = String.format( + SEARCH_URL, URLEncoder.encode(query, "UTF-8"), page, + rankedOnlyFlag, rankedOnlyFlag, rankedOnlyFlag + ); + JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); + + // parse result list + JSONArray arr = json.getJSONArray("data"); + nodes = new DownloadNode[arr.length()]; + for (int i = 0; i < nodes.length; i++) { + JSONObject item = arr.getJSONObject(i); + String + title = item.getString("title"), titleU = item.getString("titleU"), + artist = item.getString("artist"), artistU = item.getString("artistU"), + creator = item.getString("creator"); + // bug with v1.x API (as of 10-13-16): + // sometimes titleU is artistU instead of the proper title + if (titleU.equals(artistU) && !titleU.equals(title)) + titleU = title; + nodes[i] = new DownloadNode( + item.getInt("id"), item.getString("syncedDateTime"), + title, titleU, artist, artistU, creator + ); } - String html = Utils.readDataFromUrl(new URL(search)); - if (html == null) { - this.totalResults = -1; - return null; - } - - // parse results - // NOTE: Maybe an HTML parser would be better for this... - // FORMAT: - //
- //
- // - //
- // Creator: {{creator}}
- // MaxBpm: {{bpm}}
- // Title: {{titleUnicode}}
- // Artist: {{artistUnicode}}
- // Status: {{"Ranked?" || "Unranked"}}
- //
- //

- // Fork: bloodcat
- // UpdateTime: {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}
- // Mode: {{...}} - //
- //
- // Osu.ppy - //
- //
- // DownLoad - //
- //
- List nodeList = new ArrayList(); - final String - START_TAG = "
", - CREATOR_TAG = "Creator: ", TITLE_TAG = "Title: ", ARTIST_TAG = "Artist: ", - TIMESTAMP_TAG = "UpdateTime: ", DOWNLOAD_TAG = "
", - BR_TAG = "
", HREF_TAG = "
n) continue; - j = html.indexOf(HREF_TAG_END, i + 1); - if (j == -1 || j > n) continue; - String beatmap = html.substring(i + NAME_TAG.length(), j); - String[] beatmapTokens = beatmap.split(" - ", 2); - if (beatmapTokens.length < 2) - continue; - String artist = beatmapTokens[0]; - String title = beatmapTokens[1]; - - // find other beatmap details - i = html.indexOf(CREATOR_TAG, j + HREF_TAG_END.length()); - if (i == -1 || i > n) continue; - j = html.indexOf(BR_TAG, i + CREATOR_TAG.length()); - if (j == -1 || j > n) continue; - String creator = html.substring(i + CREATOR_TAG.length(), j); - i = html.indexOf(TITLE_TAG, j + BR_TAG.length()); - if (i == -1 || i > n) continue; - j = html.indexOf(BR_TAG, i + TITLE_TAG.length()); - if (j == -1 || j > n) continue; - String titleUnicode = html.substring(i + TITLE_TAG.length(), j); - i = html.indexOf(ARTIST_TAG, j + BR_TAG.length()); - if (i == -1 || i > n) continue; - j = html.indexOf(BR_TAG, i + ARTIST_TAG.length()); - if (j == -1 || j > n) continue; - String artistUnicode = html.substring(i + ARTIST_TAG.length(), j); - i = html.indexOf(TIMESTAMP_TAG, j + BR_TAG.length()); - if (i == -1 || i >= n) continue; - j = html.indexOf(BR_TAG, i + TIMESTAMP_TAG.length()); - if (j == -1 || j > n) continue; - String date = html.substring(i + TIMESTAMP_TAG.length(), j); - - // find beatmap ID - i = html.indexOf(DOWNLOAD_TAG, j + BR_TAG.length()); - if (i == -1 || i >= n) continue; - i = html.indexOf(HREF_TAG, i + DOWNLOAD_TAG.length()); - if (i == -1 || i > n) continue; - j = html.indexOf('"', i + HREF_TAG.length()); - if (j == -1 || j > n) continue; - String downloadURL = html.substring(i + HREF_TAG.length(), j); - String[] downloadTokens = downloadURL.split("(?=\\d*$)", 2); - if (downloadTokens[1].isEmpty()) continue; - int id; - try { - id = Integer.parseInt(downloadTokens[1]); - } catch (NumberFormatException e) { - continue; - } - - nodeList.add(new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator)); - } - - nodes = nodeList.toArray(new DownloadNode[nodeList.size()]); // store total result count - if (isSearch) - this.totalResults = nodes.length; - else { - int resultCount = nodes.length + (page - 1) * PAGE_LIMIT; - if (divCount == PAGE_LIMIT) - resultCount++; - this.totalResults = resultCount; - } + int pageTotal = json.getInt("pageTotal"); + int resultCount = nodes.length; + if (page == pageTotal) + resultCount = nodes.length + (pageTotal - 1) * PAGE_LIMIT; + else + resultCount = 1 + (pageTotal - 1) * PAGE_LIMIT; + this.totalResults = resultCount; } catch (MalformedURLException | UnsupportedEncodingException e) { ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); } @@ -195,7 +107,7 @@ public class MengSkyServer extends DownloadServer { } @Override - public int minQueryLength() { return 2; } + public int minQueryLength() { return 1; } @Override public int totalResults() { return totalResults; } diff --git a/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java index 95846bfd..3d25459a 100644 --- a/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java @@ -93,6 +93,8 @@ public class YaSOnlineServer extends DownloadServer { */ private String getDownloadURLFromMapData(int beatmapSetID) throws IOException { try { + Utils.setSSLCertValidation(false); + // read JSON String search = String.format(DOWNLOAD_URL, beatmapSetID); JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); @@ -114,6 +116,8 @@ public class YaSOnlineServer extends DownloadServer { } catch (MalformedURLException | UnsupportedEncodingException e) { ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true); return null; + } finally { + Utils.setSSLCertValidation(true); } } @@ -121,6 +125,8 @@ public class YaSOnlineServer extends DownloadServer { public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException { DownloadNode[] nodes = null; try { + Utils.setSSLCertValidation(false); + // read JSON String search; boolean isSearch; @@ -181,6 +187,8 @@ public class YaSOnlineServer extends DownloadServer { this.totalResults = maxServerID; } catch (MalformedURLException | UnsupportedEncodingException e) { ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); + } finally { + Utils.setSSLCertValidation(true); } return nodes; } diff --git a/src/itdelatrisu/opsu/objects/curves/Curve.java b/src/itdelatrisu/opsu/objects/curves/Curve.java index f89f840e..8e75e701 100644 --- a/src/itdelatrisu/opsu/objects/curves/Curve.java +++ b/src/itdelatrisu/opsu/objects/curves/Curve.java @@ -101,12 +101,12 @@ public abstract class Curve { Curve.borderColor = borderColor; ContextCapabilities capabilities = GLContext.getCapabilities(); - mmsliderSupported = capabilities.GL_EXT_framebuffer_object; + mmsliderSupported = capabilities.OpenGL20; if (mmsliderSupported) CurveRenderState.init(width, height, circleDiameter); else { if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) - Log.warn("New slider style requires FBO support."); + Log.warn("New slider style requires OpenGL 2.0."); } } @@ -190,8 +190,8 @@ public abstract class Curve { /** * Discards the slider cache (only used for mmsliders). */ - public void discardCache() { + public void discardGeometry() { if (renderState != null) - renderState.discardCache(); + renderState.discardGeometry(); } } diff --git a/src/itdelatrisu/opsu/render/CurveRenderState.java b/src/itdelatrisu/opsu/render/CurveRenderState.java index c321f384..65deeeab 100644 --- a/src/itdelatrisu/opsu/render/CurveRenderState.java +++ b/src/itdelatrisu/opsu/render/CurveRenderState.java @@ -189,7 +189,7 @@ public class CurveRenderState { /** * Discard the cache mapping for this curve object. */ - public void discardCache() { + public void discardGeometry() { fbo = null; FrameBufferCache.getInstance().freeMappingFor(hitObject); } diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index a9c5b7cf..62f0eff6 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -47,8 +47,6 @@ import itdelatrisu.opsu.ui.UI; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineListener; @@ -663,15 +661,18 @@ public class DownloadsMenu extends BasicGameState { SoundController.stopTrack(); } else { // play preview - try { - final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID())); - MusicController.pause(); - new Thread() { - @Override - public void run() { - try { - previewID = -1; - SoundController.playTrack(url, true, new LineListener() { + final String url = serverMenu.getSelectedItem().getPreviewURL(node.getID()); + MusicController.pause(); + new Thread() { + @Override + public void run() { + try { + previewID = -1; + boolean playing = SoundController.playTrack( + url, + Integer.toString(node.getID()), + true, + new LineListener() { @Override public void update(LineEvent event) { if (event.getType() == LineEvent.Type.STOP) { @@ -681,18 +682,16 @@ public class DownloadsMenu extends BasicGameState { } } } - }); + } + ); + if (playing) previewID = node.getID(); - } catch (SlickException e) { - UI.sendBarNotification("Failed to load track preview. See log for details."); - Log.error(e); - } + } catch (SlickException e) { + UI.sendBarNotification("Failed to load track preview. See log for details."); + Log.error(e); } - }.start(); - } catch (MalformedURLException e) { - UI.sendBarNotification("Could not load track preview (bad URL)."); - Log.error(e); - } + } + }.start(); } return; } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 8af1b09f..b26d1400 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -100,6 +100,12 @@ public class Game extends BasicGameState { /** Maximum rotation, in degrees, over fade out upon death. */ private static final float MAX_ROTATION = 90f; + /** The duration of the score changing animation. */ + private static final float SCOREBOARD_ANIMATION_TIME = 500f; + + /** The time the scoreboard takes to fade in. */ + private static final float SCOREBOARD_FADE_IN_TIME = 300f; + /** Minimum time before start of song, in milliseconds, to process skip-related actions. */ private static final int SKIP_OFFSET = 2000; @@ -265,6 +271,21 @@ public class Game extends BasicGameState { private float epiImgY; private int epiImgTime; + /** The previous scores. */ + private ScoreData[] previousScores; + + /** The current rank in the scores. */ + private int currentRank; + + /** The time the rank was last updated. */ + private int lastRankUpdateTime; + + /** Whether the scoreboard is visible. */ + private boolean scoreboardVisible; + + /** The current alpha of the scoreboard. */ + private float currentScoreboardAlpha; + /** Music position bar background colors. */ private static final Color MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), @@ -603,6 +624,46 @@ public class Game extends BasicGameState { drawHitObjects(g, trackPosition); } + // in-game scoreboard + if (previousScores != null && trackPosition >= firstObjectTime && !GameMod.RELAX.isActive() && !GameMod.AUTOPILOT.isActive()) { + ScoreData currentScore = data.getCurrentScoreData(beatmap, true); + while (currentRank > 0 && previousScores[currentRank - 1].score < currentScore.score) { + currentRank--; + lastRankUpdateTime = trackPosition; + } + + float animation = AnimationEquation.IN_OUT_QUAD.calc( + Utils.clamp((trackPosition - lastRankUpdateTime) / SCOREBOARD_ANIMATION_TIME, 0f, 1f) + ); + int scoreboardPosition = 2 * container.getHeight() / 3; + + if (currentRank < 4) { + // draw the (new) top 5 ranks + for (int i = 0; i < 4; i++) { + int index = i + (i >= currentRank ? 1 : 0); + if (i < previousScores.length) { + float position = index + (i == currentRank ? animation - 3f : -2f); + previousScores[i].drawSmall(g, scoreboardPosition, index + 1, position, data, currentScoreboardAlpha, false); + } + } + currentScore.drawSmall(g, scoreboardPosition, currentRank + 1, currentRank - 1f - animation, data, currentScoreboardAlpha, true); + } else { + // draw the top 2 and next 2 ranks + previousScores[0].drawSmall(g, scoreboardPosition, 1, -2f, data, currentScoreboardAlpha, false); + previousScores[1].drawSmall(g, scoreboardPosition, 2, -1f, data, currentScoreboardAlpha, false); + previousScores[currentRank - 2].drawSmall( + g, scoreboardPosition, currentRank - 1, animation - 1f, data, currentScoreboardAlpha * animation, false + ); + previousScores[currentRank - 1].drawSmall(g, scoreboardPosition, currentRank, animation, data, currentScoreboardAlpha, false); + currentScore.drawSmall(g, scoreboardPosition, currentRank + 1, 2f, data, currentScoreboardAlpha, true); + if (animation < 1.0f && currentRank < previousScores.length) { + previousScores[currentRank].drawSmall( + g, scoreboardPosition, currentRank + 2, 1f + 5 * animation, data, currentScoreboardAlpha * (1f - animation), false + ); + } + } + } + if (!Dancer.hideui && GameMod.AUTO.isActive()) GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); @@ -677,6 +738,7 @@ public class Game extends BasicGameState { if (isReplay || GameMod.AUTO.isActive()) playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY); int trackPosition = MusicController.getPosition(); + int firstObjectTime = beatmap.objects[0].getTime(); // returning from pause screen: must click previous mouse position if (pauseTime > -1) { @@ -782,6 +844,20 @@ public class Game extends BasicGameState { } } + // update in-game scoreboard + if (previousScores != null && trackPosition > firstObjectTime) { + // show scoreboard if selected, and always in break + if (scoreboardVisible || breakTime > 0) { + currentScoreboardAlpha += 1f / SCOREBOARD_FADE_IN_TIME * delta; + if (currentScoreboardAlpha > 1f) + currentScoreboardAlpha = 1f; + } else { + currentScoreboardAlpha -= 1f / SCOREBOARD_FADE_IN_TIME * delta; + if (currentScoreboardAlpha < 0f) + currentScoreboardAlpha = 0f; + } + } + data.updateDisplays(delta); } @@ -1057,6 +1133,9 @@ public class Game extends BasicGameState { case Input.KEY_F12: Utils.takeScreenShot(); break; + case Input.KEY_TAB: + scoreboardVisible = !scoreboardVisible; + break; case Input.KEY_M: if (Dancer.mirror) { mirrorTo = objectIndex; @@ -1407,6 +1486,14 @@ public class Game extends BasicGameState { leadInTime = beatmap.audioLeadIn + approachTime; restart = Restart.FALSE; + // fetch previous scores + previousScores = ScoreDB.getMapScoresExcluding(beatmap, replay == null ? null : replay.getReplayFilename()); + lastRankUpdateTime = -1000; + if (previousScores != null) + currentRank = previousScores.length; + scoreboardVisible = true; + currentScoreboardAlpha = 0f; + // needs to play before setting position to resume without lag later MusicController.play(false); MusicController.setPosition(0); diff --git a/src/itdelatrisu/opsu/ui/Colors.java b/src/itdelatrisu/opsu/ui/Colors.java index 2f79efb9..7f00603a 100644 --- a/src/itdelatrisu/opsu/ui/Colors.java +++ b/src/itdelatrisu/opsu/ui/Colors.java @@ -42,6 +42,7 @@ public class Colors { DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f), RED_HIGHLIGHT = new Color(246, 154, 161), BLUE_HIGHLIGHT = new Color(173, 216, 230), + BLUE_SCOREBOARD = new Color(133, 208, 212), BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f), BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f), BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);