5 Commits b9574d7f0a ... 12b86aea19

Author SHA1 Message Date
  kiwec 12b86aea19 Save player_id in scores 2 months ago
  kiwec eeb36bfc0b Add tooearly/toolate hitsounds 2 months ago
  kiwec 6eb1edd28e Fix restoreMods() always using Nightcore when speed modifying 2 months ago
  kiwec 4f0d465fe8 Add instafade_sliders convar 2 months ago
  kiwec 16d47b4a38 Revert osu_slider_sliderhead_fadeout 2 months ago

BIN
resources/materials/default/tooearly.wav


BIN
resources/materials/default/toolate.wav


+ 3 - 3
src/App/Osu/Beatmap.cpp

@@ -1796,8 +1796,8 @@ void Beatmap::handlePreviewPlay() {
                 m_bWasSeekFrame = true;
             } else {
                 m_music->setPositionMS_fast(m_selectedDifficulty2->getPreviewTime() < 0
-                                           ? (unsigned long)(m_music->getLengthMS() * 0.40f)
-                                           : m_selectedDifficulty2->getPreviewTime());
+                                                ? (unsigned long)(m_music->getLengthMS() * 0.40f)
+                                                : m_selectedDifficulty2->getPreviewTime());
                 m_bWasSeekFrame = true;
             }
 
