Added "favorites" and "last played" beatmap groups, and more sorts.

- New sorts: by date added, and most played.
- Sorts are moved to a dropdown menu.
- Tabs are now groupings (all songs, last played, favorites).
- Add/remove "favorite" beatmaps in the right-click menu.
- Beatmap database is now updateable like score database (no longer drops/recreates on every update).
- Various bug fixes.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2016-12-22 05:26:09 -05:00
parent 4446487575
commit ed06a8b0ac
12 changed files with 601 additions and 133 deletions

View File

@ -21,7 +21,9 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapWatchService; import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.downloads.DownloadList; import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.downloads.Updater;
@ -145,6 +147,8 @@ public class Container extends AppGameContainer {
SoundController.stopTrack(); SoundController.stopTrack();
// reset BeatmapSetList data // reset BeatmapSetList data
BeatmapGroup.set(BeatmapGroup.ALL);
BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
if (BeatmapSetList.get() != null) if (BeatmapSetList.get() != null)
BeatmapSetList.get().reset(); BeatmapSetList.get().reset();

View File

@ -68,6 +68,18 @@ public class Beatmap implements Comparable<Beatmap> {
/** The star rating. */ /** The star rating. */
public double starRating = -1; public double starRating = -1;
/** The timestamp this beatmap was first loaded. */
public long dateAdded = 0;
/** Whether this beatmap is marked as a "favorite". */
public boolean favorite = false;
/** Total number of times this beatmap has been played. */
public int playCount = 0;
/** The last time this beatmap was played (timestamp). */
public long lastPlayed = 0;
/** /**
* [General] * [General]
*/ */
@ -501,4 +513,12 @@ public class Beatmap implements Comparable<Beatmap> {
String[] rgb = s.split(","); String[] rgb = s.split(",");
this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]))); this.sliderBorder = new Color(new Color(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
} }
/**
* Increments the play counter and last played time.
*/
public void incrementPlayCounter() {
this.playCount++;
this.lastPlayed = System.currentTimeMillis();
}
} }

View File

@ -0,0 +1,179 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.PriorityQueue;
import org.newdawn.slick.Image;
/**
* Beatmap groups.
*/
public enum BeatmapGroup {
/** All beatmaps (no filter). */
ALL (0, "All Songs", null),
/** Most recently played beatmaps. */
RECENT (1, "Last Played", "Your recently played beatmaps will appear in this list!") {
/** Number of elements to show. */
private static final int K = 20;
/** Returns the latest "last played" time in a beatmap set. */
private long lastPlayed(BeatmapSet set) {
long max = 0;
for (Beatmap beatmap : set) {
if (beatmap.lastPlayed > max)
max = beatmap.lastPlayed;
}
return max;
}
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find top K elements
PriorityQueue<BeatmapSetNode> pq = new PriorityQueue<BeatmapSetNode>(K, new Comparator<BeatmapSetNode>() {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
return Long.compare(lastPlayed(v.getBeatmapSet()), lastPlayed(w.getBeatmapSet()));
}
});
for (BeatmapSetNode node : list) {
long timestamp = lastPlayed(node.getBeatmapSet());
if (timestamp == 0)
continue; // skip unplayed beatmaps
if (pq.size() < K || timestamp > lastPlayed(pq.peek().getBeatmapSet())) {
if (pq.size() == K)
pq.poll();
pq.add(node);
}
}
// return as list
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : pq)
filteredList.add(node);
return filteredList;
}
},
/** "Favorite" beatmaps. */
FAVORITE (2, "Favorites", "Right-click a beatmap to add it to your Favorites!") {
@Override
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
// find "favorite" beatmaps
ArrayList<BeatmapSetNode> filteredList = new ArrayList<BeatmapSetNode>();
for (BeatmapSetNode node : list) {
if (node.getBeatmapSet().isFavorite())
filteredList.add(node);
}
return filteredList;
}
};
/** The ID of the group (used for tab positioning). */
private final int id;
/** The name of the group. */
private final String name;
/** The message to display if this list is empty. */
private final String emptyMessage;
/** The tab associated with the group (displayed in Song Menu screen). */
private MenuButton tab;
/** Total number of groups. */
private static final int SIZE = values().length;
/** Array of BeatmapGroup objects in reverse order. */
public static final BeatmapGroup[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current group. */
private static BeatmapGroup currentGroup = ALL;
/**
* Returns the current group.
* @return the current group
*/
public static BeatmapGroup current() { return currentGroup; }
/**
* Sets a new group.
* @param group the new group
*/
public static void set(BeatmapGroup group) { currentGroup = group; }
/**
* Constructor.
* @param id the ID of the group (for tab positioning)
* @param name the group name
* @param emptyMessage the message to display if this list is empty
*/
BeatmapGroup(int id, String name, String emptyMessage) {
this.id = id;
this.name = name;
this.emptyMessage = emptyMessage;
}
/**
* Returns the message to display if this list is empty.
* @return the message, or null if none
*/
public String getEmptyMessage() { return emptyMessage; }
/**
* Returns a filtered list of beatmap set nodes.
* @param list the unfiltered list
* @return the filtered list
*/
public ArrayList<BeatmapSetNode> filter(ArrayList<BeatmapSetNode> list) {
return list;
}
/**
* Initializes the tab.
* @param containerWidth the container width
* @param bottomY the bottom y coordinate
*/
public void init(int containerWidth, float bottomY) {
Image tab = GameImage.MENU_TAB.getImage();
int tabWidth = tab.getWidth();
float buttonX = containerWidth / 2f;
float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
if (tabOffset > tabWidth) { // prevent tabs from being spaced out
tabOffset = tabWidth;
buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
}
this.tab = new MenuButton(tab,
(buttonX + (tabWidth / 2f)) + (id * tabOffset),
bottomY - (tab.getHeight() / 2f)
);
}
/**
* Checks if the coordinates are within the image bounds.
* @param x the x coordinate
* @param y the y coordinate
* @return true if within bounds
*/
public boolean contains(float x, float y) { return tab.contains(x, y); }
/**
* Draws the tab.
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public void draw(boolean selected, boolean isHover) {
UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
}
}

View File

@ -120,6 +120,7 @@ public class BeatmapParser {
// parse directories // parse directories
BeatmapSetNode lastNode = null; BeatmapSetNode lastNode = null;
long timestamp = System.currentTimeMillis();
for (File dir : dirs) { for (File dir : dirs) {
currentDirectoryIndex++; currentDirectoryIndex++;
if (!dir.isDirectory()) if (!dir.isDirectory())
@ -163,6 +164,7 @@ public class BeatmapParser {
// add to parsed beatmap list // add to parsed beatmap list
if (beatmap != null) { if (beatmap != null) {
beatmap.dateAdded = timestamp;
beatmaps.add(beatmap); beatmaps.add(beatmap);
parsedBeatmaps.add(beatmap); parsedBeatmaps.add(beatmap);
} }

View File

@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap; package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod; import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.db.BeatmapDB;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.NumberFormat; import java.text.NumberFormat;
@ -185,4 +186,26 @@ public class BeatmapSet implements Iterable<Beatmap> {
return false; return false;
} }
/**
* Returns whether this beatmap set is a "favorite".
*/
public boolean isFavorite() {
for (Beatmap map : beatmaps) {
if (map.favorite)
return true;
}
return false;
}
/**
* Sets the "favorite" status of this beatmap set.
* @param flag whether this beatmap set should have "favorite" status
*/
public void setFavorite(boolean flag) {
for (Beatmap map : beatmaps) {
map.favorite = flag;
BeatmapDB.updateFavoriteStatus(map);
}
}
} }

