diff --git a/core/src/bms/player/beatoraja/PlayDataAccessor.java b/core/src/bms/player/beatoraja/PlayDataAccessor.java index f4c4429c..c9c54cea 100644 --- a/core/src/bms/player/beatoraja/PlayDataAccessor.java +++ b/core/src/bms/player/beatoraja/PlayDataAccessor.java @@ -5,6 +5,8 @@ import java.nio.file.Paths; import java.security.MessageDigest; import java.util.*; + +import bms.player.beatoraja.select.QueryScoreContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.stream.Stream; @@ -21,6 +23,7 @@ import bms.player.beatoraja.ScoreDatabaseAccessor.ScoreDataCollector; import bms.player.beatoraja.ScoreLogDatabaseAccessor.ScoreLog; import bms.player.beatoraja.ir.LR2IRConnection; +import bms.player.beatoraja.modmenu.SongManagerMenu; import bms.player.beatoraja.song.SongData; import com.badlogic.gdx.utils.Json; @@ -171,6 +174,13 @@ public ScoreData readScoreData(String hash, boolean ln, int lnmode) { return scoredb.getScoreData(hash, ln ? lnmode : 0); } + public ScoreData readScoreData(String hash, boolean ln, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + return readScoreData(hash, ln, ctx.lnMode()); + } + return scoredatalogdb.getBestScoreDataLog(hash, ctx); + } + /** * スコアデータをまとめて読み込み、collectorに渡す * @param collector スコアデータのcollector @@ -181,6 +191,14 @@ public void readScoreDatas(ScoreDataCollector collector, SongData[] songs, int l scoredb.getScoreDatas(collector, songs, lnmode); } + public void readScoreDatas(ScoreDataCollector collector, SongData[] songs, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + readScoreDatas(collector, songs, ctx.lnMode()); + } + scoredatalogdb.getBestScoreDataLogs(collector, songs, ctx); + } + + public List readScoreDatas(String sql) { return scoredb.getScoreDatas(sql); } @@ -285,6 +303,7 @@ public void writeScoreData(ScoreData newscore, BMSModel model, int lnmode, boole newscore.setClearcount(score.getClearcount()); newscore.setScorehash(getScoreHash(newscore)); + SongManagerMenu.invalidCache(newscore.getSha256()); scoredatalogdb.setScoreDataLog(newscore); } @@ -359,6 +378,16 @@ public ScoreData readScoreData(String[] hashes, boolean ln, int lnmode, int opti return readScoreData(hash, ln, lnmode, option, constraint); } + /** + * Load one specific chart's play history + * + * @param hash chart's hash + * @return play records + */ + public List readScoreDataLog(String hash) { + return scoredatalogdb.getScoreDataLog(hash); + } + /** * コーススコアデータを書き込む */ @@ -432,6 +461,19 @@ public void writeScoreData(ScoreData newscore, BMSModel[] models, int lnmode, in log.setDate(score.getDate()); scorelogdb.setScoreLog(log); } + if (log.getSha256() != null && scoredatalogdb != null) { + // TODO: I don't know why newscore doesn't have course's sha256 here, pls kill me + newscore.setSha256(log.getSha256()); + newscore.setTrophy(""); + newscore.setMode(score.getMode()); + newscore.setDate(score.getDate()); + newscore.setPlaycount(score.getPlaycount()); + newscore.setClearcount(score.getClearcount()); + newscore.setScorehash(getScoreHash(newscore)); + + SongManagerMenu.invalidCache(newscore.getSha256()); + scoredatalogdb.setScoreDataLog(newscore); + } logger.info("スコアデータベース更新完了 "); diff --git a/core/src/bms/player/beatoraja/PlayerResource.java b/core/src/bms/player/beatoraja/PlayerResource.java index 06ae3b18..0d795b15 100644 --- a/core/src/bms/player/beatoraja/PlayerResource.java +++ b/core/src/bms/player/beatoraja/PlayerResource.java @@ -30,7 +30,7 @@ */ public final class PlayerResource { private static final Logger logger = LoggerFactory.getLogger(PlayerResource.class); - + /** * 選曲中のBMS */ @@ -132,8 +132,10 @@ public final class PlayerResource { private String tablelevel = ""; private String tablefull; private boolean freqOn; + private int freqValue; private String freqString; private boolean forceNoIRSend; + private int overrideJudgeRank; // Full list of difficult tables that contains current song private List reverseLookup = new ArrayList<>(); @@ -672,4 +674,20 @@ public void setForceNoIRSend(boolean forceNoIRSend) { public Future getAnalysisTask() { return analysisTask; } -} + + public int getFreqValue() { + return freqValue; + } + + public void setFreqValue(int freqValue) { + this.freqValue = freqValue; + } + + public int getOverrideJudgeRank() { + return overrideJudgeRank; + } + + public void setOverrideJudgeRank(int overrideJudgeRank) { + this.overrideJudgeRank = overrideJudgeRank; + } +} \ No newline at end of file diff --git a/core/src/bms/player/beatoraja/ScoreData.java b/core/src/bms/player/beatoraja/ScoreData.java index 7cce7b36..a1b8ada2 100644 --- a/core/src/bms/player/beatoraja/ScoreData.java +++ b/core/src/bms/player/beatoraja/ScoreData.java @@ -119,6 +119,14 @@ public class ScoreData implements Validatable { * プレイゲージ */ private int gauge; + /** + * Rate percentage, e.g. 120 == 1.2x + */ + private int rate; + /** + * Override judgement, -1 as didn't use + */ + private int overridejudge = -1; /** * 入力デバイス */ @@ -236,6 +244,18 @@ public int getLms() { public void setLms(int lms) { this.lms = lms; } + public int getRate() { + return rate; + } + public void setRate(int rate) { + this.rate = rate; + } + public int getOverridejudge() { + return overridejudge; + } + public void setOverridejudge(int overridejudge) { + this.overridejudge = overridejudge; + } public int getJudgeCount(int judge) { return getJudgeCount(judge, true) + getJudgeCount(judge, false); diff --git a/core/src/bms/player/beatoraja/ScoreDataLogDatabaseAccessor.java b/core/src/bms/player/beatoraja/ScoreDataLogDatabaseAccessor.java index ec043ce3..cbfffc02 100644 --- a/core/src/bms/player/beatoraja/ScoreDataLogDatabaseAccessor.java +++ b/core/src/bms/player/beatoraja/ScoreDataLogDatabaseAccessor.java @@ -2,6 +2,9 @@ import java.sql.*; import java.util.*; + +import bms.player.beatoraja.select.QueryScoreContext; +import bms.player.beatoraja.song.SongData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,8 +12,13 @@ import org.apache.commons.dbutils.ResultSetHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import org.sqlite.SQLiteConfig; -import org.sqlite.SQLiteDataSource; import org.sqlite.SQLiteConfig.SynchronousMode; +import org.sqlite.SQLiteDataSource; + +import javax.management.Query; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; /** * スコアデータログデータベースアクセサ @@ -24,9 +32,10 @@ public class ScoreDataLogDatabaseAccessor extends SQLiteDatabaseAccessor { private final ResultSetHandler> scoreHandler = new BeanListHandler(ScoreData.class); private final QueryRunner qr; + private static final int LOAD_CHUNK_SIZE = 1000; public ScoreDataLogDatabaseAccessor(String path) throws ClassNotFoundException { - super( new Table("scoredatalog", + super( new Table("scoredatalog", new Column("sha256", "TEXT", 1, 1), new Column("mode", "INTEGER",0,1), new Column("clear", "INTEGER"), @@ -56,7 +65,40 @@ public ScoreDataLogDatabaseAccessor(String path) throws ClassNotFoundException { new Column("date", "INTEGER"), new Column("state", "INTEGER"), new Column("scorehash", "TEXT") - )); + ), + new Table( "eddatalog", + new Column("sha256", "TEXT", 1, 0), + new Column("mode", "INTEGER"), + new Column("clear", "INTEGER"), + new Column("epg", "INTEGER"), + new Column("lpg", "INTEGER"), + new Column("egr", "INTEGER"), + new Column("lgr", "INTEGER"), + new Column("egd", "INTEGER"), + new Column("lgd", "INTEGER"), + new Column("ebd", "INTEGER"), + new Column("lbd", "INTEGER"), + new Column("epr", "INTEGER"), + new Column("lpr", "INTEGER"), + new Column("ems", "INTEGER"), + new Column("lms", "INTEGER"), + new Column("notes", "INTEGER"), + new Column("combo", "INTEGER"), + new Column("minbp", "INTEGER"), + new Column("avgjudge", "INTEGER", 1, 0, String.valueOf(Integer.MAX_VALUE)), + new Column("playcount", "INTEGER"), + new Column("clearcount", "INTEGER"), + new Column("trophy", "TEXT"), + new Column("ghost", "TEXT"), + new Column("option", "INTEGER"), + new Column("seed", "INTEGER"), + new Column("random", "INTEGER"), + new Column("date", "INTEGER"), + new Column("state", "INTEGER"), + new Column("scorehash", "TEXT"), + new Column("rate", "INTEGER"), + new Column("overridejudge", "INTEGER") + )); Class.forName("org.sqlite.JDBC"); SQLiteConfig conf = new SQLiteConfig(); @@ -83,10 +125,129 @@ public void setScoreDataLog(ScoreData[] scores) { con.setAutoCommit(false); for (ScoreData score : scores) { this.insert(qr, con, "scoredatalog", score); + this.insert(qr, con, "eddatalog", score); } con.commit(); } catch (Exception e) { - logger.error("スコア更新時の例外:" + e.getMessage()); + logger.error("スコア更新時の例外:{}", e.getMessage()); + } + } + + public List getScoreDataLog(String sha256) { + List result = null; + try { + // TODO: One day we shall use prepared statement instead + result = Validatable.removeInvalidElements(qr.query(String.format("SELECT * FROM eddatalog WHERE sha256 = '%s'", sha256), scoreHandler)); + } catch (Exception e) { + logger.error("Failed to query table eddatalog: {}", e.getMessage()); + } + return result; + } + + public List getScoreDataLog(String sha256, QueryScoreContext ctx) { + List result = null; + try (Connection con = qr.getDataSource().getConnection()) { + PreparedStatement ps = con.prepareStatement("SELECT * FROM eddatalog WHERE sha256 = ? AND rate = ? AND overridejudge = ?"); + ps.setString(1, sha256); + ps.setInt(2, ctx.freqValue() != null ? ctx.freqValue() : 0); + ps.setInt(3, ctx.overrideJudge() != null ? ctx.overrideJudge() : -1); + result = Validatable.removeInvalidElements(scoreHandler.handle(ps.executeQuery())); + } catch (Exception e) { + logger.error("Failed to query table eddatalog: {}", e.getMessage()); + } + return result; + } + + /** + * TODO: Maybe we should make the definition of "best" programmable? + */ + public ScoreData getBestScoreDataLog(String sha256, QueryScoreContext ctx) { + List rawLogs = getScoreDataLog(sha256, ctx); + if (rawLogs.isEmpty()) { + return null; + } + ScoreData result = rawLogs.get(0); + for (ScoreData score : rawLogs) { + if (score.getClear() > result.getClear()) { + result = score; + } else if (score.getClear() == result.getClear() && score.getExscore() > result.getExscore()) { + result = score; + } + } + return result; + } + + public void getBestScoreDataLogs(ScoreDatabaseAccessor.ScoreDataCollector collector, SongData[] songs, QueryScoreContext ctx) { + StringBuilder str = new StringBuilder(songs.length * 68); + getBestScoreDataLogs(collector, songs, ctx.lnMode(), str, true, ctx); + str.setLength(0); + getBestScoreDataLogs(collector, songs, 0, str, false, ctx); + } + + public void getBestScoreDataLogs(ScoreDatabaseAccessor.ScoreDataCollector collector, SongData[] songs, int mode, StringBuilder str, boolean hasLN, QueryScoreContext ctx) { + try (Connection con = qr.getDataSource().getConnection()) { + int songLength = songs.length; + int chunkLength = (songLength + LOAD_CHUNK_SIZE - 1) / LOAD_CHUNK_SIZE; + List scores = new ArrayList<>(); + for (int i = 0; i < chunkLength;++i) { + // [i * CHUNK_SIZE, min(length, (i + 1) * CHUNK_SIZE) + final int chunkStart = i * LOAD_CHUNK_SIZE; + final int chunkEnd = Math.min(songLength, (i + 1) * LOAD_CHUNK_SIZE); + for (int j = chunkStart; j < chunkEnd; ++j) { + SongData song = songs[j]; + if((hasLN && song.hasUndefinedLongNote()) || (!hasLN && !song.hasUndefinedLongNote())) { + if (str.length() > 0) { + str.append(','); + } + str.append('\'').append(song.getSha256()).append('\''); + } + } + + PreparedStatement ps = con.prepareStatement("SELECT * FROM eddatalog WHERE sha256 in (?) AND mode = ? AND rate = ? AND overridejudge = ?"); + ps.setString(1, str.toString()); + ps.setInt(2, mode); + ps.setInt(3, ctx.freqValue() != null ? ctx.freqValue() : 0); + ps.setInt(4, ctx.overrideJudge() != null ? ctx.overrideJudge() : -1); + + List subScores = Validatable.removeInvalidElements(scoreHandler.handle(ps.executeQuery())); + str.setLength(0); + scores.addAll(subScores); + } + scores.sort(Comparator.comparing(ScoreData::getSha256)); + // For every chart, we calculate it's best score + List bestScores = new ArrayList<>(); + for (int i = 0; i bestScore.getClear()) { + bestScore = next; + } else if (next.getClear() == bestScore.getClear() && next.getExscore() > bestScore.getExscore()) { + bestScore = next; + } + j++; + } + bestScores.add(bestScore); + i = j; + } + for(SongData song : songs) { + if((hasLN && song.hasUndefinedLongNote()) || (!hasLN && !song.hasUndefinedLongNote())) { + boolean b = true; + for (ScoreData score : bestScores) { + if(song.getSha256().equals(score.getSha256())) { + collector.collect(song, score); + b = false; + break; + } + } + if(b) { + collector.collect(song, null); + } + } + } + } catch (Exception e) { + logger.error("Failed to query table eddatalog: {}", e.getMessage()); } } } diff --git a/core/src/bms/player/beatoraja/modmenu/SongManagerMenu.java b/core/src/bms/player/beatoraja/modmenu/SongManagerMenu.java index 72e10012..1951f901 100644 --- a/core/src/bms/player/beatoraja/modmenu/SongManagerMenu.java +++ b/core/src/bms/player/beatoraja/modmenu/SongManagerMenu.java @@ -1,16 +1,23 @@ package bms.player.beatoraja.modmenu; +import bms.player.beatoraja.ClearType; +import bms.player.beatoraja.CourseData; import bms.player.beatoraja.ScoreData; import bms.player.beatoraja.select.MusicSelectCommand; import bms.player.beatoraja.select.MusicSelector; +import bms.player.beatoraja.select.bar.GradeBar; import bms.player.beatoraja.select.bar.SongBar; import bms.player.beatoraja.song.SongData; import imgui.ImGui; +import imgui.flag.ImGuiTableFlags; import imgui.flag.ImGuiWindowFlags; import imgui.type.ImBoolean; +import imgui.type.ImInt; +import org.apache.commons.lang3.stream.Streams; import java.text.SimpleDateFormat; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -21,6 +28,18 @@ public class SongManagerMenu { * Current song's reverse lookup result */ private static List currentReverseLookupList = new ArrayList<>(); + /** + * In-game local records cache, could be refactored into a fixed size one in the future + */ + private static Map> localHistoryCache = new HashMap<>(); + /** + * Sort strategy + */ + private static ImInt sortStrategy = new ImInt(0); + /** + * Whether show scores based on current selected mods or not + */ + private static ImBoolean showSelectedModdedScore = new ImBoolean(false); private static ImBoolean LAST_PLAYED_SORT = new ImBoolean(false); private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -28,8 +47,18 @@ public class SongManagerMenu { public static void show(ImBoolean showSongManager) { Optional currentSongData = getCurrentSongData(); Optional currentScoreData = getCurrentScoreData(); + Optional currentCourseData = getCurrentCourseData(); if (ImGui.begin("Song Manager", showSongManager, ImGuiWindowFlags.AlwaysAutoResize)) { String songName = currentSongData.map(SongData::getTitle).orElse(""); + if (currentCourseData.isPresent()) { + songName = currentCourseData.get().getName(); + } + String sha256 = currentSongData.map(SongData::getSha256).orElse(""); + if (currentCourseData.isPresent()) { + sha256 = Arrays.stream(currentCourseData.get().getSong()) + .map(SongData::getSha256) + .collect(Collectors.joining()); + } String lastPlayRecordTime = currentScoreData.map(scoreData -> { Date date = new Date(scoreData.getDate() * 1000L); return simpleDateFormat.format(date); @@ -42,7 +71,7 @@ public static void show(ImBoolean showSongManager) { } if (songName.isEmpty()) { - ImGui.text("Not a selectable song"); + ImGui.text("Not a selectable song or course"); } else { if (ImGui.button("Show Reverse Lookup")) { updateReverseLookupData(currentSongData); @@ -56,6 +85,11 @@ public static void show(ImBoolean showSongManager) { } ImGui.endPopup(); } + ImGui.bulletText("Local History"); + ImGui.combo("sort", sortStrategy, SortStrategy.items); + ImGui.checkbox("Show Selected Mods Scores", showSelectedModdedScore); + List localHistory = loadLocalHistory(sha256); + renderLocalHistoryTable(localHistory); } } ImGui.end(); @@ -65,6 +99,13 @@ public static void injectMusicSelector(MusicSelector musicSelector) { selector = musicSelector; } + /** + * Invalid a chart's local history cache + */ + public static void invalidCache(String sha256) { + localHistoryCache.remove(sha256); + } + /** * Update current reverse lookup result by current song data * @@ -81,6 +122,74 @@ private static void updateReverseLookupData(Optional currentSongData) currentReverseLookupList = getReverseLookupData(); } + /** + * Load one chart's local history, currently it's not an async function because querying sqlite + * is already pretty fast. We can do the refactor later if needed + * Returned scores would be sorted by current sort strategy and filtered by current filtering settings + */ + private static List loadLocalHistory(String sha256) { + List snapshot = localHistoryCache.computeIfAbsent(sha256, s -> selector.main.getPlayDataAccessor().readScoreDataLog(sha256)); + SortStrategy strategy = SortStrategy.valueOf(sortStrategy.get()); + snapshot.sort(strategy.getComparator()); + if (showSelectedModdedScore.get()) { + Optional freqValue = FreqTrainerMenu.isFreqTrainerEnabled() ? Optional.of(FreqTrainerMenu.getFreq()) : Optional.empty(); + Optional overrideJudge = JudgeTrainer.isActive() ? Optional.of(JudgeTrainer.getJudgeRank()) : Optional.empty(); + return snapshot.stream().filter(score -> { + if (freqValue.isPresent() && !freqValue.get().equals(score.getRate())) { + return false; + } + if (overrideJudge.isPresent() && !overrideJudge.get().equals(score.getOverridejudge())) { + return false; + } + return true; + }).toList(); + } else { + return snapshot; + } + } + + /** + * Render local records as a table + * @param localHistory local records + */ + private static void renderLocalHistoryTable(List localHistory) { + if (ImGui.beginTable("Local History", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, 0, ImGui.getTextLineHeight() * 20)) { + ImGui.tableSetupScrollFreeze(0, 1); + ImGui.tableSetupColumn("Clear"); + ImGui.tableSetupColumn("Score"); + ImGui.tableSetupColumn("Freq"); + ImGui.tableSetupColumn("Judge"); + ImGui.tableSetupColumn("Time"); + ImGui.tableHeadersRow(); + for (ScoreData scoreData : localHistory) { + ImGui.tableNextRow(); + ImGui.pushID(scoreData.getDate()); + + ImGui.tableNextColumn(); + ImGui.text(ClearType.getClearTypeByID(scoreData.getClear()).name()); + + ImGui.tableNextColumn(); + ImGui.text("" + scoreData.getExscore()); + + ImGui.tableNextColumn(); + int rate = scoreData.getRate(); + String rateData = rate == 0 ? "/" : String.format("%.02fx", (rate / 100.0f)); + ImGui.text(rateData); + + ImGui.tableNextColumn(); + int overrideJudge = scoreData.getOverridejudge(); + String overrideJudgeDate = overrideJudge == -1 ? "/" : JudgeTrainer.JUDGE_OPTIONS[overrideJudge]; + ImGui.text(overrideJudgeDate); + + ImGui.tableNextColumn(); + ImGui.text(simpleDateFormat.format(new Date(scoreData.getDate() * 1000))); + + ImGui.popID(); + } + ImGui.endTable(); + } + } + private static Optional getCurrentSongData() { if (selector.getSelectedBar() instanceof SongBar) { final SongData sd = ((SongBar) selector.getSelectedBar()).getSongData(); @@ -91,10 +200,23 @@ private static Optional getCurrentSongData() { return Optional.empty(); } + private static Optional getCurrentCourseData() { + if (selector.getSelectedBar() instanceof GradeBar) { + CourseData courseData = ((GradeBar) selector.getSelectedBar()).getCourseData(); + if (courseData != null) { + return Optional.of(courseData); + } + } + return Optional.empty(); + } + private static Optional getCurrentScoreData() { - if (selector.getSelectedBar() instanceof SongBar) { - final ScoreData sd = ((SongBar) selector.getSelectedBar()).getScore(); - return Optional.ofNullable(sd); + if (selector.getSelectedBar() instanceof SongBar songBar) { + return Optional.ofNullable(songBar.getScore()); + } else if (selector.getSelectedBar() instanceof GradeBar gradeBar) { + return Streams.of(gradeBar.getScore(), gradeBar.getMirrorScore(), gradeBar.getRandomScore()) + .filter(Objects::nonNull) + .max(Comparator.comparing(ScoreData::getDate)); } return Optional.empty(); } @@ -110,4 +232,37 @@ public static boolean isLastPlayedSortEnabled() { public static void forceDisableLastPlayedSort() { LAST_PLAYED_SORT.set(false); } + + private enum SortStrategy { + RECORD_TIME("Record Time", (lhs, rhs) -> (int)(rhs.getDate() - lhs.getDate())), + EX_SCORE("EX Score", (lhs, rhs) -> rhs.getExscore() - lhs.getExscore()),; + + private final String name; + private final Comparator comparator; + + SortStrategy(String name, Comparator comparator) { + this.name = name; + this.comparator = comparator; + } + + public String getName() { + return name; + } + + public Comparator getComparator() { + return comparator; + } + + public static SortStrategy valueOf(int i) { + String name = items[i]; + for (SortStrategy value : SortStrategy.values()) { + if (value.getName().equals(name)) { + return value; + } + } + return null; + } + + public static String[] items = Arrays.stream(SortStrategy.values()).map(SortStrategy::getName).toArray(String[]::new); + } } diff --git a/core/src/bms/player/beatoraja/play/BMSPlayer.java b/core/src/bms/player/beatoraja/play/BMSPlayer.java index 66dcd574..25aadf05 100755 --- a/core/src/bms/player/beatoraja/play/BMSPlayer.java +++ b/core/src/bms/player/beatoraja/play/BMSPlayer.java @@ -263,6 +263,7 @@ else if(resource.getChartOption() != null) { // "Persist" some states in resource resource.setFreqOn(true); + resource.setFreqValue(freq); resource.setFreqString(FreqTrainerMenu.getFreqString()); } if (autoplay.mode == BMSPlayerMode.Mode.PLAY || autoplay.mode == BMSPlayerMode.Mode.AUTOPLAY) { @@ -291,7 +292,10 @@ else if(resource.getChartOption() != null) { assist = Math.max(assist, 2); score = false; } + resource.setOverrideJudgeRank(JudgeTrainer.getJudgeRank()); model.setJudgerank(overridingJudgeWindowRate); + } else { + resource.setOverrideJudgeRank(-1); } // Constant considered as assist in Endless Dream @@ -1022,6 +1026,10 @@ public ScoreData createScoreData() { } } } + if (resource.isFreqOn()) { + score.setRate(resource.getFreqValue()); + } + score.setOverridejudge(resource.getOverrideJudgeRank()); score.setClear(clear.id); score.setGauge(gauge.isTypeChanged() ? -1 : gauge.getType()); score.setOption(playinfo.randomoption + (model.getMode().player == 2 diff --git a/core/src/bms/player/beatoraja/select/BarManager.java b/core/src/bms/player/beatoraja/select/BarManager.java index 9e0a752f..3784c196 100644 --- a/core/src/bms/player/beatoraja/select/BarManager.java +++ b/core/src/bms/player/beatoraja/select/BarManager.java @@ -6,6 +6,9 @@ import java.lang.reflect.Method; import java.nio.file.*; import java.util.*; + +import bms.player.beatoraja.modmenu.FreqTrainerMenu; +import bms.player.beatoraja.modmenu.JudgeTrainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.stream.IntStream; @@ -374,8 +377,9 @@ public boolean updateBar(Bar bar) { for (Bar b : newcurrentsongs) { if (b instanceof SongBar) { SongData sd = ((SongBar) b).getSongData(); - if (sd != null && select.getScoreDataCache().existsScoreDataCache(sd, config.getLnmode())) { - b.setScore(select.getScoreDataCache().readScoreData(sd, config.getLnmode())); + QueryScoreContext ctx = QueryScoreContext.create(config.getLnmode()); + if (sd != null && select.getScoreDataCache().existsScoreDataCache(sd, ctx)) { + b.setScore(select.getScoreDataCache().readScoreData(sd, ctx)); } } } @@ -399,7 +403,7 @@ public boolean updateBar(Bar bar) { if (randomFolder.getFilter() != null) { Set filterKey = randomFolder.getFilter().keySet(); randomTargets = Stream.of(randomTargets).filter(r -> { - ScoreData scoreData = select.getScoreDataCache().readScoreData(r, config.getLnmode()); + ScoreData scoreData = select.getScoreDataCache().readScoreData(r, QueryScoreContext.create(config.getLnmode())); return randomFolder.filterSong(scoreData); }).toArray(SongData[]::new); } @@ -769,7 +773,7 @@ public void run() { if (bar instanceof SongBar && ((SongBar) bar).existsSong()) { SongData sd = ((SongBar) bar).getSongData(); if (bar.getScore() == null) { - bar.setScore(select.getScoreDataCache().readScoreData(sd, config.getLnmode())); + bar.setScore(select.getScoreDataCache().readScoreData(sd, QueryScoreContext.create(config.getLnmode()))); } if (rival != null && bar.getRivalScore() == null) { final ScoreData rivalScore = rival.readScoreData(sd, config.getLnmode()); diff --git a/core/src/bms/player/beatoraja/select/MusicSelector.java b/core/src/bms/player/beatoraja/select/MusicSelector.java index 99b64193..0804a3a2 100644 --- a/core/src/bms/player/beatoraja/select/MusicSelector.java +++ b/core/src/bms/player/beatoraja/select/MusicSelector.java @@ -127,6 +127,16 @@ protected ScoreData readScoreDatasFromSource(SongData song, int lnmode) { protected void readScoreDatasFromSource(ScoreDataCollector collector, SongData[] songs, int lnmode) { pda.readScoreDatas(collector, songs, lnmode); } + + @Override + protected ScoreData readScoreDatasFromSource(SongData song, QueryScoreContext ctx) { + return pda.readScoreData(song.getSha256(), song.hasUndefinedLongNote(), ctx); + } + + @Override + protected void readScoresDatasFromSource(ScoreDataCollector collector, SongData[] songs, QueryScoreContext ctx) { + pda.readScoreDatas(collector, songs, ctx); + } }; bar = new BarRenderer(this, manager); diff --git a/core/src/bms/player/beatoraja/select/QueryScoreContext.java b/core/src/bms/player/beatoraja/select/QueryScoreContext.java new file mode 100644 index 00000000..5aa1b922 --- /dev/null +++ b/core/src/bms/player/beatoraja/select/QueryScoreContext.java @@ -0,0 +1,27 @@ +package bms.player.beatoraja.select; + +import bms.player.beatoraja.modmenu.FreqTrainerMenu; +import bms.player.beatoraja.modmenu.JudgeTrainer; + +/** + * Wrapper of parameters when querying the score data + * @implNote primitive fields are must be provided, boxed fields are not. + */ +public record QueryScoreContext(int lnMode, Integer freqValue, Integer overrideJudge) { + public boolean isQueryModdedScore() { + return freqValue != null || overrideJudge != null; + } + + public static QueryScoreContext create(int lnMode) { + Integer freqValue = FreqTrainerMenu.isFreqTrainerEnabled() ? FreqTrainerMenu.getFreq() : null; + Integer overrideJudge = JudgeTrainer.isActive() ? JudgeTrainer.getJudgeRank() : null; + return new QueryScoreContext(lnMode, freqValue, overrideJudge); + } + + @Override + public int hashCode() { + // lnMode \in [0, 3), freqValue \in [0, 200), overrideJudge \in [0, 5) + // So we can simply "concat" them as the hash code + return freqValue * 100 + lnMode * 10 + overrideJudge; + } +} diff --git a/core/src/bms/player/beatoraja/select/ScoreDataCache.java b/core/src/bms/player/beatoraja/select/ScoreDataCache.java index fefb9d03..dcdcb447 100644 --- a/core/src/bms/player/beatoraja/select/ScoreDataCache.java +++ b/core/src/bms/player/beatoraja/select/ScoreDataCache.java @@ -5,9 +5,15 @@ import bms.player.beatoraja.song.SongData; import com.badlogic.gdx.utils.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + /** * スコアデータのキャッシュ * + * @implNote In an ideal future, we should replace the old query functions with the QueryScoreContext ones * @author exch */ public abstract class ScoreDataCache { @@ -18,12 +24,17 @@ public abstract class ScoreDataCache { * スコアデータのキャッシュ */ private ObjectMap[] scorecache; + /** + * Modded scores' cache + */ + private HashMap> moddedScoreCache; public ScoreDataCache() { scorecache = new ObjectMap[4]; for (int i = 0; i < scorecache.length; i++) { scorecache[i] = new ObjectMap(2000); } + moddedScoreCache = new HashMap<>(); } /** @@ -42,6 +53,23 @@ public ScoreData readScoreData(SongData song, int lnmode) { return score; } + /** + * Query specified one song's best score + */ + public ScoreData readScoreData(SongData song, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + return readScoreData(song, ctx.lnMode()); + } + Optional cachedScore = readFromModdedCache(song.getSha256(), ctx); + if (cachedScore.isPresent()) { + return cachedScore.get(); + } + ScoreData score = readScoreDatasFromSource(song, ctx); + moddedScoreCache.putIfAbsent(ctx, new ObjectMap<>()); + moddedScoreCache.get(ctx).put(song.getSha256(), score); + return score; + } + /** * * @param collector @@ -78,11 +106,43 @@ public void readScoreDatas(ScoreDataCollector collector, SongData[] songs, int l readScoreDatasFromSource(cachecollector, noscores, lnmode); } + public void readScoreDatas(ScoreDataCollector collector, SongData[] songs, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + readScoreDatas(collector, songs, ctx.lnMode()); + return ; + } + List lost = new ArrayList<>(); + for (SongData song : songs) { + Optional optScoreData = readFromModdedCache(song.getSha256(), ctx); + if (optScoreData.isPresent()) { + collector.collect(song, optScoreData.get()); + } else { + lost.add(song); + } + } + if (lost.isEmpty()) { + return ; + } + ScoreDataCollector cacheCollector = (song, score) -> { + moddedScoreCache.putIfAbsent(ctx, new ObjectMap<>()); + moddedScoreCache.get(ctx).put(song.getSha256(), score); + collector.collect(song, score); + }; + readScoresDatasFromSource(cacheCollector, lost.toArray(SongData[]::new), ctx); + } + boolean existsScoreDataCache(SongData song, int lnmode) { final int cacheindex = song.hasUndefinedLongNote() ? lnmode : 3; return scorecache[cacheindex].containsKey(song.getSha256()); } + boolean existsScoreDataCache(SongData song, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + return existsScoreDataCache(song, ctx.lnMode()); + } + return readFromModdedCache(song.getSha256(), ctx).isPresent(); + } + public void clear() { for (ObjectMap cache : scorecache) { cache.clear(); @@ -95,7 +155,34 @@ public void update(SongData song, int lnmode) { scorecache[cacheindex].put(song.getSha256(), score); } + public void update(SongData song, QueryScoreContext ctx) { + if (!ctx.isQueryModdedScore()) { + update(song, ctx.lnMode()); + return ; + } + moddedScoreCache.putIfAbsent(ctx, new ObjectMap<>()); + ScoreData score = readScoreDatasFromSource(song, ctx); + moddedScoreCache.get(ctx).put(song.getSha256(), score); + } + protected abstract ScoreData readScoreDatasFromSource(SongData songs, int lnmode); protected abstract void readScoreDatasFromSource(ScoreDataCollector collector, SongData[] songs, int lnmode); + + // NOTE: Below two functions are not abstract, and they're default to call the old functions + // This is a compromised way to keep the compatibility with old apis + protected ScoreData readScoreDatasFromSource(SongData song, QueryScoreContext ctx) { + return readScoreDatasFromSource(song, ctx.lnMode()); + } + + protected void readScoresDatasFromSource(ScoreDataCollector collector, SongData[] songs, QueryScoreContext ctx) { + readScoreDatasFromSource(collector, songs, ctx.lnMode()); + } + + private Optional readFromModdedCache(String sha256, QueryScoreContext ctx) { + if (!moddedScoreCache.containsKey(ctx)) { + return Optional.empty(); + } + return Optional.ofNullable(moddedScoreCache.get(ctx).get(sha256)); + } } \ No newline at end of file diff --git a/core/src/bms/player/beatoraja/select/bar/ContextMenuBar.java b/core/src/bms/player/beatoraja/select/bar/ContextMenuBar.java index 238f6537..faca3806 100644 --- a/core/src/bms/player/beatoraja/select/bar/ContextMenuBar.java +++ b/core/src/bms/player/beatoraja/select/bar/ContextMenuBar.java @@ -10,6 +10,7 @@ import bms.player.beatoraja.MainController; import bms.player.beatoraja.TableData; +import bms.player.beatoraja.select.QueryScoreContext; import bms.player.beatoraja.song.SongData; import bms.player.beatoraja.BMSPlayerMode; import bms.player.beatoraja.select.MusicSelector; diff --git a/core/src/bms/player/beatoraja/select/bar/DirectoryBar.java b/core/src/bms/player/beatoraja/select/bar/DirectoryBar.java index b6bfc5e2..d0d4e8fd 100644 --- a/core/src/bms/player/beatoraja/select/bar/DirectoryBar.java +++ b/core/src/bms/player/beatoraja/select/bar/DirectoryBar.java @@ -4,6 +4,7 @@ import bms.model.Mode; import bms.player.beatoraja.ScoreDatabaseAccessor.ScoreDataCollector; import bms.player.beatoraja.select.MusicSelector; +import bms.player.beatoraja.select.QueryScoreContext; import bms.player.beatoraja.song.SongData; import com.badlogic.gdx.utils.Array; @@ -146,6 +147,6 @@ protected void updateFolderStatus(SongData[] songs) { } }; - selector.getScoreDataCache().readScoreDatas(collector, songs, selector.main.getPlayerConfig().getLnmode()); + selector.getScoreDataCache().readScoreDatas(collector, songs, QueryScoreContext.create(selector.main.getPlayerConfig().getLnmode())); } }