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/
/SongPacks/
/Songs/
/Temp/
/.opsu.log
/.opsu.cfg
/.opsu.db*

View File

@ -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

View File

@ -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);

View File

@ -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<HitObjectResult>();
@ -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.

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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<ScoreData> {
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.
*/

View File

@ -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;

View File

@ -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;
}
/**

View File

@ -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) {

View File

@ -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;
}
/**

View File

@ -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:
// <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
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; }

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);