View File

@ -54,6 +54,9 @@ public class BeatmapSetList {
/** Total number of beatmaps (i.e. Beatmap objects). */ /** Total number of beatmaps (i.e. Beatmap objects). */
private int mapCount = 0; private int mapCount = 0;
/** List containing all nodes in the current group. */
private ArrayList<BeatmapSetNode> groupNodes;
/** Current list of nodes (subset of parsedNodes, used for searches). */ /** Current list of nodes (subset of parsedNodes, used for searches). */
private ArrayList<BeatmapSetNode> nodes; private ArrayList<BeatmapSetNode> nodes;
@ -97,7 +100,7 @@ public class BeatmapSetList {
* This does not erase any parsed nodes. * This does not erase any parsed nodes.
*/ */
public void reset() { public void reset() {
nodes = parsedNodes; nodes = groupNodes = BeatmapGroup.current().filter(parsedNodes);
expandedIndex = -1; expandedIndex = -1;
expandedStartNode = expandedEndNode = null; expandedStartNode = expandedEndNode = null;
lastQuery = ""; lastQuery = "";
@ -168,6 +171,7 @@ public class BeatmapSetList {
Beatmap beatmap = beatmapSet.get(0); Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index); nodes.remove(index);
parsedNodes.remove(eCur); parsedNodes.remove(eCur);
groupNodes.remove(eCur);
mapCount -= beatmapSet.size(); mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0) if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID); MSIDdb.remove(beatmap.beatmapSetID);
@ -407,7 +411,7 @@ public class BeatmapSetList {
return; return;
// sort the list // sort the list
Collections.sort(nodes, BeatmapSortOrder.getSort().getComparator()); Collections.sort(nodes, BeatmapSortOrder.current().getComparator());
expandedIndex = -1; expandedIndex = -1;
expandedStartNode = expandedEndNode = null; expandedStartNode = expandedEndNode = null;
@ -444,7 +448,7 @@ public class BeatmapSetList {
// if empty query, reset to original list // if empty query, reset to original list
if (query.isEmpty() || terms.isEmpty()) { if (query.isEmpty() || terms.isEmpty()) {
nodes = parsedNodes; nodes = groupNodes;
return true; return true;
} }
@ -472,14 +476,14 @@ public class BeatmapSetList {
String type = condType.remove(); String type = condType.remove();
String operator = condOperator.remove(); String operator = condOperator.remove();
float value = condValue.remove(); float value = condValue.remove();
for (BeatmapSetNode node : parsedNodes) { for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(type, operator, value)) if (node.getBeatmapSet().matches(type, operator, value))
nodes.add(node); nodes.add(node);
} }
} else { } else {
// normal term // normal term
String term = terms.remove(); String term = terms.remove();
for (BeatmapSetNode node : parsedNodes) { for (BeatmapSetNode node : groupNodes) {
if (node.getBeatmapSet().matches(term)) if (node.getBeatmapSet().matches(term))
nodes.add(node); nodes.add(node);
} }

View File

@ -18,28 +18,19 @@
package itdelatrisu.opsu.beatmap; package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import org.newdawn.slick.Image;
/** /**
* Beatmap sorting orders. * Beatmap sorting orders.
*/ */
public enum BeatmapSortOrder { public enum BeatmapSortOrder {
TITLE (0, "Title", new TitleOrder()), TITLE ("Title", new TitleOrder()),
ARTIST (1, "Artist", new ArtistOrder()), ARTIST ("Artist", new ArtistOrder()),
CREATOR (2, "Creator", new CreatorOrder()), CREATOR ("Creator", new CreatorOrder()),
BPM (3, "BPM", new BPMOrder()), BPM ("BPM", new BPMOrder()),
LENGTH (4, "Length", new LengthOrder()); LENGTH ("Length", new LengthOrder()),
DATE ("Date Added", new DateOrder()),
/** The ID of the sort (used for tab positioning). */ PLAYS ("Most Played", new PlayOrder());
private final int id;
/** The name of the sort. */ /** The name of the sort. */
private final String name; private final String name;
@ -47,19 +38,6 @@ public enum BeatmapSortOrder {
/** The comparator for the sort. */ /** The comparator for the sort. */
private final Comparator<BeatmapSetNode> comparator; private final Comparator<BeatmapSetNode> comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab;
/** Total number of sorts. */
private static final int SIZE = values().length;
/** Array of BeatmapSortOrder objects in reverse order. */
public static final BeatmapSortOrder[] VALUES_REVERSED;
static {
VALUES_REVERSED = values();
Collections.reverse(Arrays.asList(VALUES_REVERSED));
}
/** Current sort. */ /** Current sort. */
private static BeatmapSortOrder currentSort = TITLE; private static BeatmapSortOrder currentSort = TITLE;
@ -67,13 +45,13 @@ public enum BeatmapSortOrder {
* Returns the current sort. * Returns the current sort.
* @return the current sort * @return the current sort
*/ */
public static BeatmapSortOrder getSort() { return currentSort; } public static BeatmapSortOrder current() { return currentSort; }
/** /**
* Sets a new sort. * Sets a new sort.
* @param sort the new sort * @param sort the new sort
*/ */
public static void setSort(BeatmapSortOrder sort) { BeatmapSortOrder.currentSort = sort; } public static void set(BeatmapSortOrder sort) { currentSort = sort; }
/** /**
* Compares two BeatmapSetNode objects by title. * Compares two BeatmapSetNode objects by title.
@ -135,37 +113,57 @@ public enum BeatmapSortOrder {
} }
} }
/**
* Compares two BeatmapSetNode objects by date added.
* Uses the latest beatmap added in each set for comparison.
*/
private static class DateOrder implements Comparator<BeatmapSetNode> {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
long vMax = 0, wMax = 0;
for (Beatmap beatmap : v.getBeatmapSet()) {
if (beatmap.dateAdded > vMax)
vMax = beatmap.dateAdded;
}
for (Beatmap beatmap : w.getBeatmapSet()) {
if (beatmap.dateAdded > wMax)
wMax = beatmap.dateAdded;
}
return Long.compare(vMax, wMax);
}
}
/**
* Compares two BeatmapSetNode objects by total plays
* (summed across all beatmaps in each set).
*/
private static class PlayOrder implements Comparator<BeatmapSetNode> {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vTotal = 0, wTotal = 0;
for (Beatmap beatmap : v.getBeatmapSet())
vTotal += beatmap.playCount;
for (Beatmap beatmap : w.getBeatmapSet())
wTotal += beatmap.playCount;
return Integer.compare(vTotal, wTotal);
}
}
/** /**
* Constructor. * Constructor.
* @param id the ID of the sort (for tab positioning)
* @param name the sort name * @param name the sort name
* @param comparator the comparator for the sort * @param comparator the comparator for the sort
*/ */
BeatmapSortOrder(int id, String name, Comparator<BeatmapSetNode> comparator) { BeatmapSortOrder(String name, Comparator<BeatmapSetNode> comparator) {
this.id = id;
this.name = name; this.name = name;
this.comparator = comparator; this.comparator = comparator;
} }
/** /**
* Initializes the sort tab. * Returns the sort name.
* @param containerWidth the container width * @return the name
* @param bottomY the bottom y coordinate
*/ */
public void init(int containerWidth, float bottomY) { public String getName() { return name; }
Image tab = GameImage.MENU_TAB.getImage();
int tabWidth = tab.getWidth();
float buttonX = containerWidth / 2f;
float tabOffset = (containerWidth - buttonX - tabWidth) / (SIZE - 1);
if (tabOffset > tabWidth) { // prevent tabs from being spaced out
tabOffset = tabWidth;
buttonX = (containerWidth * 0.99f) - (tabWidth * SIZE);
}
this.tab = new MenuButton(tab,
(buttonX + (tabWidth / 2f)) + (id * tabOffset),
bottomY - (tab.getHeight() / 2f)
);
}
/** /**
* Returns the comparator for the sort. * Returns the comparator for the sort.
@ -173,20 +171,6 @@ public enum BeatmapSortOrder {
*/ */
public Comparator<BeatmapSetNode> getComparator() { return comparator; } public Comparator<BeatmapSetNode> getComparator() { return comparator; }
/** @Override
* Checks if the coordinates are within the image bounds. public String toString() { return name; }
* @param x the x coordinate
* @param y the y coordinate
* @return true if within bounds
*/
public boolean contains(float x, float y) { return tab.contains(x, y); }
/**
* Draws the sort tab.
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public void draw(boolean selected, boolean isHover) {
UI.drawTab(tab.getX(), tab.getY(), name, selected, isHover);
}
} }

View File

@ -30,6 +30,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,8 +43,30 @@ public class BeatmapDB {
/** /**
* Current database version. * Current database version.
* This value should be changed whenever the database format changes. * This value should be changed whenever the database format changes.
* Add any update queries to the {@link #getUpdateQueries(int)} method.
*/ */
private static final String DATABASE_VERSION = "2015-09-02"; private static final int DATABASE_VERSION = 20161222;
/**
* Returns a list of SQL queries to apply, in order, to update from
* the given database version to the latest version.
* @param version the current version
* @return a list of SQL queries
*/
private static List<String> getUpdateQueries(int version) {
List<String> list = new LinkedList<String>();
if (version < 20161222) {
list.add("ALTER TABLE beatmaps ADD COLUMN dateAdded INTEGER");
list.add("ALTER TABLE beatmaps ADD COLUMN favorite BOOLEAN");
list.add("ALTER TABLE beatmaps ADD COLUMN playCount INTEGER");
list.add("ALTER TABLE beatmaps ADD COLUMN lastPlayed INTEGER");
list.add("UPDATE beatmaps SET dateAdded = 0, favorite = 0, playCount = 0, lastPlayed = 0");
}
/* add future updates here */
return list;
}
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */ /** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f; private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@ -58,7 +81,9 @@ public class BeatmapDB {
private static Connection connection; private static Connection connection;
/** Query statements. */ /** Query statements. */
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, setStarsStmt, updateSizeStmt; private static PreparedStatement
insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt,
setStarsStmt, updatePlayStatsStmt, setFavoriteStmt, updateSizeStmt;
/** Current size of beatmap cache table. */ /** Current size of beatmap cache table. */
private static int cacheSize = -1; private static int cacheSize = -1;
@ -75,6 +100,9 @@ public class BeatmapDB {
if (connection == null) if (connection == null)
return; return;
// run any database updates
updateDatabase();
// create the database // create the database
createDatabase(); createDatabase();
@ -88,20 +116,21 @@ public class BeatmapDB {
// retrieve the cache size // retrieve the cache size
getCacheSize(); getCacheSize();
// check the database version
checkVersion();
// prepare sql statements (not used here) // prepare sql statements (not used here)
try { try {
insertStmt = connection.prepareStatement( insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" + "INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?" +
")"
); );
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?"); selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?"); deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?"); deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?"); setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
updatePlayStatsStmt = connection.prepareStatement("UPDATE beatmaps SET playCount = ?, lastPlayed = ? WHERE dir = ? AND file = ?");
setFavoriteStmt = connection.prepareStatement("UPDATE beatmaps SET favorite = ? WHERE dir = ? AND file = ?");
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true); ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
} }
@ -124,7 +153,8 @@ public class BeatmapDB {
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " + "audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " + "mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " + "bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
"md5hash TEXT, stars REAL" + "md5hash TEXT, stars REAL, " +
"dateAdded INTEGER, favorite BOOLEAN, playCount INTEGER, lastPlayed INTEGER" +
"); " + "); " +
"CREATE TABLE IF NOT EXISTS info (" + "CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" + "key TEXT NOT NULL UNIQUE, value TEXT" +
@ -145,29 +175,54 @@ public class BeatmapDB {
} }
/** /**
* Checks the stored table version, clears the beatmap database if different * Applies any database updates by comparing the current version to the
* from the current version, then updates the version field. * stored version. Does nothing if tables have not been created.
*/ */
private static void checkVersion() { private static void updateDatabase() {
try (Statement stmt = connection.createStatement()) { try (Statement stmt = connection.createStatement()) {
// get the stored version int version = 0;
String sql = "SELECT value FROM info WHERE key = 'version'";
ResultSet rs = stmt.executeQuery(sql);
String version = (rs.next()) ? rs.getString(1) : "";
rs.close();
// if different from current version, clear the database // if 'info' table does not exist, assume version 0 and apply all updates
if (!version.equals(DATABASE_VERSION)) { String sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'info'";
clearDatabase(); ResultSet rs = stmt.executeQuery(sql);
boolean infoExists = rs.isBeforeFirst();
rs.close();
if (!infoExists) {
// if 'beatmaps' table also does not exist, databases not yet created
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'beatmaps'";
ResultSet beatmapsRS = stmt.executeQuery(sql);
boolean beatmapsExists = beatmapsRS.isBeforeFirst();
beatmapsRS.close();
if (!beatmapsExists)
return;
} else {
// try to retrieve stored version
sql = "SELECT value FROM info WHERE key = 'version'";
ResultSet versionRS = stmt.executeQuery(sql);
String versionString = (versionRS.next()) ? versionRS.getString(1) : "0";
versionRS.close();
try {
version = Integer.parseInt(versionString);
} catch (NumberFormatException e) {}
}
// database versions match
if (version >= DATABASE_VERSION)
return;
// apply updates
for (String query : getUpdateQueries(version))
stmt.executeUpdate(query);
// update version // update version
if (infoExists) {
PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)"); PreparedStatement ps = connection.prepareStatement("REPLACE INTO info (key, value) VALUES ('version', ?)");
ps.setString(1, DATABASE_VERSION); ps.setString(1, Integer.toString(DATABASE_VERSION));
ps.executeUpdate(); ps.executeUpdate();
ps.close(); ps.close();
} }
} catch (SQLException e) { } catch (SQLException e) {
ErrorHandler.error("Beatmap database version checks failed.", e, true); ErrorHandler.error("Failed to update beatmap database.", e, true);
} }
} }
@ -344,6 +399,10 @@ public class BeatmapDB {
stmt.setString(40, beatmap.comboToString()); stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash); stmt.setString(41, beatmap.md5Hash);
stmt.setDouble(42, beatmap.starRating); stmt.setDouble(42, beatmap.starRating);
stmt.setLong(43, beatmap.dateAdded);
stmt.setBoolean(44, beatmap.favorite);
stmt.setInt(45, beatmap.playCount);
stmt.setLong(46, beatmap.lastPlayed);
} catch (SQLException e) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@ -487,6 +546,10 @@ public class BeatmapDB {
beatmap.sliderBorderFromString(rs.getString(37)); beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = rs.getString(41); beatmap.md5Hash = rs.getString(41);
beatmap.starRating = rs.getDouble(42); beatmap.starRating = rs.getDouble(42);
beatmap.dateAdded = rs.getLong(43);
beatmap.favorite = rs.getBoolean(44);
beatmap.playCount = rs.getInt(45);
beatmap.lastPlayed = rs.getLong(46);
} catch (SQLException e) { } catch (SQLException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
@ -593,6 +656,45 @@ public class BeatmapDB {
} }
} }
/**
* Updates the play statistics for a beatmap in the database.
* @param beatmap the beatmap
*/
public static void updatePlayStatistics(Beatmap beatmap) {
if (connection == null)
return;
try {
updatePlayStatsStmt.setInt(1, beatmap.playCount);
updatePlayStatsStmt.setLong(2, beatmap.lastPlayed);
updatePlayStatsStmt.setString(3, beatmap.getFile().getParentFile().getName());
updatePlayStatsStmt.setString(4, beatmap.getFile().getName());
updatePlayStatsStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error(String.format("Failed to update play statistics for beatmap '%s' in database.",
beatmap.toString()), e, true);
}
}
/**
* Updates the "favorite" status for a beatmap in the database.
* @param beatmap the beatmap
*/
public static void updateFavoriteStatus(Beatmap beatmap) {
if (connection == null)
return;
try {
setFavoriteStmt.setBoolean(1, beatmap.favorite);
setFavoriteStmt.setString(2, beatmap.getFile().getParentFile().getName());
setFavoriteStmt.setString(3, beatmap.getFile().getName());
setFavoriteStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error(String.format("Failed to update favorite status for beatmap '%s' in database.",
beatmap.toString()), e, true);
}
}
/** /**
* Closes the connection to the database. * Closes the connection to the database.
*/ */