@@ -3071,7 +3071,7 @@ void Beatmap::update2() {
 
         // all remaining clicks which have not been consumed by any hitobjects can safely be deleted
         if(m_clicks.size() > 0) {
-            if(osu_play_hitsound_on_click_while_playing.getBool()) osu->getSkin()->playHitCircleSound(0);
+            if(osu_play_hitsound_on_click_while_playing.getBool()) osu->getSkin()->playHitCircleSound(0, 0.f, 0);
 
             // nightmare mod: extra clicks = sliderbreak
             if((osu->getModNightmare() || osu_mod_jigsaw1.getBool()) && !m_bIsInSkippableSection && !m_bInBreak &&

+ 17 - 6
src/App/Osu/Changelog.cpp

@@ -29,17 +29,28 @@ Changelog::Changelog() : ScreenBackable() {
     CHANGELOG latest;
     latest.title =
         UString::format("%.2f (%s, %s)", convar->getConVarByName("osu_version")->getFloat(), __DATE__, __TIME__);
-    latest.changes.push_back("- Added keybind to toggle current map background");
-    latest.changes.push_back("- Fixed beatmaps not getting selected properly in some cases");
-    latest.changes.push_back("- Fixed crash when osu! folder couldn't be found");
-    latest.changes.push_back("- Fixed mod selection not being restored properly");
-    latest.changes.push_back("- Fixed skin selection menu being drawn behind back button");
+    latest.changes.push_back("- Added slider instafade setting");
+    latest.changes.push_back(
+        "- Added \"tooearly.wav\" and \"toolate.wav\" hitsounds, which play when you hit too early or too late (if "
+        "your skin has them)");
+    latest.changes.push_back("- Fixed local scores not saving avatar");
+    latest.changes.push_back("- Fixed Nightcore getting auto-selected instead of Double Time in some cases");
     changelogs.push_back(latest);
 
+    CHANGELOG v35_09;
+    v35_09.title = "35.09 (2024-07-03)";
+    v35_09.changes.push_back("- Added keybind to toggle current map background");
+    v35_09.changes.push_back("- Fixed beatmaps not getting selected properly in some cases");
+    v35_09.changes.push_back("- Fixed crash when osu! folder couldn't be found");
+    v35_09.changes.push_back("- Fixed mod selection not being restored properly");
+    v35_09.changes.push_back("- Fixed skin selection menu being drawn behind back button");
+    changelogs.push_back(v35_09);
+
     CHANGELOG v35_08;
     v35_08.title = "35.08 (2024-06-28)";
     v35_08.changes.push_back("- Added ability to import .osk and .osz files (drop them onto the neosu window)");
-    v35_08.changes.push_back("- Added persistent map database (downloaded or imported maps stay after restarting the game)");
+    v35_08.changes.push_back(
+        "- Added persistent map database (downloaded or imported maps stay after restarting the game)");
     v35_08.changes.push_back("- Added skin folder");
     v35_08.changes.push_back("- Now publishing 32-bit releases (for PCs running Windows 7)");
     v35_08.changes.push_back("- Fixed songs failing to restart");

+ 2 - 1
src/App/Osu/Circle.cpp

@@ -591,7 +591,8 @@ void Circle::onHit(LiveScore::HIT result, long delta, float targetDelta, float t
 
         const Vector2 osuCoords = m_beatmap->pixels2OsuCoords(m_beatmap->osuCoords2Pixels(m_vRawPos));
 
-        m_beatmap->getSkin()->playHitCircleSound(m_iSampleType, GameRules::osuCoords2Pan(osuCoords.x));
+        const long sound_delta = result == LiveScore::HIT::HIT_300 ? 0 : delta;
+        m_beatmap->getSkin()->playHitCircleSound(m_iSampleType, GameRules::osuCoords2Pan(osuCoords.x), sound_delta);
 
         m_fHitAnimation = 0.001f;  // quickfix for 1 frame missing images
         anim->moveQuadOut(&m_fHitAnimation, 1.0f, GameRules::getFadeOutTime(m_beatmap), true);

+ 37 - 42
src/App/Osu/Database.cpp

@@ -264,7 +264,7 @@ class DatabaseLoader : public Resource {
         m_db->loadStars();
 
         // load database
-        m_db->m_beatmapsets.clear(); // TODO @kiwec: this just leaks memory?
+        m_db->m_beatmapsets.clear();  // TODO @kiwec: this just leaks memory?
         std::string osuDbFilePath = osu_folder.getString().toUtf8();
         osuDbFilePath.append("osu!.db");
         Packet db = load_db(osuDbFilePath);
@@ -442,8 +442,6 @@ void Database::deleteScore(MD5Hash beatmapMD5Hash, u64 scoreUnixTimestamp) {
             m_bDidScoresChangeForSave = true;
             m_bDidScoresChangeForStats = true;
 
-            // debugLog("Deleted score for %s at %llu\n", beatmapMD5Hash.c_str(), scoreUnixTimestamp);
-
             break;
         }
     }
@@ -824,6 +822,9 @@ void Database::loadDB(Packet *db) {
 
     m_importTimer->start();
 
+    u32 nb_neosu_maps = 0;
+    u32 nb_peppy_maps = 0;
+
     bool should_read_peppy_database = db->size > 0;
     if(should_read_peppy_database) {
         // read header
@@ -834,25 +835,26 @@ void Database::loadDB(Packet *db) {
         auto playerName = read_stdstring(db);
         m_iNumBeatmapsToLoad = read<u32>(db);
 
-        debugLog("Database: version = %i, folderCount = %i, playerName = %s, numDiffs = %i\n", m_iVersion, m_iFolderCount,
-                 playerName.c_str(), m_iNumBeatmapsToLoad);
+        debugLog("Database: version = %i, folderCount = %i, playerName = %s, numDiffs = %i\n", m_iVersion,
+                 m_iFolderCount, playerName.c_str(), m_iNumBeatmapsToLoad);
 
         if(m_iVersion < 20170222) {
             debugLog("Database: Version is quite old, below 20170222 ...\n");
-            osu->getNotificationOverlay()->addNotification("osu!.db version too old, update osu! and try again!", 0xffff0000);
+            osu->getNotificationOverlay()->addNotification("osu!.db version too old, update osu! and try again!",
+                                                           0xffff0000);
             should_read_peppy_database = false;
         } else if(!osu_database_ignore_version_warnings.getBool()) {
             if(m_iVersion < 20190207) {  // xexxar angles star recalc
-                osu->getNotificationOverlay()->addNotification("osu!.db version is old, let osu! update when convenient.",
-                                                               0xffffff00, false, 3.0f);
+                osu->getNotificationOverlay()->addNotification(
+                    "osu!.db version is old, let osu! update when convenient.", 0xffffff00, false, 3.0f);
             }
         }
 
         // hard cap upper db version
         if(m_iVersion > osu_database_version.getInt() && !osu_database_ignore_version.getBool()) {
             osu->getNotificationOverlay()->addNotification(
-                UString::format("osu!.db version unknown (%i), osu!stable maps will not get loaded.", m_iVersion), 0xffffff00, false,
-                5.0f);
+                UString::format("osu!.db version unknown (%i), osu!stable maps will not get loaded.", m_iVersion),
+                0xffffff00, false, 5.0f);
             should_read_peppy_database = false;
         }
     }
@@ -881,8 +883,8 @@ void Database::loadDB(Packet *db) {
             if(m_iVersion < 20191107)  // see https://osu.ppy.sh/home/changelog/stable40/20191107.2
             {
                 // also see https://github.com/ppy/osu-wiki/commit/b90f312e06b4f86e509b397565f1fe014bb15943
-                // no idea why peppy decided to change the wiki version from 20191107 to 20191106, because that's not what
-                // stable is doing. the correct version is still 20191107
+                // no idea why peppy decided to change the wiki version from 20191107 to 20191106, because that's not
+                // what stable is doing. the correct version is still 20191107
 
                 /*unsigned int size = */ read<u32>(db);  // size in bytes of the beatmap entry
             }
@@ -912,20 +914,17 @@ void Database::loadDB(Packet *db) {
             double sliderMultiplier = read<f64>(db);
 
             unsigned int numOsuStandardStarRatings = read<u32>(db);
-            // debugLog("%i star ratings for osu!standard\n", numOsuStandardStarRatings);
             float numOsuStandardStars = 0.0f;
             for(int s = 0; s < numOsuStandardStarRatings; s++) {
                 read<u8>(db);  // ObjType
                 unsigned int mods = read<u32>(db);
                 read<u8>(db);  // ObjType
                 double starRating = read<f64>(db);
-                // debugLog("%f stars for %u\n", starRating, mods);
 
                 if(mods == 0) numOsuStandardStars = starRating;
             }
 
             unsigned int numTaikoStarRatings = read<u32>(db);
-            // debugLog("%i star ratings for taiko\n", numTaikoStarRatings);
             for(int s = 0; s < numTaikoStarRatings; s++) {
                 read<u8>(db);  // ObjType
                 read<u32>(db);
@@ -934,7 +933,6 @@ void Database::loadDB(Packet *db) {
             }
 
             unsigned int numCtbStarRatings = read<u32>(db);
-            // debugLog("%i star ratings for ctb\n", numCtbStarRatings);
             for(int s = 0; s < numCtbStarRatings; s++) {
                 read<u8>(db);  // ObjType
                 read<u32>(db);
@@ -943,7 +941,6 @@ void Database::loadDB(Packet *db) {
             }
 
             unsigned int numManiaStarRatings = read<u32>(db);
-            // debugLog("%i star ratings for mania\n", numManiaStarRatings);
             for(int s = 0; s < numManiaStarRatings; s++) {
                 read<u8>(db);  // ObjType
                 read<u32>(db);
@@ -956,14 +953,12 @@ void Database::loadDB(Packet *db) {
             duration = duration >= 0 ? duration : 0;      // sanity clamp
             int previewTime = read<u32>(db);
 
-            // debugLog("drainTime = %i sec, duration = %i ms, previewTime = %i ms\n", drainTime, duration, previewTime);
-
             unsigned int numTimingPoints = read<u32>(db);
             zarray<TIMINGPOINT> timingPoints(numTimingPoints);
             read_bytes(db, (u8 *)timingPoints.data(), sizeof(TIMINGPOINT) * numTimingPoints);
 
-            int beatmapID = read<i32>(db);     // fucking bullshit, this is NOT an unsigned integer as is described on the
-                                               // wiki, it can and is -1 sometimes
+            int beatmapID = read<i32>(db);  // fucking bullshit, this is NOT an unsigned integer as is described on the
+                                            // wiki, it can and is -1 sometimes
             int beatmapSetID = read<i32>(db);  // same here
             /*unsigned int threadID = */ read<u32>(db);
 
@@ -971,20 +966,15 @@ void Database::loadDB(Packet *db) {
             /*unsigned char taikoGrade = */ read<u8>(db);
             /*unsigned char ctbGrade = */ read<u8>(db);
             /*unsigned char maniaGrade = */ read<u8>(db);
-            // debugLog("beatmapID = %i, beatmapSetID = %i, threadID = %i, osuStandardGrade = %i, taikoGrade = %i, ctbGrade
-            // = %i, maniaGrade = %i\n", beatmapID, beatmapSetID, threadID, osuStandardGrade, taikoGrade, ctbGrade,
-            // maniaGrade);
 
             short localOffset = read<u16>(db);
             float stackLeniency = read<f32>(db);
             unsigned char mode = read<u8>(db);
-            // debugLog("localOffset = %i, stackLeniency = %f, mode = %i\n", localOffset, stackLeniency, mode);
 
             auto songSource = read_stdstring(db);
             auto songTags = read_stdstring(db);
             trim(&songSource);
             trim(&songTags);
-            // debugLog("songSource = %s, songTags = %s\n", songSource.toUtf8(), songTags.toUtf8());
 
             short onlineOffset = read<u16>(db);
             skip_string(db);  // song title font
@@ -998,9 +988,6 @@ void Database::loadDB(Packet *db) {
             trim(&path);
 
             /*long long lastOnlineCheck = */ read<u64>(db);
-            // debugLog("onlineOffset = %i, songTitleFont = %s, unplayed = %i, lastTimePlayed = %lu, isOsz2 = %i, path = %s,
-            // lastOnlineCheck = %lu\n", onlineOffset, songTitleFont.toUtf8(), (int)unplayed, lastTimePlayed, (int)isOsz2,
-            // path.c_str(), lastOnlineCheck);
 
             /*bool ignoreBeatmapSounds = */ read<u8>(db);
             /*bool ignoreBeatmapSkin = */ read<u8>(db);
@@ -1009,12 +996,9 @@ void Database::loadDB(Packet *db) {
             /*bool visualOverride = */ read<u8>(db);
             /*int lastEditTime = */ read<u32>(db);
             /*unsigned char maniaScrollSpeed = */ read<u8>(db);
-            // debugLog("ignoreBeatmapSounds = %i, ignoreBeatmapSkin = %i, disableStoryboard = %i, disableVideo = %i,
-            // visualOverride = %i, maniaScrollSpeed = %i\n", (int)ignoreBeatmapSounds, (int)ignoreBeatmapSkin,
-            // (int)disableStoryboard, (int)disableVideo, (int)visualOverride, maniaScrollSpeed);
 
-            // HACKHACK: workaround for linux and macos: it can happen that nested beatmaps are stored in the database, and
-            // that osu! stores that filepath with a backslash (because windows)
+            // HACKHACK: workaround for linux and macos: it can happen that nested beatmaps are stored in the database,
+            // and that osu! stores that filepath with a backslash (because windows)
             if(env->getOS() == Environment::OS::LINUX || env->getOS() == Environment::OS::MACOS) {
                 for(int c = 0; c < path.length(); c++) {
                     if(path[c] == '\\') {
@@ -1031,9 +1015,9 @@ void Database::loadDB(Packet *db) {
             fullFilePath.append(osuFileName);
 
             // skip invalid/corrupt entries
-            // the good way would be to check if the .osu file actually exists on disk, but that is slow af, ain't nobody
-            // got time for that so, since I've seen some concrete examples of what happens in such cases, we just exclude
-            // those
+            // the good way would be to check if the .osu file actually exists on disk, but that is slow af, ain't
+            // nobody got time for that so, since I've seen some concrete examples of what happens in such cases, we
+            // just exclude those
             if(artistName.length() < 1 && songTitle.length() < 1 && creatorName.length() < 1 &&
                difficultyName.length() < 1 && md5hash.hash[0] == 0)
                 continue;
@@ -1145,6 +1129,8 @@ void Database::loadDB(Packet *db) {
                 s.diffs2->push_back(diff2);
                 beatmapSets.push_back(s);
             }
+
+            nb_peppy_maps++;
         }
 
         // build beatmap sets
@@ -1202,7 +1188,7 @@ void Database::loadDB(Packet *db) {
             mapset_path.append(std::to_string(set_id));
             mapset_path.append("/");
 
-            std::vector<BeatmapDifficulty*> *diffs = new std::vector<DatabaseBeatmap*>();
+            std::vector<BeatmapDifficulty *> *diffs = new std::vector<DatabaseBeatmap *>();
             for(u16 j = 0; j < nb_diffs; j++) {
                 std::string osu_filename = read_stdstring(&neosu_maps);
 
@@ -1253,6 +1239,7 @@ void Database::loadDB(Packet *db) {
                 }
 
                 diffs->push_back(diff);
+                nb_neosu_maps++;
             }
 
             if(diffs->empty()) {
@@ -1267,11 +1254,11 @@ void Database::loadDB(Packet *db) {
         m_neosu_maps_loaded = true;
     }
 
-    load_collections();
-
     m_importTimer->update();
-    debugLog("Refresh finished, added %i beatmaps in %f seconds.\n", m_beatmapsets.size(),
-             m_importTimer->getElapsedTime());
+    debugLog("peppy+neosu maps: loading took %f seconds (%d peppy, %d neosu, %d maps total)\n",
+             m_importTimer->getElapsedTime(), nb_peppy_maps, nb_neosu_maps, nb_peppy_maps + nb_neosu_maps);
+
+    load_collections();
 
     // signal that we are done
     m_fLoadingProgress = 1.0f;
@@ -1517,6 +1504,10 @@ void Database::loadScores() {
                             sc.server = read_stdstring(&db);
                         }
 
+                        if(sc.version >= 20240704) {
+                            sc.player_id = read<u32>(&db);
+                        }
+
                         sc.experimentalModsConVars = read_stdstring(&db);
 
                         if(gamemode == 0x0 || (dbVersion > 20210103 && sc.version > 20190103)) {
@@ -1760,6 +1751,10 @@ void Database::saveScores() {
                 write_string(&db, score.server.c_str());
             }
 
+            if(score.version >= 20240704) {
+                write<u32>(&db, score.player_id);
+            }
+
             write_string(&db, score.experimentalModsConVars.c_str());
         }
     }

+ 9 - 12
src/App/Osu/ModSelector.cpp

@@ -537,10 +537,9 @@ void ModSelector::draw(Graphics *g) {
             g->pushTransform();
             {
                 g->rotate(90);
-                g->translate(
-                    (int)(experimentalTextHeight / 3.0f +
-                          max(0.0f, experimentalModsAnimationTranslation + m_experimentalContainer->getSize().x)),
-                    (int)(osu->getScreenHeight() / 2 - experimentalTextWidth / 2));
+                g->translate((int)(experimentalTextHeight / 3.0f + max(0.0f, experimentalModsAnimationTranslation +
+                                                                                 m_experimentalContainer->getSize().x)),
+                             (int)(osu->getScreenHeight() / 2 - experimentalTextWidth / 2));
                 g->setColor(0xff777777);
                 g->setAlpha(1.0f - m_fExperimentalAnimation * m_fExperimentalAnimation);
                 g->drawString(experimentalFont, experimentalText);
@@ -551,7 +550,7 @@ void ModSelector::draw(Graphics *g) {
             {
                 g->rotate(90);
                 g->translate((int)(rectHeight + max(0.0f, experimentalModsAnimationTranslation +
-                                                                   m_experimentalContainer->getSize().x)),
+                                                              m_experimentalContainer->getSize().x)),
                              (int)(osu->getScreenHeight() / 2 - rectWidth / 2));
                 g->drawRect(0, 0, rectWidth, rectHeight);
             }
@@ -1214,9 +1213,7 @@ void ModSelector::resetMods() {
     }
 }
 
-u32 ModSelector::getModFlags() {
-    return osu->getScore()->getModsLegacy();
-}
+u32 ModSelector::getModFlags() { return osu->getScore()->getModsLegacy(); }
 
 ModSelection ModSelector::getModSelection() {
     ModSelection selection;
@@ -1240,6 +1237,9 @@ void ModSelector::restoreMods(ModSelection selection) {
     // Reset buttons and sliders to clean state
     resetMods();
 
+    // Legacy mods
+    enableModsFromFlags(selection.flags);
+
     // Override sliders
     for(int i = 0; i < m_overrideSliders.size(); i++) {
         if(m_overrideSliders[i].lock != NULL) {
@@ -1265,10 +1265,7 @@ void ModSelector::restoreMods(ModSelection selection) {
         }
     }
 
-    // Legacy mods
-    enableModsFromFlags(selection.flags);
-
-    // osu->updateMods() is already called by enableModsFromFlags()
+    osu->updateMods();
 }
 
 void ModSelector::enableModsFromFlags(u32 flags) {

+ 2 - 1
src/App/Osu/OptionsMenu.cpp

@@ -835,7 +835,8 @@ OptionsMenu::OptionsMenu() : ScreenBackable() {
     hitResultScaleSlider->setKeyDelta(0.01f);
     addCheckbox("Draw Numbers", convar->getConVarByName("osu_draw_numbers"));
     addCheckbox("Draw Approach Circles", convar->getConVarByName("osu_draw_approach_circles"));
-    addCheckbox("Instafade", convar->getConVarByName("instafade"));
+    addCheckbox("Instafade Circles", convar->getConVarByName("instafade"));
+    addCheckbox("Instafade Sliders", convar->getConVarByName("instafade_sliders"));
     addSpacer();
     addCheckbox("Ignore Beatmap Sample Volume",
                 "Ignore beatmap timingpoint effect volumes.\nQuiet hitsounds can destroy accuracy and concentration, "

+ 11 - 4
src/App/Osu/Osu.cpp

@@ -59,7 +59,7 @@ Osu *osu = NULL;
 
 // release configuration
 ConVar auto_update("auto_update", true, FCVAR_DEFAULT);
-ConVar osu_version("osu_version", 35.09f, FCVAR_DEFAULT | FCVAR_HIDDEN);
+ConVar osu_version("osu_version", 35.10f, FCVAR_DEFAULT | FCVAR_HIDDEN);
 
 #ifdef _DEBUG
 ConVar osu_debug("osu_debug", true, FCVAR_DEFAULT);
@@ -147,6 +147,7 @@ ConVar normalize_loudness("normalize_loudness", false, FCVAR_DEFAULT, "normalize
 ConVar restart_sound_engine_before_playing("restart_sound_engine_before_playing", false, FCVAR_DEFAULT,
                                            "jank fix for users who experience sound issues after playing for a while");
 ConVar instafade("instafade", false, FCVAR_DEFAULT, "don't draw hitcircle fadeout animations");
+ConVar instafade_sliders("instafade_sliders", false, FCVAR_DEFAULT, "don't draw slider fadeout animations");
 ConVar sort_skins_by_name("sort_skins_by_name", true, FCVAR_DEFAULT, "set to false to use old behavior");
 
 ConVar use_https("use_https", true, FCVAR_DEFAULT);
@@ -158,7 +159,6 @@ ConVar submit_after_pause("submit_after_pause", true, FCVAR_DEFAULT);
 
 // If catboy.best doesn't work for you, here are some alternatives:
 // - https://api.osu.direct/d/
-// - https://chimu.moe/d/
 // - https://api.nerinyan.moe/d/
 // - https://osu.gatari.pw/d/
 // - https://osu.sayobot.cn/osu.php?s=
@@ -2028,18 +2028,25 @@ void Osu::onSkinChange(UString oldValue, UString newValue) {
         if(newValue.length() < 1) return;
     }
 
+    if(newValue == UString("default")) {
+        m_skinScheduledToLoad = new Skin(newValue, MCENGINE_DATA_DIR "materials/default/", true);
+        if(m_skin == NULL) m_skin = m_skinScheduledToLoad;
+        m_bSkinLoadScheduled = true;
+        return;
+    }
+
     std::string neosuSkinFolder = MCENGINE_DATA_DIR "skins/";
     neosuSkinFolder.append(newValue.toUtf8());
     neosuSkinFolder.append("/");
     if(env->directoryExists(neosuSkinFolder)) {
-        m_skinScheduledToLoad = new Skin(newValue, neosuSkinFolder, (newValue == UString("default")));
+        m_skinScheduledToLoad = new Skin(newValue, neosuSkinFolder, false);
     } else {
         UString ppySkinFolder = m_osu_folder_ref->getString();
         ppySkinFolder.append(m_osu_folder_sub_skins_ref->getString());
         ppySkinFolder.append(newValue);
         ppySkinFolder.append("/");
         std::string sf = ppySkinFolder.toUtf8();
-        m_skinScheduledToLoad = new Skin(newValue, sf, (newValue == UString("default")));
+        m_skinScheduledToLoad = new Skin(newValue, sf, false);
     }
 
     // initial load

+ 29 - 18
src/App/Osu/Skin.cpp

@@ -2,8 +2,6 @@
 
 #include <string.h>
 
-#include "miniz.h"
-
 #include "Beatmap.h"
 #include "ConVar.h"
 #include "Engine.h"
@@ -17,6 +15,7 @@
 #include "SkinImage.h"
 #include "SoundEngine.h"
 #include "VolumeOverlay.h"
+#include "miniz.h"
 
 using namespace std;
 
@@ -55,7 +54,6 @@ ConVar osu_sound_panning_multiplier("osu_sound_panning_multiplier", 1.0f, FCVAR_
 ConVar osu_ignore_beatmap_combo_colors("osu_ignore_beatmap_combo_colors", false, FCVAR_DEFAULT);
 ConVar osu_ignore_beatmap_sample_volume("osu_ignore_beatmap_sample_volume", false, FCVAR_DEFAULT);
 
-const char *Skin::OSUSKIN_DEFAULT_SKIN_PATH = "";  // set dynamically below in the constructor
 Image *Skin::m_missingTexture = NULL;
 
 ConVar *Skin::m_osu_skin_async = &osu_skin_async;
@@ -64,10 +62,10 @@ ConVar *Skin::m_osu_skin_hd = &osu_skin_hd;
 ConVar *Skin::m_osu_skin_ref = NULL;
 ConVar *Skin::m_osu_mod_fposu_ref = NULL;
 
-void Skin::unpack(const char* filepath) {
+void Skin::unpack(const char *filepath) {
     auto skin_name = env->getFileNameFromFilePath(filepath);
     debugLog("Extracting %s...\n", skin_name.c_str());
-    skin_name.erase(skin_name.size() - 4); // remove .osk extension
+    skin_name.erase(skin_name.size() - 4);  // remove .osk extension
 
     auto skin_root = std::string(MCENGINE_DATA_DIR "skins/");
     skin_root.append(skin_name);
@@ -138,8 +136,6 @@ Skin::Skin(UString name, std::string filepath, bool isDefaultSkin) {
 
     if(m_missingTexture == NULL) m_missingTexture = engine->getResourceManager()->getImage("MISSING_TEXTURE");
 
-    OSUSKIN_DEFAULT_SKIN_PATH = "default/";
-
     // vars
     m_hitCircle = m_missingTexture;
     m_approachCircle = m_missingTexture;
@@ -285,6 +281,9 @@ Skin::Skin(UString name, std::string filepath, bool isDefaultSkin) {
     m_spinnerBonus = NULL;
     m_spinnerSpinSound = NULL;
 
+    m_tooearly = NULL;
+    m_toolate = NULL;
+
     m_combobreak = NULL;
     m_failsound = NULL;
     m_applause = NULL;
@@ -487,15 +486,12 @@ void Skin::load() {
     // skin ini
     randomizeFilePath();
     m_sSkinIniFilePath = m_sFilePath;
-    UString defaultSkinIniFilePath = MCENGINE_DATA_DIR "/materials/";
-    defaultSkinIniFilePath.append(OSUSKIN_DEFAULT_SKIN_PATH);
-    defaultSkinIniFilePath.append("skin.ini");
     m_sSkinIniFilePath.append("skin.ini");
     bool parseSkinIni1Status = true;
     bool parseSkinIni2Status = true;
     if(!parseSkinINI(m_sSkinIniFilePath)) {
         parseSkinIni1Status = false;
-        m_sSkinIniFilePath = defaultSkinIniFilePath;
+        m_sSkinIniFilePath = MCENGINE_DATA_DIR "materials/default/skin.ini";
         parseSkinIni2Status = parseSkinINI(m_sSkinIniFilePath);
     }
 
@@ -925,6 +921,9 @@ void Skin::load() {
     checkLoadSound(&m_spinnerBonus, "spinnerbonus", "OSU_SKIN_SPINNERBONUS_SND", true, true);
     checkLoadSound(&m_spinnerSpinSound, "spinnerspin", "OSU_SKIN_SPINNERSPIN_SND", false, true, true);
 
+    checkLoadSound(&m_tooearly, "tooearly", "OSU_SKIN_TOOEARLY_SND", true, true, false, false, 0.8f);
+    checkLoadSound(&m_toolate, "toolate", "OSU_SKIN_TOOLATE_SND", true, true, false, false, 0.85f);
+
     // others
     checkLoadSound(&m_combobreak, "combobreak", "OSU_SKIN_COMBOBREAK_SND", true, true);
     checkLoadSound(&m_failsound, "failsound", "OSU_SKIN_FAILSOUND_SND");
@@ -1286,7 +1285,7 @@ Color Skin::getComboColorForCounter(int i, int offset) {
 
 void Skin::setBeatmapComboColors(std::vector<Color> colors) { m_beatmapComboColors = colors; }
 
-void Skin::playHitCircleSound(int sampleType, float pan) {
+void Skin::playHitCircleSound(int sampleType, float pan, long delta) {
     if(m_iSampleVolume <= 0) {
         return;
     }
@@ -1298,6 +1297,18 @@ void Skin::playHitCircleSound(int sampleType, float pan) {
         pan *= osu_sound_panning_multiplier.getFloat();
     }
 
+    debugLog("delta: %d\n", delta);
+    if(delta < 0 && m_tooearly != NULL) {
+        debugLog("Too early!\n");
+        engine->getSound()->play(m_tooearly, pan);
+        return;
+    }
+    if(delta > 0 && m_toolate != NULL) {
+        debugLog("Too late!\n");
+        engine->getSound()->play(m_toolate, pan);
+        return;
+    }
+
     int actualSampleSet = m_iSampleSet;
     if(osu_skin_force_hitsound_sample_set.getInt() > 0) actualSampleSet = osu_skin_force_hitsound_sample_set.getInt();
 
@@ -1437,14 +1448,12 @@ void Skin::checkLoadImage(Image **addressOfPointer, std::string skinElementName,
     // NOTE: only the default skin is loaded with a resource name (it must never be unloaded by other instances), and it
     // is NOT added to the resources vector
 
-    std::string defaultFilePath1 = MCENGINE_DATA_DIR "/materials/";
-    defaultFilePath1.append(OSUSKIN_DEFAULT_SKIN_PATH);
+    std::string defaultFilePath1 = MCENGINE_DATA_DIR "materials/default/";
     defaultFilePath1.append(skinElementName);
     defaultFilePath1.append("@2x.");
     defaultFilePath1.append(fileExtension);
 
-    std::string defaultFilePath2 = MCENGINE_DATA_DIR "/materials/";
-    defaultFilePath2.append(OSUSKIN_DEFAULT_SKIN_PATH);
+    std::string defaultFilePath2 = MCENGINE_DATA_DIR "materials/default/";
     defaultFilePath2.append(skinElementName);
     defaultFilePath2.append(".");
     defaultFilePath2.append(fileExtension);
@@ -1580,6 +1589,7 @@ void Skin::checkLoadSound(Sound **addressOfPointer, std::string skinElementName,
 
             std::string path = base_path;
             path.append(fn);
+            debugLog("Loading %s\n", path.c_str());
 
             if(env->fileExists(path)) {
                 if(osu_skin_async.getBool()) {
@@ -1589,13 +1599,14 @@ void Skin::checkLoadSound(Sound **addressOfPointer, std::string skinElementName,
             }
         }
 
+        debugLog("Failed to load %s\n", filename.c_str());
+
         return (Sound *)NULL;
     };
 
     // load default skin
     if(fallback_to_default) {
-        std::string defaultpath = MCENGINE_DATA_DIR "./materials/";
-        defaultpath.append(OSUSKIN_DEFAULT_SKIN_PATH);
+        std::string defaultpath = MCENGINE_DATA_DIR "materials/default/";
         std::string defaultResourceName = resourceName;
         defaultResourceName.append("_DEFAULT");
         *addressOfPointer = try_load_sound(defaultpath, skinElementName, defaultResourceName, loop);

+ 5 - 3
src/App/Osu/Skin.h

@@ -10,8 +10,7 @@ class SkinImage;
 
 class Skin {
    public:
-    static const char *OSUSKIN_DEFAULT_SKIN_PATH;
-    static void unpack(const char* filepath);
+    static void unpack(const char *filepath);
 
     static ConVar *m_osu_skin_async;
     static ConVar *m_osu_skin_hd;
@@ -35,7 +34,7 @@ class Skin {
     void setSampleVolume(float volume, bool force = false);
     void resetSampleVolume();
 
-    void playHitCircleSound(int sampleType, float pan = 0.0f);
+    void playHitCircleSound(int sampleType, float pan = 0.0f, long delta = 0);
     void playSliderTickSound(float pan = 0.0f);
     void playSliderSlideSound(float pan = 0.0f);
     void playSpinnerSpinSound();
@@ -584,6 +583,9 @@ class Skin {
     Sound *m_spinnerBonus;
     Sound *m_spinnerSpinSound;
 
+    Sound *m_tooearly;
+    Sound *m_toolate;
+
     // Plays when sending a message in chat
     Sound *m_messageSent = NULL;
 

+ 2 - 4
src/App/Osu/SkinImage.cpp

@@ -100,13 +100,11 @@ bool SkinImage::loadImage(std::string skinElementName, bool ignoreDefaultSkin) {
     filepath2.append(skinElementName);
     filepath2.append(".png");
 
-    std::string defaultFilePath1 = "./materials/";
-    defaultFilePath1.append(Skin::OSUSKIN_DEFAULT_SKIN_PATH);
+    std::string defaultFilePath1 = MCENGINE_DATA_DIR "materials/default/";
     defaultFilePath1.append(skinElementName);
     defaultFilePath1.append("@2x.png");
 
-    std::string defaultFilePath2 = "./materials/";
-    defaultFilePath2.append(Skin::OSUSKIN_DEFAULT_SKIN_PATH);
+    std::string defaultFilePath2 = MCENGINE_DATA_DIR "materials/default/";
     defaultFilePath2.append(skinElementName);
     defaultFilePath2.append(".png");
 

+ 9 - 9
src/App/Osu/Slider.cpp

@@ -57,9 +57,7 @@ ConVar osu_slider_body_fade_out_time_multiplier("osu_slider_body_fade_out_time_m
 ConVar osu_slider_reverse_arrow_animated("osu_slider_reverse_arrow_animated", true, FCVAR_DEFAULT,
                                          "pulse animation on reverse arrows");
 ConVar osu_slider_reverse_arrow_alpha_multiplier("osu_slider_reverse_arrow_alpha_multiplier", 1.0f, FCVAR_DEFAULT);
-
-// osu!stable doesn't display a fadeout animation for sliderheads
-ConVar osu_slider_sliderhead_fadeout("osu_slider_sliderhead_fadeout", false, FCVAR_DEFAULT);
+ConVar osu_slider_sliderhead_fadeout("osu_slider_sliderhead_fadeout", true, FCVAR_DEFAULT);
 
 ConVar *Slider::m_osu_playfield_mirror_horizontal_ref = NULL;
 ConVar *Slider::m_osu_playfield_mirror_vertical_ref = NULL;
@@ -368,8 +366,9 @@ void Slider::draw(Graphics *g) {
     }
 
     // slider body fade animation, draw start/end circle hit animation
-    bool is_instafade = convar->getConVarByName("instafade")->getBool();
-    if(!is_instafade && m_fEndSliderBodyFadeAnimation > 0.0f && m_fEndSliderBodyFadeAnimation != 1.0f &&
+    bool instafade_slider_body = convar->getConVarByName("instafade_sliders")->getBool();
+    bool instafade_slider_head = convar->getConVarByName("instafade")->getBool();
+    if(!instafade_slider_body && m_fEndSliderBodyFadeAnimation > 0.0f && m_fEndSliderBodyFadeAnimation != 1.0f &&
        !osu->getModHD()) {
         std::vector<Vector2> emptyVector;
         std::vector<Vector2> alwaysPoints;
@@ -382,7 +381,7 @@ void Slider::draw(Graphics *g) {
                                  1.0f - m_fEndSliderBodyFadeAnimation, getTime());
     }
 
-    if(!is_instafade && osu_slider_sliderhead_fadeout.getBool() && m_fStartHitAnimation > 0.0f &&
+    if(!instafade_slider_head && osu_slider_sliderhead_fadeout.getBool() && m_fStartHitAnimation > 0.0f &&
        m_fStartHitAnimation != 1.0f && !osu->getModHD()) {
         float alpha = 1.0f - m_fStartHitAnimation;
 
@@ -421,7 +420,7 @@ void Slider::draw(Graphics *g) {
         g->popTransform();
     }
 
-    if(!is_instafade && m_fEndHitAnimation > 0.0f && m_fEndHitAnimation != 1.0f && !osu->getModHD()) {
+    if(!instafade_slider_head && m_fEndHitAnimation > 0.0f && m_fEndHitAnimation != 1.0f && !osu->getModHD()) {
         float alpha = 1.0f - m_fEndHitAnimation;
 
         float scale = m_fEndHitAnimation;
@@ -1125,10 +1124,11 @@ void Slider::onHit(LiveScore::HIT result, long delta, bool startOrEnd, float tar
 
             const Vector2 osuCoords = m_beatmap->pixels2OsuCoords(m_beatmap->osuCoords2Pixels(m_vCurPointRaw));
 
+            const long sound_delta = result == LiveScore::HIT::HIT_300 ? 0 : delta;
             m_beatmap->getSkin()->playHitCircleSound(m_iCurRepeatCounterForHitSounds < m_hitSounds.size()
                                                          ? m_hitSounds[m_iCurRepeatCounterForHitSounds]
                                                          : m_iSampleType,
-                                                     GameRules::osuCoords2Pan(osuCoords.x));
+                                                     GameRules::osuCoords2Pan(osuCoords.x), sound_delta);
 
             if(!startOrEnd) {
                 m_fStartHitAnimation = 0.001f;  // quickfix for 1 frame missing images
@@ -1247,7 +1247,7 @@ void Slider::onRepeatHit(bool successful, bool sliderend) {
         m_beatmap->getSkin()->playHitCircleSound(m_iCurRepeatCounterForHitSounds < m_hitSounds.size()
                                                      ? m_hitSounds[m_iCurRepeatCounterForHitSounds]
                                                      : m_iSampleType,
-                                                 GameRules::osuCoords2Pan(osuCoords.x));
+                                                 GameRules::osuCoords2Pan(osuCoords.x), 0);
 
         float animation_multiplier = osu->getSpeedMultiplier() / osu->getAnimationSpeedMultiplier();
         float tick_pulse_time = GameRules::osu_slider_followcircle_tick_pulse_time.getFloat() * animation_multiplier;

+ 1 - 1
src/App/Osu/SongBrowser/SongBrowser.cpp

@@ -2754,7 +2754,7 @@ void SongBrowser::onDatabaseLoadingFinished() {
     // having a copy of the vector in here is actually completely unnecessary
     m_beatmaps = std::vector<DatabaseBeatmap *>(m_db->getDatabaseBeatmaps());
 
-    debugLog("SongBrowser::onDatabaseLoadingFinished() : %i beatmaps.\n", m_beatmaps.size());
+    debugLog("SongBrowser::onDatabaseLoadingFinished() : %i beatmapsets.\n", m_beatmaps.size());
 
     // initialize all collection (grouped) buttons
     {

+ 1 - 1
src/App/Osu/Spinner.cpp

@@ -428,7 +428,7 @@ void Spinner::onHit() {
 
         const Vector2 osuCoords = m_beatmap->pixels2OsuCoords(m_beatmap->osuCoords2Pixels(m_vRawPos));
 
-        m_beatmap->getSkin()->playHitCircleSound(m_iSampleType, GameRules::osuCoords2Pan(osuCoords.x));
+        m_beatmap->getSkin()->playHitCircleSound(m_iSampleType, GameRules::osuCoords2Pan(osuCoords.x), 0);
     }
 
     // add it, and we are finished

+ 1 - 1
src/App/Osu/score.h

@@ -122,7 +122,7 @@ struct FinishedScore {
 
 class LiveScore {
    public:
-    static const u32 VERSION = 20240412;
+    static const u32 VERSION = 20240704;
 
     enum class HIT {
         // score