Merge branch 'master' of https://github.com/itdelatrisu/opsu into 'upstream'

# Conflicts:
#	README.md
#	src/itdelatrisu/opsu/Opsu.java
#	src/itdelatrisu/opsu/Utils.java
#	src/itdelatrisu/opsu/render/CurveRenderState.java
#	src/itdelatrisu/opsu/states/Game.java
This commit is contained in:
yugecin 2016-12-10 01:19:31 +01:00
commit e878f0fb0d
17 changed files with 412 additions and 206 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/Skins/ /Skins/
/SongPacks/ /SongPacks/
/Songs/ /Songs/
/Temp/
/.opsu.log /.opsu.log
/.opsu.cfg /.opsu.cfg
/.opsu.db* /.opsu.db*

View File

@ -19,6 +19,7 @@
package itdelatrisu.opsu; package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapWatchService; import itdelatrisu.opsu.beatmap.BeatmapWatchService;
@ -61,6 +62,7 @@ public class Container extends AppGameContainer {
public void start() throws SlickException { public void start() throws SlickException {
try { try {
setup(); setup();
ErrorHandler.setGlString();
getDelta(); getDelta();
while (running()) while (running())
gameLoop(); gameLoop();
@ -131,6 +133,9 @@ public class Container extends AppGameContainer {
// prevent loading tracks from re-initializing OpenAL // prevent loading tracks from re-initializing OpenAL
MusicController.reset(); MusicController.reset();
// stop any playing track
SoundController.stopTrack();
// reset BeatmapSetList data // reset BeatmapSetList data
if (BeatmapSetList.get() != null) if (BeatmapSetList.get() != null)
BeatmapSetList.get().reset(); BeatmapSetList.get().reset();
@ -142,6 +147,9 @@ public class Container extends AppGameContainer {
if (!Options.isWatchServiceEnabled()) if (!Options.isWatchServiceEnabled())
BeatmapWatchService.destroy(); BeatmapWatchService.destroy();
BeatmapWatchService.removeListeners(); BeatmapWatchService.removeListeners();
// delete temporary directory
Utils.deleteDirectory(Options.TEMP_DIR);
} }
@Override @Override

View File

@ -33,6 +33,7 @@ import javax.swing.JScrollPane;
import javax.swing.JTextArea; import javax.swing.JTextArea;
import javax.swing.UIManager; import javax.swing.UIManager;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.util.Log; import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader; import org.newdawn.slick.util.ResourceLoader;
@ -73,9 +74,23 @@ public class ErrorHandler {
message = { desc, scroll }, message = { desc, scroll },
messageReport = { descReport, scroll }; messageReport = { descReport, scroll };
/** OpenGL string (if any). */
private static String glString = null;
// This class should not be instantiated. // This class should not be instantiated.
private ErrorHandler() {} 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. * Displays an error popup and logs the given error.
* @param error a description of the error * @param error a description of the error
@ -197,6 +212,11 @@ public class ErrorHandler {
sb.append("**JRE:** "); sb.append("**JRE:** ");
sb.append(System.getProperty("java.version")); sb.append(System.getProperty("java.version"));
sb.append('\n'); sb.append('\n');
if (glString != null) {
sb.append("**OpenGL Version:** ");
sb.append(glString);
sb.append('\n');
}
if (error != null) { if (error != null) {
sb.append("**Error:** `"); sb.append("**Error:** `");
sb.append(error); sb.append(error);

View File

@ -391,7 +391,7 @@ public class GameData {
if (hitResultList != null) { if (hitResultList != null) {
for (HitObjectResult hitResult : hitResultList) { for (HitObjectResult hitResult : hitResultList) {
if (hitResult.curve != null) if (hitResult.curve != null)
hitResult.curve.discardCache(); hitResult.curve.discardGeometry();
} }
} }
hitResultList = new LinkedBlockingDeque<HitObjectResult>(); hitResultList = new LinkedBlockingDeque<HitObjectResult>();
@ -964,7 +964,7 @@ public class GameData {
hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME); hitResult.alpha = 1 - ((float) (trackPosition - hitResult.time) / HITRESULT_FADE_TIME);
} else { } else {
if (hitResult.curve != null) if (hitResult.curve != null)
hitResult.curve.discardCache(); hitResult.curve.discardGeometry();
iter.remove(); iter.remove();
} }
} }
@ -1436,32 +1436,43 @@ public class GameData {
* (i.e. this will not overwrite existing data). * (i.e. this will not overwrite existing data).
* @param beatmap the beatmap * @param beatmap the beatmap
* @return the ScoreData object * @return the ScoreData object
* @see #getCurrentScoreData(Beatmap, boolean)
*/ */
public ScoreData getScoreData(Beatmap beatmap) { public ScoreData getScoreData(Beatmap beatmap) {
if (scoreData != null) if (scoreData == null)
scoreData = getCurrentScoreData(beatmap, false);
return scoreData; return scoreData;
}
scoreData = new ScoreData(); /**
scoreData.timestamp = System.currentTimeMillis() / 1000L; * Returns a ScoreData object encapsulating all current game data.
scoreData.MID = beatmap.beatmapID; * @param beatmap the beatmap
scoreData.MSID = beatmap.beatmapSetID; * @param slidingScore if true, use the display score (might not be actual score)
scoreData.title = beatmap.title; * @return the ScoreData object
scoreData.artist = beatmap.artist; * @see #getScoreData(Beatmap)
scoreData.creator = beatmap.creator; */
scoreData.version = beatmap.version; public ScoreData getCurrentScoreData(Beatmap beatmap, boolean slidingScore) {
scoreData.hit300 = hitResultCount[HIT_300]; ScoreData sd = new ScoreData();
scoreData.hit100 = hitResultCount[HIT_100]; sd.timestamp = System.currentTimeMillis() / 1000L;
scoreData.hit50 = hitResultCount[HIT_50]; sd.MID = beatmap.beatmapID;
scoreData.geki = hitResultCount[HIT_300G]; sd.MSID = beatmap.beatmapSetID;
scoreData.katu = hitResultCount[HIT_300K] + hitResultCount[HIT_100K]; sd.title = beatmap.title;
scoreData.miss = hitResultCount[HIT_MISS]; sd.artist = beatmap.artist;
scoreData.score = score; sd.creator = beatmap.creator;
scoreData.combo = comboMax; sd.version = beatmap.version;
scoreData.perfect = (comboMax == fullObjectCount); sd.hit300 = hitResultCount[HIT_300];
scoreData.mods = GameMod.getModState(); sd.hit100 = hitResultCount[HIT_100];
scoreData.replayString = (replay == null) ? null : replay.getReplayFilename(); sd.hit50 = hitResultCount[HIT_50];
scoreData.playerName = null; // TODO sd.geki = hitResultCount[HIT_300G];
return scoreData; 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;
} }
/** /**

View File

@ -107,10 +107,13 @@ public class Opsu extends StateBasedGame {
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.error(e); Log.error(e);
} }
// set default exception handler
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override @Override
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(Thread t, Throwable e) {
ErrorHandler.error("** Uncaught Exception! **", e, true); ErrorHandler.error("** Uncaught Exception! **", e, true);
System.exit(1);
} }
}); });
@ -124,16 +127,21 @@ public class Opsu extends StateBasedGame {
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// shouldn't happen // shouldn't happen
} catch (IOException e) { } catch (IOException e) {
ErrorHandler.error(String.format( errorAndExit(
null,
String.format(
"opsu! could not be launched for one of these reasons:\n" + "opsu! could not be launched for one of these reasons:\n" +
"- An instance of opsu! is already running.\n" + "- An instance of opsu! is already running.\n" +
"- Another program is bound to port %d. " + "- Another program is bound to port %d. " +
"You can change the port opsu! uses by editing the \"Port\" field in the configuration file.", "You can change the port opsu! uses by editing the \"Port\" field in the configuration file.",
Options.getPort()), null, false); Options.getPort()
System.exit(1); ),
false
);
} }
} }
// load natives
File nativeDir; File nativeDir;
if (!Utils.isJarRunning() && ( if (!Utils.isJarRunning() && (
(nativeDir = new File("./target/natives/")).isDirectory() || (nativeDir = new File("./target/natives/")).isDirectory() ||
@ -166,7 +174,7 @@ public class Opsu extends StateBasedGame {
try { try {
DBController.init(); DBController.init();
} catch (UnsatisfiedLinkError e) { } catch (UnsatisfiedLinkError e) {
errorAndExit(e, "The databases could not be initialized."); errorAndExit(e, "The databases could not be initialized.", true);
} }
// check if just updated // check if just updated
@ -213,7 +221,7 @@ public class Opsu extends StateBasedGame {
} }
} }
} catch (SlickException e) { } 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 { } else {
if (id == STATE_GAME) { if (id == STATE_GAME) {
MusicController.pause(); MusicController.pause();
MusicController.setPitch(1.0f);
MusicController.resume(); MusicController.resume();
} else } else
songMenu.resetTrackOnLoad(); songMenu.resetTrackOnLoad();
@ -278,15 +287,16 @@ public class Opsu extends StateBasedGame {
* Throws an error and exits the application with the given message. * Throws an error and exits the application with the given message.
* @param e the exception that caused the crash * @param e the exception that caused the crash
* @param message the message to display * @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 '!' // JARs will not run properly inside directories containing '!'
// http://bugs.java.com/view_bug.do?bug_id=4523159 // http://bugs.java.com/view_bug.do?bug_id=4523159
if (Utils.isJarRunning() && Utils.getRunningDirectory() != null && if (Utils.isJarRunning() && Utils.getRunningDirectory() != null &&
Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1) 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); ErrorHandler.error("JARs cannot be run from some paths containing '!'. Please move or rename the file and try again.", null, false);
else else
ErrorHandler.error(message, e, true); ErrorHandler.error(message, e, report);
System.exit(1); System.exit(1);
} }
} }

View File

@ -101,6 +101,9 @@ public class Options {
/** Directory where natives are unpacked. */ /** Directory where natives are unpacked. */
public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/"); 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. */ /** Font file name. */
public static final String FONT_NAME = "DroidSansFallback.ttf"; public static final String FONT_NAME = "DroidSansFallback.ttf";

View File

@ -30,6 +30,7 @@ import java.sql.SQLException;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
import org.newdawn.slick.Graphics; import org.newdawn.slick.Graphics;
@ -328,6 +329,69 @@ public class ScoreData implements Comparable<ScoreData> {
c.a = oldAlpha; 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. * Returns the tooltip string for this score.
*/ */

View File

@ -23,7 +23,6 @@ import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject; import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadNode; import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.objects.Circle;
import itdelatrisu.opsu.replay.PlaybackSpeed; import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI; import itdelatrisu.opsu.ui.UI;
@ -44,6 +43,7 @@ import java.net.URL;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
@ -51,6 +51,10 @@ import java.util.Scanner;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import javax.imageio.ImageIO; 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.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -349,7 +353,7 @@ public class Utils {
* deletes the directory itself. * deletes the directory itself.
* @param dir the directory to delete * @param dir the directory to delete
*/ */
private static void deleteDirectory(File dir) { public static void deleteDirectory(File dir) {
if (dir == null || !dir.isDirectory()) if (dir == null || !dir.isDirectory())
return; 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) { public static int getQuadrant(double x, double y) {
if (x < Options.width / 2d) { if (x < Options.width / 2d) {
return y < Options.height / 2d ? 2 : 3; return y < Options.height / 2d ? 2 : 3;

View File

@ -22,6 +22,9 @@ import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options; import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.audio.HitSound.SampleSet; import itdelatrisu.opsu.audio.HitSound.SampleSet;
import itdelatrisu.opsu.beatmap.HitObject; 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.File;
import java.io.IOException; 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. * 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 isMP3 true if MP3, false if WAV
* @param listener the line listener * @param listener the line listener
* @return the MultiClip being played * @return true if playing, false otherwise
* @throws SlickException if any error occurred * @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(); stopTrack();
try {
AudioInputStream audioIn = AudioSystem.getAudioInputStream(url); // download new track
currentTrack = loadClip(url.getFile(), audioIn, isMP3); File dir = Options.TEMP_DIR;
playClip(currentTrack, Options.getMusicVolume() * Options.getMasterVolume(), listener); if (!dir.isDirectory())
return currentTrack; dir.mkdir();
} catch (Exception e) { String filename = String.format("%s.%s", name, isMP3 ? "mp3" : "wav");
throw new SlickException(String.format("Failed to load clip '%s'.", url.getFile(), e)); 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;
} }
/** /**

View File

@ -304,6 +304,16 @@ public class ScoreDB {
* @return all scores for the beatmap, or null if any error occurred * @return all scores for the beatmap, or null if any error occurred
*/ */
public static ScoreData[] getMapScores(Beatmap beatmap) { 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) if (connection == null)
return null; return null;
@ -317,8 +327,12 @@ public class ScoreDB {
ResultSet rs = selectMapStmt.executeQuery(); ResultSet rs = selectMapStmt.executeQuery();
while (rs.next()) { while (rs.next()) {
ScoreData s = new ScoreData(rs); ScoreData s = new ScoreData(rs);
if (s.replayString != null && s.replayString.equals(exclude)) {
// don't return this score
} else {
list.add(s); list.add(s);
} }
}
rs.close(); rs.close();
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Failed to read scores from database.", e, true); ErrorHandler.error("Failed to read scores from database.", e, true);

View File

@ -167,12 +167,13 @@ public class Download {
/** /**
* Starts the download from the "waiting" status. * 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) if (status != Status.WAITING)
return; return null;
new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
// open connection // open connection
@ -274,7 +275,9 @@ public class Download {
listener.error(); listener.error();
} }
} }
}.start(); };
t.start();
return t;
} }
/** /**

View File

@ -27,8 +27,9 @@ import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; 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/ * Download server: http://osu.mengsky.net/
@ -38,13 +39,10 @@ public class MengSkyServer extends DownloadServer {
private static final String SERVER_NAME = "MengSky"; private static final String SERVER_NAME = "MengSky";
/** Formatted download URL: {@code beatmapSetID} */ /** 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} */ /** Formatted search URL: {@code query,page,unranked,approved,qualified} */
private static final String SEARCH_URL = "http://osu.mengsky.net/index.php?search_keywords=%s"; private static final String SEARCH_URL = "http://osu.mengsky.net/api/beatmapinfo?query=%s&page=%d&ranked=1&unrank=%d&approved=%d&qualified=%d";
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "http://osu.mengsky.net/index.php?next=1&page=%d";
/** Maximum beatmaps displayed per page. */ /** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 20; 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 { public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null; DownloadNode[] nodes = null;
try { try {
// read HTML // read JSON
String search; int rankedOnlyFlag = rankedOnly ? 0 : 1;
boolean isSearch; String search = String.format(
if (query.isEmpty()) { SEARCH_URL, URLEncoder.encode(query, "UTF-8"), page,
isSearch = false; rankedOnlyFlag, rankedOnlyFlag, rankedOnlyFlag
search = String.format(HOME_URL, page - 1); );
} else { JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8")); // 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:
// <div class="beatmap" style="{{...}}">
// <div class="preview" style="background-image:url(http://b.ppy.sh/thumb/{{id}}l.jpg)"></div>
// <div class="name"> <a href="">{{artist}} - {{title}}</a> </div>
// <div class="douban_details">
// <span>Creator:</span> {{creator}}<br>
// <span>MaxBpm:</span> {{bpm}}<br>
// <span>Title:</span> {{titleUnicode}}<br>
// <span>Artist:</span> {{artistUnicode}}<br>
// <span>Status:</span> <font color={{"#00CD00" || "#EE0000"}}>{{"Ranked?" || "Unranked"}}</font><br>
// </div>
// <div class="details"> <a href=""></a> <br>
// <span>Fork:</span> bloodcat<br>
// <span>UpdateTime:</span> {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}<br>
// <span>Mode:</span> <img id="{{'s' || 'c' || ...}}" src="/img/{{'s' || 'c' || ...}}.png"> {{...}}
// </div>
// <div class="download">
// <a href="https://osu.ppy.sh/s/{{id}}" class=" btn" target="_blank">Osu.ppy</a>
// </div>
// <div class="download">
// <a href="http://osu.mengsky.net/d.php?id={{id}}" class=" btn" target="_blank">DownLoad</a>
// </div>
// </div>
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String
START_TAG = "<div class=\"beatmap\"", NAME_TAG = "<div class=\"name\"> <a href=\"\">",
CREATOR_TAG = "<span>Creator:</span> ", TITLE_TAG = "<span>Title:</span> ", ARTIST_TAG = "<span>Artist:</span> ",
TIMESTAMP_TAG = "<span>UpdateTime:</span> ", DOWNLOAD_TAG = "<div class=\"download\">",
BR_TAG = "<br>", HREF_TAG = "<a href=\"", HREF_TAG_END = "</a>";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
int divCount = 0;
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
divCount++;
int i, j;
// find beatmap
i = html.indexOf(NAME_TAG, index + START_TAG.length());
if (i == -1 || i > 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 // store total result count
if (isSearch) int pageTotal = json.getInt("pageTotal");
this.totalResults = nodes.length; int resultCount = nodes.length;
else { if (page == pageTotal)
int resultCount = nodes.length + (page - 1) * PAGE_LIMIT; resultCount = nodes.length + (pageTotal - 1) * PAGE_LIMIT;
if (divCount == PAGE_LIMIT) else
resultCount++; resultCount = 1 + (pageTotal - 1) * PAGE_LIMIT;
this.totalResults = resultCount; this.totalResults = resultCount;
}
} catch (MalformedURLException | UnsupportedEncodingException e) { } catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
} }
@ -195,7 +107,7 @@ public class MengSkyServer extends DownloadServer {
} }
@Override @Override
public int minQueryLength() { return 2; } public int minQueryLength() { return 1; }
@Override @Override
public int totalResults() { return totalResults; } public int totalResults() { return totalResults; }

View File

@ -93,6 +93,8 @@ public class YaSOnlineServer extends DownloadServer {
*/ */
private String getDownloadURLFromMapData(int beatmapSetID) throws IOException { private String getDownloadURLFromMapData(int beatmapSetID) throws IOException {
try { try {
Utils.setSSLCertValidation(false);
// read JSON // read JSON
String search = String.format(DOWNLOAD_URL, beatmapSetID); String search = String.format(DOWNLOAD_URL, beatmapSetID);
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search)); JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
@ -114,6 +116,8 @@ public class YaSOnlineServer extends DownloadServer {
} catch (MalformedURLException | UnsupportedEncodingException e) { } catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true); ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true);
return null; 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 { public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null; DownloadNode[] nodes = null;
try { try {
Utils.setSSLCertValidation(false);
// read JSON // read JSON
String search; String search;
boolean isSearch; boolean isSearch;
@ -181,6 +187,8 @@ public class YaSOnlineServer extends DownloadServer {
this.totalResults = maxServerID; this.totalResults = maxServerID;
} catch (MalformedURLException | UnsupportedEncodingException e) { } catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true); ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
} finally {
Utils.setSSLCertValidation(true);
} }
return nodes; return nodes;
} }

View File

@ -101,12 +101,12 @@ public abstract class Curve {
Curve.borderColor = borderColor; Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities(); ContextCapabilities capabilities = GLContext.getCapabilities();
mmsliderSupported = capabilities.GL_EXT_framebuffer_object; mmsliderSupported = capabilities.OpenGL20;
if (mmsliderSupported) if (mmsliderSupported)
CurveRenderState.init(width, height, circleDiameter); CurveRenderState.init(width, height, circleDiameter);
else { else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER) 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). * Discards the slider cache (only used for mmsliders).
*/ */
public void discardCache() { public void discardGeometry() {
if (renderState != null) if (renderState != null)
renderState.discardCache(); renderState.discardGeometry();
} }
} }

View File

@ -47,8 +47,6 @@ import itdelatrisu.opsu.ui.UI;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import javax.sound.sampled.LineEvent; import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener; import javax.sound.sampled.LineListener;
@ -663,15 +661,18 @@ public class DownloadsMenu extends BasicGameState {
SoundController.stopTrack(); SoundController.stopTrack();
} else { } else {
// play preview // play preview
try { final String url = serverMenu.getSelectedItem().getPreviewURL(node.getID());
final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
MusicController.pause(); MusicController.pause();
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
try { try {
previewID = -1; previewID = -1;
SoundController.playTrack(url, true, new LineListener() { boolean playing = SoundController.playTrack(
url,
Integer.toString(node.getID()),
true,
new LineListener() {
@Override @Override
public void update(LineEvent event) { public void update(LineEvent event) {
if (event.getType() == LineEvent.Type.STOP) { if (event.getType() == LineEvent.Type.STOP) {
@ -681,7 +682,9 @@ public class DownloadsMenu extends BasicGameState {
} }
} }
} }
}); }
);
if (playing)
previewID = node.getID(); previewID = node.getID();
} catch (SlickException e) { } catch (SlickException e) {
UI.sendBarNotification("Failed to load track preview. See log for details."); UI.sendBarNotification("Failed to load track preview. See log for details.");
@ -689,10 +692,6 @@ public class DownloadsMenu extends BasicGameState {
} }
} }
}.start(); }.start();
} catch (MalformedURLException e) {
UI.sendBarNotification("Could not load track preview (bad URL).");
Log.error(e);
}
} }
return; return;
} }

View File

@ -100,6 +100,12 @@ public class Game extends BasicGameState {
/** Maximum rotation, in degrees, over fade out upon death. */ /** Maximum rotation, in degrees, over fade out upon death. */
private static final float MAX_ROTATION = 90f; 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. */ /** Minimum time before start of song, in milliseconds, to process skip-related actions. */
private static final int SKIP_OFFSET = 2000; private static final int SKIP_OFFSET = 2000;
@ -265,6 +271,21 @@ public class Game extends BasicGameState {
private float epiImgY; private float epiImgY;
private int epiImgTime; 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. */ /** Music position bar background colors. */
private static final Color private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f), MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
@ -603,6 +624,46 @@ public class Game extends BasicGameState {
drawHitObjects(g, trackPosition); 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()) if (!Dancer.hideui && GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
@ -677,6 +738,7 @@ public class Game extends BasicGameState {
if (isReplay || GameMod.AUTO.isActive()) if (isReplay || GameMod.AUTO.isActive())
playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY); playbackSpeed.getButton().hoverUpdate(delta, mouseX, mouseY);
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
int firstObjectTime = beatmap.objects[0].getTime();
// returning from pause screen: must click previous mouse position // returning from pause screen: must click previous mouse position
if (pauseTime > -1) { 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); data.updateDisplays(delta);
} }
@ -1057,6 +1133,9 @@ public class Game extends BasicGameState {
case Input.KEY_F12: case Input.KEY_F12:
Utils.takeScreenShot(); Utils.takeScreenShot();
break; break;
case Input.KEY_TAB:
scoreboardVisible = !scoreboardVisible;
break;
case Input.KEY_M: case Input.KEY_M:
if (Dancer.mirror) { if (Dancer.mirror) {
mirrorTo = objectIndex; mirrorTo = objectIndex;
@ -1407,6 +1486,14 @@ public class Game extends BasicGameState {
leadInTime = beatmap.audioLeadIn + approachTime; leadInTime = beatmap.audioLeadIn + approachTime;
restart = Restart.FALSE; 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 // needs to play before setting position to resume without lag later
MusicController.play(false); MusicController.play(false);
MusicController.setPosition(0); MusicController.setPosition(0);

View File

@ -42,6 +42,7 @@ public class Colors {
DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f), DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
RED_HIGHLIGHT = new Color(246, 154, 161), RED_HIGHLIGHT = new Color(246, 154, 161),
BLUE_HIGHLIGHT = new Color(173, 216, 230), 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_NORMAL = new Color(0, 0, 0, 0.25f),
BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f), BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f),
BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f); BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);