View File

@ -46,8 +46,8 @@ import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException; import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame; import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition;
import org.newdawn.slick.state.transition.EmptyTransition; import org.newdawn.slick.state.transition.EmptyTransition;
import org.newdawn.slick.state.transition.FadeInTransition;
/** /**
* Generic button menu state. * Generic button menu state.
@ -69,8 +69,8 @@ public class ButtonMenu extends BasicGameState {
Button.NO.click(container, game); Button.NO.click(container, game);
} }
}, },
/** The initial beatmap management screen. */ /** The initial beatmap management screen (for a non-"favorite" beatmap). */
BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.DELETE, Button.CANCEL }) { BEATMAP (new Button[] { Button.CLEAR_SCORES, Button.FAVORITE_ADD, Button.DELETE, Button.CANCEL }) {
@Override @Override
public String[] getTitle(GameContainer container, StateBasedGame game) { public String[] getTitle(GameContainer container, StateBasedGame game) {
BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode(); BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
@ -90,6 +90,23 @@ public class ButtonMenu extends BasicGameState {
super.scroll(container, game, newValue); super.scroll(container, game, newValue);
} }
}, },
/** The initial beatmap management screen (for a "favorite" beatmap). */
BEATMAP_FAVORITE (new Button[] { Button.CLEAR_SCORES, Button.FAVORITE_REMOVE, Button.DELETE, Button.CANCEL }) {
@Override
public String[] getTitle(GameContainer container, StateBasedGame game) {
return BEATMAP.getTitle(container, game);
}
@Override
public void leave(GameContainer container, StateBasedGame game) {
BEATMAP.leave(container, game);
}
@Override
public void scroll(GameContainer container, StateBasedGame game, int newValue) {
BEATMAP.scroll(container, game, newValue);
}
},
/** The beatmap deletion screen for a beatmap set with multiple beatmaps. */ /** The beatmap deletion screen for a beatmap set with multiple beatmaps. */
BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) { BEATMAP_DELETE_SELECT (new Button[] { Button.DELETE_GROUP, Button.DELETE_SONG, Button.CANCEL_DELETE }) {
@Override @Override
@ -468,6 +485,25 @@ public class ButtonMenu extends BasicGameState {
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition()); game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
} }
}, },
FAVORITE_ADD ("Add to Favorites", Color.blue) {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
node.getBeatmapSet().setFavorite(true);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
}
},
FAVORITE_REMOVE ("Remove from Favorites", Color.blue) {
@Override
public void click(GameContainer container, StateBasedGame game) {
SoundController.playSound(SoundEffect.MENUHIT);
BeatmapSetNode node = ((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).getNode();
node.getBeatmapSet().setFavorite(false);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).doStateActionOnLoad(MenuState.BEATMAP_FAVORITE);
game.enterState(Opsu.STATE_SONGMENU, new EmptyTransition(), new FadeInTransition());
}
},
DELETE ("Delete...", Color.red) { DELETE ("Delete...", Color.red) {
@Override @Override
public void click(GameContainer container, StateBasedGame game) { public void click(GameContainer container, StateBasedGame game) {

View File

@ -1235,6 +1235,12 @@ public class Game extends BasicGameState {
// restart the game // restart the game
if (restart != Restart.FALSE) { if (restart != Restart.FALSE) {
// update play stats
if (restart == Restart.NEW) {
beatmap.incrementPlayCounter();
BeatmapDB.updatePlayStatistics(beatmap);
}
// load mods // load mods
if (isReplay) { if (isReplay) {
previousMods = GameMod.getModState(); previousMods = GameMod.getModState();

View File

@ -32,6 +32,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect; import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap; import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator; import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
import itdelatrisu.opsu.beatmap.BeatmapGroup;
import itdelatrisu.opsu.beatmap.BeatmapParser; import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSet; import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList; import itdelatrisu.opsu.beatmap.BeatmapSetList;
@ -45,6 +46,7 @@ import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState; import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.Colors; import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.DropdownMenu;
import itdelatrisu.opsu.ui.Fonts; import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KineticScrolling; import itdelatrisu.opsu.ui.KineticScrolling;
import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.MenuButton;
@ -315,6 +317,9 @@ public class SongMenu extends BasicGameState {
/** Whether the menu is currently scrolling to the focus node (blocks other actions). */ /** Whether the menu is currently scrolling to the focus node (blocks other actions). */
private boolean isScrollingToFocusNode = false; private boolean isScrollingToFocusNode = false;
/** Sort order dropdown menu. */
private DropdownMenu<BeatmapSortOrder> sortMenu;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@ -351,8 +356,37 @@ public class SongMenu extends BasicGameState {
footerLogoButton.setHoverExpand(1.2f); footerLogoButton.setHoverExpand(1.2f);
// initialize sorts // initialize sorts
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) int sortWidth = (int) (width * 0.12f);
sort.init(width, headerY - SongMenu.DIVIDER_LINE_WIDTH / 2); sortMenu = new DropdownMenu<BeatmapSortOrder>(container, BeatmapSortOrder.values(),
width * 0.87f, headerY - GameImage.MENU_TAB.getImage().getHeight() * 2.25f, sortWidth) {
@Override
public void itemSelected(int index, BeatmapSortOrder item) {
BeatmapSortOrder.set(item);
if (focusNode == null)
return;
BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null;
BeatmapSetList.get().init();
SongMenu.this.setFocus(oldFocusBase, oldFocusFileIndex, true, true);
}
@Override
public boolean menuClicked(int index) {
if (isInputBlocked())
return false;
SoundController.playSound(SoundEffect.MENUCLICK);
return true;
}
};
sortMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
sortMenu.setBorderColor(Colors.BLUE_DIVIDER);
sortMenu.setChevronRightColor(Color.white);
// initialize group tabs
for (BeatmapGroup group : BeatmapGroup.values())
group.init(width, headerY - DIVIDER_LINE_WIDTH / 2);
// initialize score data buttons // initialize score data buttons
ScoreData.init(width, headerY + height * 0.01f); ScoreData.init(width, headerY + height * 0.01f);
@ -428,6 +462,7 @@ public class SongMenu extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
// background // background
if (focusNode != null) { if (focusNode != null) {
@ -499,7 +534,7 @@ public class SongMenu extends BasicGameState {
g.clearClip(); g.clearClip();
// scroll bar // scroll bar
if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY)) if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY) && !inDropdownMenu)
ScoreData.drawScrollbar(g, startScorePos.getPosition(), focusScores.length * ScoreData.getButtonOffset()); ScoreData.drawScrollbar(g, startScorePos.getPosition(), focusScores.length * ScoreData.getButtonOffset());
} }
@ -517,7 +552,7 @@ public class SongMenu extends BasicGameState {
Float position = MusicController.getBeatProgress(); Float position = MusicController.getBeatProgress();
if (position == null) // default to 60bpm if (position == null) // default to 60bpm
position = System.currentTimeMillis() % 1000 / 1000f; position = System.currentTimeMillis() % 1000 / 1000f;
if (footerLogoButton.contains(mouseX, mouseY, 0.25f)) { if (footerLogoButton.contains(mouseX, mouseY, 0.25f) && !inDropdownMenu) {
// hovering over logo: stop pulsing // hovering over logo: stop pulsing
footerLogoButton.draw(); footerLogoButton.draw();
} else { } else {
@ -607,20 +642,22 @@ public class SongMenu extends BasicGameState {
GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY()); GameImage.SELECTION_OTHER_OPTIONS.getImage().drawCentered(selectOptionsButton.getX(), selectOptionsButton.getY());
selectOptionsButton.draw(); selectOptionsButton.draw();
// sorting tabs // group tabs
BeatmapSortOrder currentSort = BeatmapSortOrder.getSort(); BeatmapGroup currentGroup = BeatmapGroup.current();
BeatmapSortOrder hoverSort = null; BeatmapGroup hoverGroup = null;
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) { if (!inDropdownMenu) {
if (sort.contains(mouseX, mouseY)) { for (BeatmapGroup group : BeatmapGroup.values()) {
hoverSort = sort; if (group.contains(mouseX, mouseY)) {
hoverGroup = group;
break; break;
} }
} }
for (BeatmapSortOrder sort : BeatmapSortOrder.VALUES_REVERSED) {
if (sort != currentSort)
sort.draw(false, sort == hoverSort);
} }
currentSort.draw(true, false); for (BeatmapGroup group : BeatmapGroup.VALUES_REVERSED) {
if (group != currentGroup)
group.draw(false, group == hoverGroup);
}
currentGroup.draw(true, false);
// search // search
boolean searchEmpty = search.getText().isEmpty(); boolean searchEmpty = search.getText().isEmpty();
@ -655,6 +692,9 @@ public class SongMenu extends BasicGameState {
(searchResultString == null) ? "Searching..." : searchResultString, Color.white); (searchResultString == null) ? "Searching..." : searchResultString, Color.white);
} }
// sorting options
sortMenu.render(container, g);
// reloading beatmaps // reloading beatmaps
if (reloadThread != null) { if (reloadThread != null) {
// darken the screen // darken the screen
@ -678,15 +718,19 @@ public class SongMenu extends BasicGameState {
if (reloadThread == null) if (reloadThread == null)
MusicController.loopTrackIfEnded(true); MusicController.loopTrackIfEnded(true);
else if (reloadThread.isFinished()) { else if (reloadThread.isFinished()) {
BeatmapGroup.set(BeatmapGroup.ALL);
BeatmapSortOrder.set(BeatmapSortOrder.TITLE);
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
if (BeatmapSetList.get().size() > 0) { if (BeatmapSetList.get().size() > 0) {
// initialize song list // initialize song list
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true); setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else } else
MusicController.playThemeSong(); MusicController.playThemeSong();
reloadThread = null; reloadThread = null;
} }
int mouseX = input.getMouseX(), mouseY = input.getMouseY(); int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = sortMenu.contains(mouseX, mouseY);
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY); UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
selectModsButton.hoverUpdate(delta, mouseX, mouseY); selectModsButton.hoverUpdate(delta, mouseX, mouseY);
selectRandomButton.hoverUpdate(delta, mouseX, mouseY); selectRandomButton.hoverUpdate(delta, mouseX, mouseY);
@ -700,7 +744,9 @@ public class SongMenu extends BasicGameState {
if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) { if (beatmapMenuTimer >= BEATMAP_MENU_DELAY) {
beatmapMenuTimer = -1; beatmapMenuTimer = -1;
if (focusNode != null) { if (focusNode != null) {
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode); MenuState state = focusNode.getBeatmapSet().isFavorite() ?
MenuState.BEATMAP_FAVORITE : MenuState.BEATMAP;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(state, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU); game.enterState(Opsu.STATE_BUTTONMENU);
} }
return; return;
@ -789,7 +835,7 @@ public class SongMenu extends BasicGameState {
// mouse hover // mouse hover
BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY); BeatmapSetNode node = getNodeAtPosition(mouseX, mouseY);
if (node != null) { if (node != null && !inDropdownMenu) {
if (node == hoverIndex) if (node == hoverIndex)
hoverOffset.update(delta); hoverOffset.update(delta);
else { else {
@ -803,7 +849,9 @@ public class SongMenu extends BasicGameState {
} }
// tooltips // tooltips
if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) { if (sortMenu.baseContains(mouseX, mouseY))
UI.updateTooltip(delta, "Sort by...", false);
else if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset()); int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset()); int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS); int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
@ -830,11 +878,6 @@ public class SongMenu extends BasicGameState {
if (isScrollingToFocusNode) if (isScrollingToFocusNode)
return; return;
if (footerLogoButton.contains(x, y, 0.25f)) {
startGame();
return;
}
songScrolling.pressed(); songScrolling.pressed();
startScorePos.pressed(); startScorePos.pressed();
} }
@ -886,24 +929,41 @@ public class SongMenu extends BasicGameState {
return; return;
} }
// group tabs
for (BeatmapGroup group : BeatmapGroup.values()) {
if (group.contains(x, y)) {
if (group != BeatmapGroup.current()) {
BeatmapGroup.set(group);
SoundController.playSound(SoundEffect.MENUCLICK);
startNode = focusNode = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
scoreMap = null;
focusScores = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = 0;
searchResultString = null;
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
if (BeatmapSetList.get().size() < 1 && group.getEmptyMessage() != null)
UI.sendBarNotification(group.getEmptyMessage());
}
return;
}
}
if (focusNode == null) if (focusNode == null)
return; return;
// sorting buttons // logo: start game
for (BeatmapSortOrder sort : BeatmapSortOrder.values()) { if (footerLogoButton.contains(x, y, 0.25f)) {
if (sort.contains(x, y)) { startGame();
if (sort != BeatmapSortOrder.getSort()) {
BeatmapSortOrder.setSort(sort);
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null;
BeatmapSetList.get().init();
setFocus(oldFocusBase, oldFocusFileIndex, true, true);
}
return; return;
} }
}
// song buttons // song buttons
BeatmapSetNode node = getNodeAtPosition(x, y); BeatmapSetNode node = getNodeAtPosition(x, y);
@ -984,6 +1044,7 @@ public class SongMenu extends BasicGameState {
search.setText(""); search.setText("");
searchTimer = SEARCH_DELAY; searchTimer = SEARCH_DELAY;
searchTransitionTimer = 0; searchTransitionTimer = 0;
searchResultString = null;
} else { } else {
// return to main menu // return to main menu
SoundController.playSound(SoundEffect.MENUBACK); SoundController.playSound(SoundEffect.MENUBACK);
@ -1005,7 +1066,11 @@ public class SongMenu extends BasicGameState {
SongNode prev; SongNode prev;
if (randomStack.isEmpty() || (prev = randomStack.pop()) == null) if (randomStack.isEmpty() || (prev = randomStack.pop()) == null)
break; break;
setFocus(prev.getNode(), prev.getIndex(), true, true); BeatmapSetNode node = prev.getNode();
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index == expandedIndex)
node = node.next; // move past base node
setFocus(node, prev.getIndex(), true, true);
} else { } else {
// random track, add previous to stack // random track, add previous to stack
randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex)); randomStack.push(new SongNode(BeatmapSetList.get().getBaseNode(focusNode.index), focusNode.beatmapIndex));
@ -1016,7 +1081,9 @@ public class SongMenu extends BasicGameState {
if (focusNode == null) if (focusNode == null)
break; break;
SoundController.playSound(SoundEffect.MENUHIT); SoundController.playSound(SoundEffect.MENUHIT);
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.BEATMAP, focusNode); MenuState state = focusNode.getBeatmapSet().isFavorite() ?
MenuState.BEATMAP_FAVORITE : MenuState.BEATMAP;
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(state, focusNode);
game.enterState(Opsu.STATE_BUTTONMENU); game.enterState(Opsu.STATE_BUTTONMENU);
break; break;
case Input.KEY_F5: case Input.KEY_F5:
@ -1190,6 +1257,8 @@ public class SongMenu extends BasicGameState {
songChangeTimer.setTime(songChangeTimer.getDuration()); songChangeTimer.setTime(songChangeTimer.getDuration());
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration()); musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
starStream.clear(); starStream.clear();
sortMenu.activate();
sortMenu.reset();
// reset song stack // reset song stack
randomStack = new Stack<SongNode>(); randomStack = new Stack<SongNode>();
@ -1241,6 +1310,15 @@ public class SongMenu extends BasicGameState {
if (GameMod.AUTO.isActive()) if (GameMod.AUTO.isActive())
GameMod.AUTO.toggle(false); GameMod.AUTO.toggle(false);
// re-sort (in case play count updated)
if (BeatmapSortOrder.current() == BeatmapSortOrder.PLAYS) {
BeatmapSetNode oldFocusBase = BeatmapSetList.get().getBaseNode(focusNode.index);
int oldFocusFileIndex = focusNode.beatmapIndex;
focusNode = null;
BeatmapSetList.get().init();
setFocus(oldFocusBase, oldFocusFileIndex, true, true);
}
resetGame = false; resetGame = false;
} }
@ -1325,6 +1403,19 @@ public class SongMenu extends BasicGameState {
case RELOAD: // reload beatmaps case RELOAD: // reload beatmaps
reloadBeatmaps(true); reloadBeatmaps(true);
break; break;
case BEATMAP_FAVORITE: // removed favorite, reset beatmap list
if (BeatmapGroup.current() == BeatmapGroup.FAVORITE) {
startNode = focusNode = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
scoreMap = null;
focusScores = null;
BeatmapSetList.get().reset();
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
}
break;
default: default:
break; break;
} }
@ -1338,6 +1429,7 @@ public class SongMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game) public void leave(GameContainer container, StateBasedGame game)
throws SlickException { throws SlickException {
search.setFocus(false); search.setFocus(false);
sortMenu.deactivate();
} }
/** /**

View File

@ -95,6 +95,9 @@ public class DropdownMenu<E> extends AbstractComponent {
/** The chevron images. */ /** The chevron images. */
private Image chevronDown, chevronRight; private Image chevronDown, chevronRight;
/** Should the next click be blocked? */
private boolean blockClick = false;
/** /**
* Creates a new dropdown menu. * Creates a new dropdown menu.
* @param container the container rendering this menu * @param container the container rendering this menu
@ -327,6 +330,7 @@ public class DropdownMenu<E> extends AbstractComponent {
this.expanded = false; this.expanded = false;
this.lastUpdateTime = 0; this.lastUpdateTime = 0;
expandProgress.setTime(0); expandProgress.setTime(0);
blockClick = false;
} }
@Override @Override
@ -349,9 +353,21 @@ public class DropdownMenu<E> extends AbstractComponent {
this.itemIndex = idx; this.itemIndex = idx;
itemSelected(idx, items[idx]); itemSelected(idx, items[idx]);
} }
blockClick = true;
consumeEvent(); consumeEvent();
} }
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
if (!active)
return;
if (blockClick) {
blockClick = false;
consumeEvent();
}
}
/** /**
* Notification that a new item was selected (via override). * Notification that a new item was selected (via override).
* @param index the index of the item selected * @param index the index of the item selected