Parcourir la source

Optimize beatmap list loading some more

Clément Wolf il y a 2 semaines
Parent
commit
a36fb3591d

+ 43 - 42
src/App/Osu/Bancho.cpp

@@ -186,8 +186,9 @@ UString get_disk_uuid() {
 }
 
 void handle_packet(Packet *packet) {
+    // XXX: This is a bit of a mess, should at least group packets by type for readability
     if(packet->id == USER_ID) {
-        bancho.user_id = read_u32(packet);
+        bancho.user_id = read<u32>(packet);
         if(bancho.user_id > 0) {
             debugLog("Logged in as user #%d.\n", bancho.user_id);
             bancho.osu->m_optionsMenu->logInButton->setText("Disconnect");
@@ -259,7 +260,7 @@ void handle_packet(Packet *packet) {
         UString sender = read_string(packet);
         UString text = read_string(packet);
         UString recipient = read_string(packet);
-        i32 sender_id = read_u32(packet);
+        i32 sender_id = read<u32>(packet);
 
         bancho.osu->m_chat->addMessage(recipient, ChatMessage{
                                                       .tms = time(NULL),
@@ -270,44 +271,44 @@ void handle_packet(Packet *packet) {
     } else if(packet->id == PONG) {
         // (nothing to do)
     } else if(packet->id == USER_STATS) {
-        i32 stats_user_id = read_u32(packet);
-        u8 action = read_u8(packet);
+        i32 stats_user_id = read<u32>(packet);
+        u8 action = read<u8>(packet);
 
         UserInfo *user = get_user_info(stats_user_id);
         user->action = (Action)action;
         user->info_text = read_string(packet);
         user->map_md5 = read_string(packet);
-        user->mods = read_u32(packet);
-        user->mode = (GameMode)read_u8(packet);
-        user->map_id = read_u32(packet);
-        user->ranked_score = read_u64(packet);
-        user->accuracy = read_f32(packet);
-        user->plays = read_u32(packet);
-        user->total_score = read_u64(packet);
-        user->global_rank = read_u32(packet);
-        user->pp = read_u16(packet);
+        user->mods = read<u32>(packet);
+        user->mode = (GameMode)read<u8>(packet);
+        user->map_id = read<u32>(packet);
+        user->ranked_score = read<u64>(packet);
+        user->accuracy = read<f32>(packet);
+        user->plays = read<u32>(packet);
+        user->total_score = read<u64>(packet);
+        user->global_rank = read<u32>(packet);
+        user->pp = read<u16>(packet);
 
         if(stats_user_id == bancho.user_id) {
             bancho.osu->m_songBrowser2->m_userButton->updateUserStats();
         }
     } else if(packet->id == USER_LOGOUT) {
-        i32 logged_out_id = read_u32(packet);
-        read_u8(packet);
+        i32 logged_out_id = read<u32>(packet);
+        read<u8>(packet);
         if(logged_out_id == bancho.user_id) {
             debugLog("Logged out.\n");
             disconnect();
         }
     } else if(packet->id == SPECTATOR_JOINED) {
-        i32 spectator_id = read_u32(packet);
+        i32 spectator_id = read<u32>(packet);
         debugLog("Spectator joined: user id %d\n", spectator_id);
     } else if(packet->id == SPECTATOR_LEFT) {
-        i32 spectator_id = read_u32(packet);
+        i32 spectator_id = read<u32>(packet);
         debugLog("Spectator left: user id %d\n", spectator_id);
     } else if(packet->id == VERSION_UPDATE) {
         disconnect();
         bancho.osu->getNotificationOverlay()->addNotification("Server uses an unsupported protocol version.");
     } else if(packet->id == SPECTATOR_CANT_SPECTATE) {
-        i32 spectator_id = read_u32(packet);
+        i32 spectator_id = read<u32>(packet);
         debugLog("Spectator can't spectate: user id %d\n", spectator_id);
     } else if(packet->id == GET_ATTENTION) {
         // (nothing to do)
@@ -329,7 +330,7 @@ void handle_packet(Packet *packet) {
         auto room = new Room(packet);
         bancho.osu->m_lobby->addRoom(room);
     } else if(packet->id == ROOM_CLOSED) {
-        i32 room_id = read_u32(packet);
+        i32 room_id = read<u32>(packet);
         bancho.osu->m_lobby->removeRoom(room_id);
     } else if(packet->id == ROOM_JOIN_SUCCESS) {
         auto room = Room(packet);
@@ -338,10 +339,10 @@ void handle_packet(Packet *packet) {
         bancho.osu->getNotificationOverlay()->addNotification("Failed to join room.");
         bancho.osu->m_lobby->on_room_join_failed();
     } else if(packet->id == FELLOW_SPECTATOR_JOINED) {
-        u32 spectator_id = read_u32(packet);
+        u32 spectator_id = read<u32>(packet);
         (void)spectator_id;  // (spectating not implemented; nothing to do)
     } else if(packet->id == FELLOW_SPECTATOR_LEFT) {
-        u32 spectator_id = read_u32(packet);
+        u32 spectator_id = read<u32>(packet);
         (void)spectator_id;  // (spectating not implemented; nothing to do)
     } else if(packet->id == MATCH_STARTED) {
         auto room = Room(packet);
@@ -353,7 +354,7 @@ void handle_packet(Packet *packet) {
     } else if(packet->id == MATCH_ALL_PLAYERS_LOADED) {
         bancho.osu->m_room->on_all_players_loaded();
     } else if(packet->id == MATCH_PLAYER_FAILED) {
-        i32 slot_id = read_u32(packet);
+        i32 slot_id = read<u32>(packet);
         bancho.osu->m_room->on_player_failed(slot_id);
     } else if(packet->id == MATCH_FINISHED) {
         bancho.osu->m_room->on_match_finished();
@@ -371,7 +372,7 @@ void handle_packet(Packet *packet) {
     } else if(packet->id == CHANNEL_INFO) {
         UString channel_name = read_string(packet);
         UString channel_topic = read_string(packet);
-        i32 nb_members = read_u32(packet);
+        i32 nb_members = read<u32>(packet);
         update_channel(channel_name, channel_topic, nb_members);
     } else if(packet->id == LEFT_CHANNEL) {
         UString name = read_string(packet);
@@ -379,21 +380,21 @@ void handle_packet(Packet *packet) {
     } else if(packet->id == CHANNEL_AUTO_JOIN) {
         UString channel_name = read_string(packet);
         UString channel_topic = read_string(packet);
-        i32 nb_members = read_u32(packet);
+        i32 nb_members = read<u32>(packet);
         update_channel(channel_name, channel_topic, nb_members);
     } else if(packet->id == PRIVILEGES) {
-        read_u32(packet);
+        read<u32>(packet);
         // (nothing to do)
     } else if(packet->id == FRIENDS_LIST) {
         friends.clear();
 
-        u16 nb_friends = read_u16(packet);
+        u16 nb_friends = read<u16>(packet);
         for(int i = 0; i < nb_friends; i++) {
-            u32 friend_id = read_u32(packet);
+            u32 friend_id = read<u32>(packet);
             friends.push_back(friend_id);
         }
     } else if(packet->id == PROTOCOL_VERSION) {
-        int protocol_version = read_u32(packet);
+        int protocol_version = read<u32>(packet);
         if(protocol_version != 19) {
             disconnect();
             bancho.osu->getNotificationOverlay()->addNotification("Server uses an unsupported protocol version.");
@@ -405,7 +406,7 @@ void handle_packet(Packet *packet) {
             bancho.server_icon_url = urls[0];
         }
     } else if(packet->id == MATCH_PLAYER_SKIPPED) {
-        i32 user_id = read_u32(packet);
+        i32 user_id = read<u32>(packet);
         bancho.osu->m_room->on_player_skip(user_id);
 
         // I'm not sure the server ever sends MATCH_SKIP... So, double-checking here.
@@ -421,20 +422,20 @@ void handle_packet(Packet *packet) {
             bancho.osu->m_room->on_all_players_skipped();
         }
     } else if(packet->id == USER_PRESENCE) {
-        i32 presence_user_id = read_u32(packet);
+        i32 presence_user_id = read<u32>(packet);
         UString presence_username = read_string(packet);
 
         UserInfo *user = get_user_info(presence_user_id);
         user->name = presence_username;
-        user->utc_offset = read_u8(packet);
-        user->country = read_u8(packet);
-        user->privileges = read_u8(packet);
-        user->longitude = read_f32(packet);
-        user->latitude = read_f32(packet);
-        user->global_rank = read_u32(packet);
+        user->utc_offset = read<u8>(packet);
+        user->country = read<u8>(packet);
+        user->privileges = read<u8>(packet);
+        user->longitude = read<f32>(packet);
+        user->latitude = read<f32>(packet);
+        user->global_rank = read<u32>(packet);
     } else if(packet->id == RESTART) {
         // XXX: wait 'ms' milliseconds before reconnecting
-        i32 ms = read_u32(packet);
+        i32 ms = read<u32>(packet);
         (void)ms;
 
         // Some servers send "restart" packets when password is incorrect
@@ -447,7 +448,7 @@ void handle_packet(Packet *packet) {
         UString text = read_string(packet);
         UString recipient = read_string(packet);
         (void)recipient;
-        i32 sender_id = read_u32(packet);
+        i32 sender_id = read<u32>(packet);
         bancho.osu->m_chat->addMessage(recipient, ChatMessage{
                                                       .tms = time(NULL),
                                                       .author_id = sender_id,
@@ -463,23 +464,23 @@ void handle_packet(Packet *packet) {
         debugLog("Room changed password to %s\n", new_password.toUtf8());
         bancho.room.password = new_password;
     } else if(packet->id == SILENCE_END) {
-        i32 delta = read_u32(packet);
+        i32 delta = read<u32>(packet);
         debugLog("Silence ends in %d seconds.\n", delta);
         // XXX: Prevent user from sending messages while silenced
     } else if(packet->id == USER_SILENCED) {
-        i32 user_id = read_u32(packet);
+        i32 user_id = read<u32>(packet);
         debugLog("User #%d silenced.\n", user_id);
     } else if(packet->id == USER_DM_BLOCKED) {
         read_string(packet);
         read_string(packet);
         UString blocked = read_string(packet);
-        read_u32(packet);
+        read<u32>(packet);
         debugLog("Blocked %s.\n", blocked.toUtf8());
     } else if(packet->id == TARGET_IS_SILENCED) {
         read_string(packet);
         read_string(packet);
         UString blocked = read_string(packet);
-        read_u32(packet);
+        read<u32>(packet);
         debugLog("Silenced %s.\n", blocked.toUtf8());
     } else if(packet->id == VERSION_UPDATE_FORCED) {
         disconnect();

+ 2 - 2
src/App/Osu/BanchoNetworking.cpp

@@ -276,9 +276,9 @@ static void send_bancho_packet(CURL *curl, Packet outgoing) {
     }
 
     while(response.pos < response.size) {
-        u16 packet_id = read_u16(&response);
+        u16 packet_id = read<u16>(&response);
         response.pos++;
-        u32 packet_len = read_u32(&response);
+        u32 packet_len = read<u32>(&response);
         if(packet_len > 10485760) {
             debugLog("Received a packet over 10Mb! Dropping response.\n");
             goto end;

+ 27 - 60
src/App/Osu/BanchoProtocol.cpp

@@ -5,18 +5,21 @@
 
 #include "Bancho.h"
 
+// Null array for returning empty structures when trying to read more data out of a Packet than expected.
+u8 NULL_ARRAY[NULL_ARRAY_SIZE] = {0};
+
 Room::Room() {
     // 0-initialized room means we're not in multiplayer at the moment
 }
 
 Room::Room(Packet *packet) {
-    id = read_u16(packet);
-    in_progress = read_u8(packet);
-    match_type = read_u8(packet);
-    mods = read_u32(packet);
+    id = read<u16>(packet);
+    in_progress = read<u8>(packet);
+    match_type = read<u8>(packet);
+    mods = read<u32>(packet);
     name = read_string(packet);
 
-    has_password = read_u8(packet) > 0;
+    has_password = read<u8>(packet) > 0;
     if(has_password) {
         // Discard password. It should be an empty string, but just in case, read it properly.
         packet->pos--;
@@ -24,17 +27,17 @@ Room::Room(Packet *packet) {
     }
 
     map_name = read_string(packet);
-    map_id = read_u32(packet);
+    map_id = read<u32>(packet);
 
     auto hash_str = read_string(packet);
     map_md5 = hash_str.toUtf8();
 
     nb_players = 0;
     for(int i = 0; i < 16; i++) {
-        slots[i].status = read_u8(packet);
+        slots[i].status = read<u8>(packet);
     }
     for(int i = 0; i < 16; i++) {
-        slots[i].team = read_u8(packet);
+        slots[i].team = read<u8>(packet);
     }
     for(int s = 0; s < 16; s++) {
         if(!slots[s].is_locked()) {
@@ -42,23 +45,23 @@ Room::Room(Packet *packet) {
         }
 
         if(slots[s].has_player()) {
-            slots[s].player_id = read_u32(packet);
+            slots[s].player_id = read<u32>(packet);
             nb_players++;
         }
     }
 
-    host_id = read_u32(packet);
-    mode = read_u8(packet);
-    win_condition = read_u8(packet);
-    team_type = read_u8(packet);
-    freemods = read_u8(packet);
+    host_id = read<u32>(packet);
+    mode = read<u8>(packet);
+    win_condition = read<u8>(packet);
+    team_type = read<u8>(packet);
+    freemods = read<u8>(packet);
     if(freemods) {
         for(int i = 0; i < 16; i++) {
-            slots[i].mods = read_u32(packet);
+            slots[i].mods = read<u32>(packet);
         }
     }
 
-    seed = read_u32(packet);
+    seed = read<u32>(packet);
 }
 
 void Room::pack(Packet *packet) {
@@ -102,34 +105,10 @@ bool Room::is_host() { return host_id == bancho.user_id; }
 void read_bytes(Packet *packet, u8 *bytes, size_t n) {
     if(packet->pos + n > packet->size) {
         packet->pos = packet->size + 1;
-        return;
+    } else {
+        memcpy(bytes, packet->memory + packet->pos, n);
+        packet->pos += n;
     }
-    memcpy(bytes, packet->memory + packet->pos, n);
-    packet->pos += n;
-}
-
-u8 read_u8(Packet *packet) {
-    u8 byte = 0;
-    read_bytes(packet, &byte, 1);
-    return byte;
-}
-
-u16 read_u16(Packet *packet) {
-    u16 s = 0;
-    read_bytes(packet, (u8 *)&s, 2);
-    return s;
-}
-
-u32 read_u32(Packet *packet) {
-    u32 i = 0;
-    read_bytes(packet, (u8 *)&i, 4);
-    return i;
-}
-
-u64 read_u64(Packet *packet) {
-    u64 i = 0;
-    read_bytes(packet, (u8 *)&i, 8);
-    return i;
 }
 
 u32 read_uleb128(Packet *packet) {
@@ -138,7 +117,7 @@ u32 read_uleb128(Packet *packet) {
     u8 byte = 0;
 
     do {
-        byte = read_u8(packet);
+        byte = read<u8>(packet);
         result |= (byte & 0x7f) << shift;
         shift += 7;
     } while(byte & 0x80);
@@ -146,20 +125,8 @@ u32 read_uleb128(Packet *packet) {
     return result;
 }
 
-float read_f32(Packet *packet) {
-    float f = 0;
-    read_bytes(packet, (u8 *)&f, 4);
-    return f;
-}
-
-double read_f64(Packet *packet) {
-    double f = 0;
-    read_bytes(packet, (u8 *)&f, 8);
-    return f;
-}
-
 UString read_string(Packet *packet) {
-    u8 empty_check = read_u8(packet);
+    u8 empty_check = read<u8>(packet);
     if(empty_check == 0) return UString("");
 
     u32 len = read_uleb128(packet);
@@ -174,7 +141,7 @@ UString read_string(Packet *packet) {
 }
 
 std::string read_stdstring(Packet *packet) {
-    u8 empty_check = read_u8(packet);
+    u8 empty_check = read<u8>(packet);
     if(empty_check == 0) return std::string();
 
     u32 len = read_uleb128(packet);
@@ -190,7 +157,7 @@ std::string read_stdstring(Packet *packet) {
 MD5Hash read_hash(Packet *packet) {
     MD5Hash hash;
 
-    u8 empty_check = read_u8(packet);
+    u8 empty_check = read<u8>(packet);
     if(empty_check == 0) return hash;
 
     u32 len = read_uleb128(packet);
@@ -204,7 +171,7 @@ MD5Hash read_hash(Packet *packet) {
 }
 
 void skip_string(Packet *packet) {
-    u8 empty_check = read_u8(packet);
+    u8 empty_check = read<u8>(packet);
     if(empty_check == 0) {
         return;
     }

+ 31 - 6
src/App/Osu/BanchoProtocol.h

@@ -242,18 +242,31 @@ class Room {
 };
 
 void read_bytes(Packet *packet, u8 *bytes, size_t n);
-u8 read_u8(Packet *packet);
-u16 read_u16(Packet *packet);
-u32 read_u32(Packet *packet);
-u64 read_u64(Packet *packet);
-f32 read_f32(Packet *packet);
-f64 read_f64(Packet *packet);
 u32 read_uleb128(Packet *packet);
 UString read_string(Packet *packet);
 std::string read_stdstring(Packet *packet);
 void skip_string(Packet *packet);
 MD5Hash read_hash(Packet *packet);
 
+// Null array for returning empty structures when trying to read more data out of a Packet than expected.
+// See the read<> template below.
+#define NULL_ARRAY_SIZE 128
+extern u8 NULL_ARRAY[NULL_ARRAY_SIZE];
+
+template <typename T>
+T read(Packet *packet) {
+    static_assert(sizeof(T) <= sizeof(NULL_ARRAY), "Please make NULL_ARRAY_SIZE bigger");
+
+    if(packet->pos + sizeof(T) > packet->size) {
+        packet->pos = packet->size + 1;
+        return *(T *)NULL_ARRAY;
+    } else {
+        T out = *(T *)(packet->memory + packet->pos);
+        packet->pos += sizeof(T);
+        return out;
+    }
+}
+
 void write_bytes(Packet *packet, u8 *bytes, size_t n);
 void write_u8(Packet *packet, u8 b);
 void write_u16(Packet *packet, u16 s);
@@ -263,3 +276,15 @@ void write_f32(Packet *packet, f32 f);
 void write_f64(Packet *packet, f64 f);
 void write_uleb128(Packet *packet, u32 num);
 void write_string(Packet *packet, const char *str);
+
+template <typename T>
+void write(Packet *packet, T t) {
+    if(packet->pos + sizeof(T) > packet->size) {
+        packet->memory = (u8 *)realloc(packet->memory, packet->size + sizeof(T) + 128);
+        packet->size += sizeof(T) + 128;
+        if(!packet->memory) return;
+    }
+
+    *(packet->memory + packet->pos) = t;
+    packet->pos += sizeof(T);
+}

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

@@ -46,7 +46,7 @@ Changelog::Changelog(Osu *osu) : ScreenBackable(osu) {
     latest.changes.push_back("- Changed default instant replay key to F2 to avoid conflicts with mod selector");
     latest.changes.push_back("- Disabled score submission when mods are toggled mid-game");
     latest.changes.push_back("- Forced exclusive mode when using WASAPI output");
-    latest.changes.push_back("- Optimized beatmap list loading speed");
+    latest.changes.push_back("- Optimized beatmap list loading speed (a lot)");
     latest.changes.push_back("- Optimized collection processing speed");
     latest.changes.push_back("- Removed support for the Nintendo Switch");
     latest.changes.push_back("- Updated protocol version");

+ 7 - 7
src/App/Osu/Collections.cpp

@@ -100,8 +100,8 @@ bool load_collections() {
     peppy_collections_path.append("collection.db");
     Packet peppy_collections = load_db(peppy_collections_path);
     if(peppy_collections.size > 0) {
-        u32 version = read_u32(&peppy_collections);
-        u32 nb_collections = read_u32(&peppy_collections);
+        u32 version = read<u32>(&peppy_collections);
+        u32 nb_collections = read<u32>(&peppy_collections);
 
         if(version > osu_database_version) {
             debugLog("osu!stable collection.db version more recent than neosu, loading might fail.\n");
@@ -109,7 +109,7 @@ bool load_collections() {
 
         for(int c = 0; c < nb_collections; c++) {
             auto name = read_stdstring(&peppy_collections);
-            u32 nb_maps = read_u32(&peppy_collections);
+            u32 nb_maps = read<u32>(&peppy_collections);
 
             auto collection = get_or_create_collection(name);
             collection->maps.reserve(nb_maps);
@@ -126,8 +126,8 @@ bool load_collections() {
 
     auto neosu_collections = load_db("collections.db");
     if(neosu_collections.size > 0) {
-        u32 version = read_u32(&neosu_collections);
-        u32 nb_collections = read_u32(&neosu_collections);
+        u32 version = read<u32>(&neosu_collections);
+        u32 nb_collections = read<u32>(&neosu_collections);
 
         if(version > COLLECTIONS_DB_VERSION) {
             debugLog("neosu collections.db version is too recent! Cannot load it without stuff breaking.\n");
@@ -142,7 +142,7 @@ bool load_collections() {
 
             u32 nb_deleted_maps = 0;
             if(version >= 20240429) {
-                nb_deleted_maps = read_u32(&neosu_collections);
+                nb_deleted_maps = read<u32>(&neosu_collections);
             }
 
             collection->deleted_maps.reserve(nb_deleted_maps);
@@ -157,7 +157,7 @@ bool load_collections() {
                 collection->deleted_maps.push_back(map_hash);
             }
 
-            u32 nb_maps = read_u32(&neosu_collections);
+            u32 nb_maps = read<u32>(&neosu_collections);
             collection->maps.reserve(collection->maps.size() + nb_maps);
             collection->neosu_maps.reserve(nb_maps);
 

+ 139 - 297
src/App/Osu/Database.cpp

@@ -70,13 +70,6 @@ ConVar osu_user_include_relax_and_autopilot_for_stats("osu_user_include_relax_an
 ConVar osu_user_switcher_include_legacy_scores_for_names("osu_user_switcher_include_legacy_scores_for_names", true,
                                                          FCVAR_NONE);
 
-TIMINGPOINT read_timing_point(Packet *packet) {
-    const double bpm = read_f64(packet);
-    const double offset = read_f64(packet);
-    const bool timingChange = (bool)read_u8(packet);
-    return (struct TIMINGPOINT){bpm, offset, timingChange};
-}
-
 Packet load_db(std::string path) {
     Packet db;
 
@@ -923,12 +916,6 @@ void Database::scheduleLoadRaw() {
 
         m_bRawBeatmapLoadScheduled = true;
         m_importTimer->start();
-
-        if(m_bIsFirstLoad) {
-            // reset
-            m_rawHashToDiff2.clear();
-            m_rawHashToBeatmap.clear();
-        }
     } else
         m_fLoadingProgress = 1.0f;
 
@@ -956,12 +943,12 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     m_importTimer->start();
 
     // read header
-    m_iVersion = read_u32(db);
-    m_iFolderCount = read_u32(db);
-    read_u8(db);
-    read_u64(db) /* timestamp */;
+    m_iVersion = read<u32>(db);
+    m_iFolderCount = read<u32>(db);
+    read<u8>(db);
+    read<u64>(db) /* timestamp */;
     auto playerName = read_stdstring(db);
-    m_iNumBeatmapsToLoad = read_u32(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);
@@ -1002,8 +989,6 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
     };
     std::vector<BeatmapSet> beatmapSets;
     std::unordered_map<int, size_t> setIDToIndex;
-    std::unordered_map<MD5Hash, DatabaseBeatmap *> hashToDiff2;
-    std::unordered_map<MD5Hash, DatabaseBeatmap *> hashToBeatmap;
     for(int i = 0; i < m_iNumBeatmapsToLoad; i++) {
         if(m_bInterruptLoad.load()) break;  // cancellation point
 
@@ -1017,7 +1002,7 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
             // 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
+            /*unsigned int size = */ read<u32>(db);  // size in bytes of the beatmap entry
         }
 
         std::string artistName = read_stdstring(db);
@@ -1034,25 +1019,25 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         auto hash_str = read_stdstring(db);
         MD5Hash md5hash = hash_str.c_str();
         std::string osuFileName = read_stdstring(db);
-        /*unsigned char rankedStatus = */ read_u8(db);
-        unsigned short numCircles = read_u16(db);
-        unsigned short numSliders = read_u16(db);
-        unsigned short numSpinners = read_u16(db);
-        long long lastModificationTime = read_u64(db);
-        float AR = read_f32(db);
-        float CS = read_f32(db);
-        float HP = read_f32(db);
-        float OD = read_f32(db);
-        double sliderMultiplier = read_f64(db);
-
-        unsigned int numOsuStandardStarRatings = read_u32(db);
+        /*unsigned char rankedStatus = */ read<u8>(db);
+        unsigned short numCircles = read<u16>(db);
+        unsigned short numSliders = read<u16>(db);
+        unsigned short numSpinners = read<u16>(db);
+        long long lastModificationTime = read<u64>(db);
+        float AR = read<f32>(db);
+        float CS = read<f32>(db);
+        float HP = read<f32>(db);
+        float OD = read<f32>(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);
+            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;
@@ -1066,63 +1051,60 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
             if(result != m_starsCache.end()) numOsuStandardStars = result->second.starsNomod;
         }
 
-        unsigned int numTaikoStarRatings = read_u32(db);
+        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);
-            read_u8(db);  // ObjType
-            read_f64(db);
+            read<u8>(db);  // ObjType
+            read<u32>(db);
+            read<u8>(db);  // ObjType
+            read<f64>(db);
         }
 
-        unsigned int numCtbStarRatings = read_u32(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);
-            read_u8(db);  // ObjType
-            read_f64(db);
+            read<u8>(db);  // ObjType
+            read<u32>(db);
+            read<u8>(db);  // ObjType
+            read<f64>(db);
         }
 
-        unsigned int numManiaStarRatings = read_u32(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);
-            read_u8(db);  // ObjType
-            read_f64(db);
+            read<u8>(db);  // ObjType
+            read<u32>(db);
+            read<u8>(db);  // ObjType
+            read<f64>(db);
         }
 
-        /*unsigned int drainTime = */ read_u32(db);  // seconds
-        int duration = read_u32(db);                 // milliseconds
-        duration = duration >= 0 ? duration : 0;     // sanity clamp
-        int previewTime = read_u32(db);
+        /*unsigned int drainTime = */ read<u32>(db);  // seconds
+        int duration = read<u32>(db);                 // milliseconds
+        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);
-        // debugLog("%i timingpoints\n", numTimingPoints);
-        std::vector<TIMINGPOINT> timingPoints;
-        for(int t = 0; t < numTimingPoints; t++) {
-            timingPoints.push_back(read_timing_point(db));
-        }
+        unsigned int numTimingPoints = read<u32>(db);
+        zarray<TIMINGPOINT> timingPoints(numTimingPoints);
+        read_bytes(db, (u8 *)timingPoints.data(), sizeof(TIMINGPOINT) * numTimingPoints);
 
-        int beatmapID = read_u32(db);     // fucking bullshit, this is NOT an unsigned integer as is described on the
-                                          // wiki, it can and is -1 sometimes
-        int beatmapSetID = read_u32(db);  // same here
-        /*unsigned int threadID = */ read_u32(db);
+        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);
 
-        /*unsigned char osuStandardGrade = */ read_u8(db);
-        /*unsigned char taikoGrade = */ read_u8(db);
-        /*unsigned char ctbGrade = */ read_u8(db);
-        /*unsigned char maniaGrade = */ read_u8(db);
+        /*unsigned char osuStandardGrade = */ read<u8>(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);
+        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);
@@ -1131,29 +1113,29 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         trim(&songTags);
         // debugLog("songSource = %s, songTags = %s\n", songSource.toUtf8(), songTags.toUtf8());
 
-        short onlineOffset = read_u16(db);
+        short onlineOffset = read<u16>(db);
         skip_string(db);  // song title font
-        /*bool unplayed = */ read_u8(db);
-        /*long long lastTimePlayed = */ read_u64(db);
-        /*bool isOsz2 = */ read_u8(db);
+        /*bool unplayed = */ read<u8>(db);
+        /*long long lastTimePlayed = */ read<u64>(db);
+        /*bool isOsz2 = */ read<u8>(db);
 
         // somehow, some beatmaps may have spaces at the start/end of their
         // path, breaking the Windows API (e.g. https://osu.ppy.sh/s/215347)
         auto path = read_stdstring(db);
         trim(&path);
 
-        /*long long lastOnlineCheck = */ read_u64(db);
+        /*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);
-        /*bool disableStoryboard = */ read_u8(db);
-        /*bool disableVideo = */ read_u8(db);
-        /*bool visualOverride = */ read_u8(db);
-        /*int lastEditTime = */ read_u32(db);
-        /*unsigned char maniaScrollSpeed = */ read_u8(db);
+        /*bool ignoreBeatmapSounds = */ read<u8>(db);
+        /*bool ignoreBeatmapSkin = */ read<u8>(db);
+        /*bool disableStoryboard = */ read<u8>(db);
+        /*bool disableVideo = */ read<u8>(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);
@@ -1224,132 +1206,19 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
                 diff2->m_fStarsNomod = numOsuStandardStars;
 
                 // calculate bpm range
-                float minBeatLength = 0;
-                float maxBeatLength = std::numeric_limits<float>::max();
-                std::vector<TIMINGPOINT> uninheritedTimingpoints;
-                for(int j = 0; j < timingPoints.size(); j++) {
-                    const TIMINGPOINT &t = timingPoints[j];
-
-                    if(t.msPerBeat >= 0)  // NOT inherited
-                    {
-                        uninheritedTimingpoints.push_back(t);
-
-                        if(t.msPerBeat > minBeatLength) minBeatLength = t.msPerBeat;
-                        if(t.msPerBeat < maxBeatLength) maxBeatLength = t.msPerBeat;
-                    }
-                }
-
-                // convert from msPerBeat to BPM
-                const float msPerMinute = 1 * 60 * 1000;
-                if(minBeatLength != 0) minBeatLength = msPerMinute / minBeatLength;
-                if(maxBeatLength != 0) maxBeatLength = msPerMinute / maxBeatLength;
-
-                diff2->m_iMinBPM = (int)std::round(minBeatLength);
-                diff2->m_iMaxBPM = (int)std::round(maxBeatLength);
-
-                struct MostCommonBPMHelper {
-                    static int calculateMostCommonBPM(const std::vector<TIMINGPOINT> &uninheritedTimingpoints,
-                                                      long lastTime) {
-                        if(uninheritedTimingpoints.size() < 1) return 0;
-
-                        struct Tuple {
-                            float beatLength;
-                            long duration;
-
-                            size_t sortHack;
-                        };
-
-                        // "Construct a set of (beatLength, duration) tuples for each individual timing point."
-                        std::vector<Tuple> tuples;
-                        tuples.reserve(uninheritedTimingpoints.size());
-                        for(size_t i = 0; i < uninheritedTimingpoints.size(); i++) {
-                            const TIMINGPOINT &t = uninheritedTimingpoints[i];
-
-                            Tuple tuple;
-                            {
-                                if(t.offset > lastTime) {
-                                    tuple.beatLength = std::round(t.msPerBeat * 1000.0f) / 1000.0f;
-                                    tuple.duration = 0;
-                                } else {
-                                    // "osu-stable forced the first control point to start at 0."
-                                    // "This is reproduced here to maintain compatibility around osu!mania scroll speed
-                                    // and song select display."
-                                    const long currentTime = (i == 0 ? 0 : t.offset);
-                                    const long nextTime = (i >= uninheritedTimingpoints.size() - 1
-                                                               ? lastTime
-                                                               : uninheritedTimingpoints[i + 1].offset);
-
-                                    tuple.beatLength = std::round(t.msPerBeat * 1000.0f) / 1000.0f;
-                                    tuple.duration = std::max(nextTime - currentTime, (long)0);
-                                }
-
-                                tuple.sortHack = i;
-                            }
-                            tuples.push_back(tuple);
-                        }
-
-                        // "Aggregate durations into a set of (beatLength, duration) tuples for each beat length"
-                        std::vector<Tuple> aggregations;
-                        aggregations.reserve(tuples.size());
-                        for(size_t i = 0; i < tuples.size(); i++) {
-                            const Tuple &t = tuples[i];
-
-                            bool foundExistingAggregation = false;
-                            size_t aggregationIndex = 0;
-                            for(size_t j = 0; j < aggregations.size(); j++) {
-                                if(aggregations[j].beatLength == t.beatLength) {
-                                    foundExistingAggregation = true;
-                                    aggregationIndex = j;
-                                    break;
-                                }
-                            }
-
-                            if(!foundExistingAggregation)
-                                aggregations.push_back(t);
-                            else
-                                aggregations[aggregationIndex].duration += t.duration;
-                        }
-
-                        // "Get the most common one, or 0 as a suitable default"
-                        struct SortByDuration {
-                            bool operator()(Tuple const &a, Tuple const &b) const {
-                                // first condition: duration
-                                // second condition: if duration is the same, higher BPM goes before lower BPM
-
-                                // strict weak ordering!
-                                if(a.duration == b.duration && a.beatLength == b.beatLength)
-                                    return a.sortHack > b.sortHack;
-                                else if(a.duration == b.duration)
-                                    return (a.beatLength < b.beatLength);
-                                else
-                                    return (a.duration > b.duration);
-                            }
-                        };
-                        std::sort(aggregations.begin(), aggregations.end(), SortByDuration());
-
-                        float mostCommonBPM = aggregations[0].beatLength;
-                        {
-                            // convert from msPerBeat to BPM
-                            const float msPerMinute = 1.0f * 60.0f * 1000.0f;
-                            if(mostCommonBPM != 0.0f) mostCommonBPM = msPerMinute / mostCommonBPM;
-                        }
-                        return (int)std::round(mostCommonBPM);
-                    }
-                };
-                diff2->m_iMostCommonBPM = MostCommonBPMHelper::calculateMostCommonBPM(
-                    uninheritedTimingpoints,
-                    (timingPoints.size() > 0 ? timingPoints[timingPoints.size() - 1].offset : 0));
+                auto bpm = getBPM(timingPoints, (numTimingPoints > 0 ? timingPoints[numTimingPoints - 1].offset : 0));
+                diff2->m_iMinBPM = bpm.min;
+                diff2->m_iMaxBPM = bpm.max;
+                diff2->m_iMostCommonBPM = bpm.most_common;
 
                 // build temp partial timingpoints, only used for menu animations
-                for(int t = 0; t < timingPoints.size(); t++) {
-                    DatabaseBeatmap::TIMINGPOINT tp;
-                    {
-                        tp.offset = timingPoints[t].offset;
-                        tp.msPerBeat = timingPoints[t].msPerBeat;
-                        tp.timingChange = timingPoints[t].timingChange;
-                        tp.kiai = false;
-                    }
-                    diff2->m_timingpoints.push_back(tp);
+                // a bit hacky to avoid slow ass allocations
+                diff2->m_timingpoints.resize(numTimingPoints);
+                memset(diff2->m_timingpoints.data(), 0, numTimingPoints * sizeof(DatabaseBeatmap::TIMINGPOINT));
+                for(int t = 0; t < numTimingPoints; t++) {
+                    diff2->m_timingpoints[t].offset = (long)timingPoints[t].offset;
+                    diff2->m_timingpoints[t].msPerBeat = (float)timingPoints[t].msPerBeat;
+                    diff2->m_timingpoints[t].timingChange = timingPoints[t].timingChange;
                 }
             }
 
@@ -1389,9 +1258,6 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
 
                 beatmapSets.push_back(s);
             }
-
-            // and add an entry in our hashmap
-            hashToDiff2[diff2->getMD5Hash()] = diff2;
         }
     }
 
@@ -1405,17 +1271,11 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
         if(beatmapSets[i].diffs2.size() > 0)  // sanity check
         {
             if(beatmapSets[i].setID > 0) {
-                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, beatmapSets[i].diffs2);
+                DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, std::move(beatmapSets[i].diffs2));
 
                 m_databaseBeatmaps.push_back(bm);
 
-                // and add an entry in our hashmap
-                for(int d = 0; d < beatmapSets[i].diffs2.size(); d++) {
-                    const MD5Hash &md5hash = beatmapSets[i].diffs2[d]->getMD5Hash();
-                    hashToBeatmap[md5hash] = bm;
-                }
-
-                // and in the other hashmap
+                // and add an entry in the hashmap
                 std::string titleArtist = bm->getTitle();
                 titleArtist.append(bm->getArtist());
                 if(titleArtist.length() > 0) titleArtistToBeatmap[titleArtist] = bm;
@@ -1454,9 +1314,6 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
                             // we have found a matching beatmap, add ourself to its diffs
                             const_cast<std::vector<DatabaseBeatmap *> &>(result->second->getDifficulties())
                                 .push_back(diff2);
-
-                            // and add an entry in our hashmap
-                            hashToBeatmap[diff2->getMD5Hash()] = result->second;
                         }
                     }
 
@@ -1465,15 +1322,9 @@ void Database::loadDB(Packet *db, bool &fallbackToRawLoad) {
                         std::vector<DatabaseBeatmap *> diffs2;
                         diffs2.push_back(beatmapSets[i].diffs2[b]);
 
-                        DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, diffs2);
+                        DatabaseBeatmap *bm = new DatabaseBeatmap(m_osu, std::move(diffs2));
 
                         m_databaseBeatmaps.push_back(bm);
-
-                        // and add an entry in our hashmap
-                        for(int d = 0; d < diffs2.size(); d++) {
-                            const MD5Hash &md5hash = diffs2[d]->getMD5Hash();
-                            hashToBeatmap[md5hash] = bm;
-                        }
                     }
                 }
             }
@@ -1501,18 +1352,18 @@ void Database::loadStars() {
     if(cache.size > 0) {
         m_starsCache.clear();
 
-        const int cacheVersion = read_u32(&cache);
+        const int cacheVersion = read<u32>(&cache);
 
         if(cacheVersion <= starsCacheVersion) {
             skip_string(&cache);  // ignore md5
-            const i64 numStarsCacheEntries = read_u64(&cache);
+            const i64 numStarsCacheEntries = read<u64>(&cache);
 
             debugLog("Stars cache: version = %i, numStarsCacheEntries = %i\n", cacheVersion, numStarsCacheEntries);
 
             for(i64 i = 0; i < numStarsCacheEntries; i++) {
                 auto hash_str = read_stdstring(&cache);
                 const MD5Hash beatmapMD5Hash = hash_str.c_str();
-                const float starsNomod = read_f32(&cache);
+                const float starsNomod = read<f32>(&cache);
 
                 STARS_CACHE_ENTRY entry;
                 { entry.starsNomod = starsNomod; }
@@ -1584,14 +1435,14 @@ void Database::loadScores() {
         if(db.size > 0) {
             customScoresFileSize = db.size;
 
-            const u32 dbVersion = read_u32(&db);
-            const u32 numBeatmaps = read_u32(&db);
+            const u32 dbVersion = read<u32>(&db);
+            const u32 numBeatmaps = read<u32>(&db);
             debugLog("Custom scores: version = %u, numBeatmaps = %u\n", dbVersion, numBeatmaps);
 
             if(dbVersion <= LiveScore::VERSION) {
                 for(int b = 0; b < numBeatmaps; b++) {
                     auto hash_str = read_stdstring(&db);
-                    const int numScores = read_u32(&db);
+                    const int numScores = read<u32>(&db);
 
                     if(hash_str.size() < 32) {
                         if(Osu::debug->getBool()) {
@@ -1617,57 +1468,57 @@ void Database::loadScores() {
                         sc.numCircles = -1;
                         sc.perfect = false;
 
-                        const unsigned char gamemode = read_u8(&db);  // NOTE: abused as isImportedLegacyScore flag
-                        sc.version = read_u32(&db);
+                        const unsigned char gamemode = read<u8>(&db);  // NOTE: abused as isImportedLegacyScore flag
+                        sc.version = read<u32>(&db);
 
                         if(dbVersion == 20210103 && sc.version > 20190103) {
-                            sc.isImportedLegacyScore = read_u8(&db);
+                            sc.isImportedLegacyScore = read<u8>(&db);
                         } else if(dbVersion > 20210103 && sc.version > 20190103) {
                             // HACKHACK: for explanation see hackIsImportedLegacyScoreFlag
                             sc.isImportedLegacyScore = (gamemode & hackIsImportedLegacyScoreFlag);
                         }
 
-                        sc.unixTimestamp = read_u64(&db);
+                        sc.unixTimestamp = read<u64>(&db);
 
                         // default
                         sc.playerName = read_stdstring(&db);
 
-                        sc.num300s = read_u16(&db);
-                        sc.num100s = read_u16(&db);
-                        sc.num50s = read_u16(&db);
-                        sc.numGekis = read_u16(&db);
-                        sc.numKatus = read_u16(&db);
-                        sc.numMisses = read_u16(&db);
+                        sc.num300s = read<u16>(&db);
+                        sc.num100s = read<u16>(&db);
+                        sc.num50s = read<u16>(&db);
+                        sc.numGekis = read<u16>(&db);
+                        sc.numKatus = read<u16>(&db);
+                        sc.numMisses = read<u16>(&db);
 
-                        sc.score = read_u64(&db);
-                        sc.comboMax = read_u16(&db);
-                        sc.modsLegacy = read_u32(&db);
+                        sc.score = read<u64>(&db);
+                        sc.comboMax = read<u16>(&db);
+                        sc.modsLegacy = read<u32>(&db);
 
                         // custom
-                        sc.numSliderBreaks = read_u16(&db);
-                        sc.pp = read_f32(&db);
-                        sc.unstableRate = read_f32(&db);
-                        sc.hitErrorAvgMin = read_f32(&db);
-                        sc.hitErrorAvgMax = read_f32(&db);
-                        sc.starsTomTotal = read_f32(&db);
-                        sc.starsTomAim = read_f32(&db);
-                        sc.starsTomSpeed = read_f32(&db);
-                        sc.speedMultiplier = read_f32(&db);
-                        sc.CS = read_f32(&db);
-                        sc.AR = read_f32(&db);
-                        sc.OD = read_f32(&db);
-                        sc.HP = read_f32(&db);
+                        sc.numSliderBreaks = read<u16>(&db);
+                        sc.pp = read<f32>(&db);
+                        sc.unstableRate = read<f32>(&db);
+                        sc.hitErrorAvgMin = read<f32>(&db);
+                        sc.hitErrorAvgMax = read<f32>(&db);
+                        sc.starsTomTotal = read<f32>(&db);
+                        sc.starsTomAim = read<f32>(&db);
+                        sc.starsTomSpeed = read<f32>(&db);
+                        sc.speedMultiplier = read<f32>(&db);
+                        sc.CS = read<f32>(&db);
+                        sc.AR = read<f32>(&db);
+                        sc.OD = read<f32>(&db);
+                        sc.HP = read<f32>(&db);
 
                         if(sc.version > 20180722) {
-                            sc.maxPossibleCombo = read_u32(&db);
-                            sc.numHitObjects = read_u32(&db);
-                            sc.numCircles = read_u32(&db);
+                            sc.maxPossibleCombo = read<u32>(&db);
+                            sc.numHitObjects = read<u32>(&db);
+                            sc.numCircles = read<u32>(&db);
                             sc.perfect = sc.comboMax >= sc.maxPossibleCombo;
                         }
 
                         if(sc.version >= 20240412) {
                             sc.has_replay = true;
-                            sc.online_score_id = read_u32(&db);
+                            sc.online_score_id = read<u32>(&db);
                             sc.server = read_stdstring(&db);
                         }
 
@@ -1704,8 +1555,8 @@ void Database::loadScores() {
         // point directly to neosu, which would break legacy score db loading
         // here since there is no magic number)
         if(db.size > 0 && db.size != customScoresFileSize) {
-            const int dbVersion = read_u32(&db);
-            const int numBeatmaps = read_u32(&db);
+            const int dbVersion = read<u32>(&db);
+            const int numBeatmaps = read<u32>(&db);
 
             debugLog("Legacy scores: version = %i, numBeatmaps = %i\n", dbVersion, numBeatmaps);
 
@@ -1723,7 +1574,7 @@ void Database::loadScores() {
                 }
 
                 const MD5Hash md5hash = hash_str.c_str();
-                const int numScores = read_u32(&db);
+                const int numScores = read<u32>(&db);
 
                 if(Osu::debug->getBool())
                     debugLog("Beatmap[%i]: md5hash = %s, numScores = %i\n", b, md5hash.toUtf8(), numScores);
@@ -1750,27 +1601,27 @@ void Database::loadScores() {
                     sc.numHitObjects = -1;
                     sc.numCircles = -1;
 
-                    const unsigned char gamemode = read_u8(&db);
-                    sc.version = read_u32(&db);
+                    const unsigned char gamemode = read<u8>(&db);
+                    sc.version = read<u32>(&db);
                     skip_string(&db);  // beatmap hash (already have it)
 
                     sc.playerName = read_stdstring(&db);
                     skip_string(&db);  // replay hash (don't use it)
 
-                    sc.num300s = read_u16(&db);
-                    sc.num100s = read_u16(&db);
-                    sc.num50s = read_u16(&db);
-                    sc.numGekis = read_u16(&db);
-                    sc.numKatus = read_u16(&db);
-                    sc.numMisses = read_u16(&db);
+                    sc.num300s = read<u16>(&db);
+                    sc.num100s = read<u16>(&db);
+                    sc.num50s = read<u16>(&db);
+                    sc.numGekis = read<u16>(&db);
+                    sc.numKatus = read<u16>(&db);
+                    sc.numMisses = read<u16>(&db);
 
-                    i32 score = read_u32(&db);
+                    i32 score = read<u32>(&db);
                     sc.score = (score < 0 ? 0 : score);
 
-                    sc.comboMax = read_u16(&db);
-                    sc.perfect = read_u8(&db);
+                    sc.comboMax = read<u16>(&db);
+                    sc.perfect = read<u8>(&db);
 
-                    sc.modsLegacy = read_u32(&db);
+                    sc.modsLegacy = read<u32>(&db);
                     sc.speedMultiplier = 1.0f;
                     if(sc.modsLegacy & ModFlags::HalfTime)
                         sc.speedMultiplier = 0.75f;
@@ -1779,12 +1630,12 @@ void Database::loadScores() {
 
                     skip_string(&db);  // hp graph
 
-                    u64 full_tms = read_u64(&db);
+                    u64 full_tms = read<u64>(&db);
                     sc.unixTimestamp = (full_tms - 621355968000000000) / 10000000;
                     sc.legacyReplayTimestamp = full_tms - 504911232000000000;
 
                     // Always -1, but let's skip it properly just in case
-                    i32 old_replay_size = read_u32(&db);
+                    i32 old_replay_size = read<u32>(&db);
                     if(old_replay_size > 0) {
                         db.pos += old_replay_size;
                     }
@@ -1793,12 +1644,12 @@ void Database::loadScores() {
                     sc.has_replay = true;
 
                     if(sc.version >= 20140721)
-                        sc.online_score_id = read_u64(&db);
+                        sc.online_score_id = read<u64>(&db);
                     else if(sc.version >= 20121008)
-                        sc.online_score_id = read_u32(&db);
+                        sc.online_score_id = read<u32>(&db);
 
                     if(sc.modsLegacy & ModFlags::Target) /*double totalAccuracy = */
-                        read_f64(&db);
+                        read<f64>(&db);
 
                     if(gamemode == 0) {  // gamemode filter (osu!standard)
                         // runtime
@@ -1981,17 +1832,8 @@ DatabaseBeatmap *Database::loadRawBeatmap(std::string beatmapPath) {
 
     // build beatmap from diffs
     DatabaseBeatmap *beatmap = NULL;
-    {
-        if(diffs2.size() > 0) {
-            beatmap = new DatabaseBeatmap(m_osu, diffs2);
-
-            // and add entries in our hashmaps
-            for(size_t i = 0; i < diffs2.size(); i++) {
-                DatabaseBeatmap *diff2 = diffs2[i];
-                m_rawHashToDiff2[diff2->getMD5Hash()] = diff2;
-                m_rawHashToBeatmap[diff2->getMD5Hash()] = beatmap;
-            }
-        }
+    if(diffs2.size() > 0) {
+        beatmap = new DatabaseBeatmap(m_osu, std::move(diffs2));
     }
 
     return beatmap;

+ 5 - 4
src/App/Osu/Database.h

@@ -13,12 +13,14 @@ class OsuFile;
 class DatabaseBeatmap;
 class DatabaseLoader;
 
+// Field ordering matters here
+#pragma pack(push, 1)
 struct TIMINGPOINT {
     double msPerBeat;
     double offset;
     bool timingChange;
 };
-TIMINGPOINT read_timing_point(Packet *packet);
+#pragma pack(pop)
 
 Packet load_db(std::string path);
 bool save_db(Packet *db, std::string path);
@@ -95,6 +97,8 @@ class Database {
 
     DatabaseBeatmap *loadRawBeatmap(std::string beatmapPath);  // only used for raw loading without db
 
+    void loadDB(Packet *db, bool &fallbackToRawLoad);
+
    private:
     friend class DatabaseLoader;
 
@@ -105,7 +109,6 @@ class Database {
 
     std::string parseLegacyCfgBeatmapDirectoryParameter();
     void scheduleLoadRaw();
-    void loadDB(Packet *db, bool &fallbackToRawLoad);
 
     void loadStars();
     void saveStars();
@@ -146,8 +149,6 @@ class Database {
     std::string m_sRawBeatmapLoadOsuSongFolder;
     std::vector<std::string> m_rawBeatmapFolders;
     std::vector<std::string> m_rawLoadBeatmapFolders;
-    std::unordered_map<MD5Hash, DatabaseBeatmap *> m_rawHashToDiff2;
-    std::unordered_map<MD5Hash, DatabaseBeatmap *> m_rawHashToBeatmap;
 
     // stars.cache
     struct STARS_CACHE_ENTRY {

+ 43 - 203
src/App/Osu/DatabaseBeatmap.cpp

@@ -141,9 +141,9 @@ DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::string filePath, std::string fol
     m_iOnlineOffset = 0;
 }
 
-DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &difficulties)
+DatabaseBeatmap::DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &&difficulties)
     : DatabaseBeatmap(osu, "", "") {
-    setDifficulties(difficulties);
+    setDifficulties(std::move(difficulties));
 }
 
 DatabaseBeatmap::~DatabaseBeatmap() {
@@ -513,7 +513,7 @@ DatabaseBeatmap::PRIMITIVE_CONTAINER DatabaseBeatmap::loadPrimitiveObjects(const
 }
 
 DatabaseBeatmap::CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT DatabaseBeatmap::calculateSliderTimesClicksTicks(
-    int beatmapVersion, std::vector<SLIDER> &sliders, std::vector<TIMINGPOINT> &timingpoints, float sliderMultiplier,
+    int beatmapVersion, std::vector<SLIDER> &sliders, zarray<TIMINGPOINT> &timingpoints, float sliderMultiplier,
     float sliderTickRate) {
     std::atomic<bool> dead;
     dead = false;
@@ -522,7 +522,7 @@ DatabaseBeatmap::CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT DatabaseBeatmap::cal
 }
 
 DatabaseBeatmap::CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT DatabaseBeatmap::calculateSliderTimesClicksTicks(
-    int beatmapVersion, std::vector<SLIDER> &sliders, std::vector<TIMINGPOINT> &timingpoints, float sliderMultiplier,
+    int beatmapVersion, std::vector<SLIDER> &sliders, zarray<TIMINGPOINT> &timingpoints, float sliderMultiplier,
     float sliderTickRate, const std::atomic<bool> &dead) {
     CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT r;
     { r.errorCode = 0; }
@@ -942,7 +942,7 @@ bool DatabaseBeatmap::loadMetadata(DatabaseBeatmap *databaseBeatmap) {
     if(databaseBeatmap->m_difficulties.size() > 0) return false;  // we are just a container
 
     // reset
-    databaseBeatmap->m_timingpoints = std::vector<TIMINGPOINT>();
+    databaseBeatmap->m_timingpoints.clear();
 
     if(Osu::debug->getBool()) debugLog("DatabaseBeatmap::loadMetadata() : %s\n", databaseBeatmap->m_sFilePath.c_str());
 
@@ -1182,121 +1182,11 @@ bool DatabaseBeatmap::loadMetadata(DatabaseBeatmap *databaseBeatmap) {
                   TimingPointSortComparator());
 
         // calculate bpm range
-        float tempMinBPM = 0.0f;
-        float tempMaxBPM = std::numeric_limits<float>::max();
-        std::vector<TIMINGPOINT> uninheritedTimingpoints;
-        for(int i = 0; i < databaseBeatmap->m_timingpoints.size(); i++) {
-            const TIMINGPOINT &t = databaseBeatmap->m_timingpoints[i];
-
-            if(t.msPerBeat >= 0.0f)  // NOT inherited
-            {
-                uninheritedTimingpoints.push_back(t);
-
-                if(t.msPerBeat > tempMinBPM) tempMinBPM = t.msPerBeat;
-                if(t.msPerBeat < tempMaxBPM) tempMaxBPM = t.msPerBeat;
-            }
-        }
-
-        // convert from msPerBeat to BPM
-        const float msPerMinute = 1.0f * 60.0f * 1000.0f;
-        if(tempMinBPM != 0.0f) tempMinBPM = msPerMinute / tempMinBPM;
-        if(tempMaxBPM != 0.0f) tempMaxBPM = msPerMinute / tempMaxBPM;
-
-        databaseBeatmap->m_iMinBPM = (int)std::round(tempMinBPM);
-        databaseBeatmap->m_iMaxBPM = (int)std::round(tempMaxBPM);
-
-        struct MostCommonBPMHelper {
-            static int calculateMostCommonBPM(const std::vector<DatabaseBeatmap::TIMINGPOINT> &uninheritedTimingpoints,
-                                              long lastTime) {
-                if(uninheritedTimingpoints.size() < 1) return 0;
-
-                struct Tuple {
-                    float beatLength;
-                    long duration;
-
-                    size_t sortHack;
-                };
-
-                // "Construct a set of (beatLength, duration) tuples for each individual timing point."
-                std::vector<Tuple> tuples;
-                tuples.reserve(uninheritedTimingpoints.size());
-                for(size_t i = 0; i < uninheritedTimingpoints.size(); i++) {
-                    const DatabaseBeatmap::TIMINGPOINT &t = uninheritedTimingpoints[i];
-
-                    Tuple tuple;
-                    {
-                        if(t.offset > lastTime) {
-                            tuple.beatLength = std::round(t.msPerBeat * 1000.0f) / 1000.0f;
-                            tuple.duration = 0;
-                        } else {
-                            // "osu-stable forced the first control point to start at 0."
-                            // "This is reproduced here to maintain compatibility around osu!mania scroll speed and song
-                            // select display."
-                            const long currentTime = (i == 0 ? 0 : t.offset);
-                            const long nextTime =
-                                (i >= uninheritedTimingpoints.size() - 1 ? lastTime
-                                                                         : uninheritedTimingpoints[i + 1].offset);
-
-                            tuple.beatLength = std::round(t.msPerBeat * 1000.0f) / 1000.0f;
-                            tuple.duration = std::max(nextTime - currentTime, (long)0);
-                        }
-
-                        tuple.sortHack = i;
-                    }
-                    tuples.push_back(tuple);
-                }
-
-                // "Aggregate durations into a set of (beatLength, duration) tuples for each beat length"
-                std::vector<Tuple> aggregations;
-                aggregations.reserve(tuples.size());
-                for(size_t i = 0; i < tuples.size(); i++) {
-                    const Tuple &t = tuples[i];
-
-                    bool foundExistingAggregation = false;
-                    size_t aggregationIndex = 0;
-                    for(size_t j = 0; j < aggregations.size(); j++) {
-                        if(aggregations[j].beatLength == t.beatLength) {
-                            foundExistingAggregation = true;
-                            aggregationIndex = j;
-                            break;
-                        }
-                    }
-
-                    if(!foundExistingAggregation)
-                        aggregations.push_back(t);
-                    else
-                        aggregations[aggregationIndex].duration += t.duration;
-                }
-
-                // "Get the most common one, or 0 as a suitable default"
-                struct SortByDuration {
-                    bool operator()(Tuple const &a, Tuple const &b) const {
-                        // first condition: duration
-                        // second condition: if duration is the same, higher BPM goes before lower BPM
-
-                        // strict weak ordering!
-                        if(a.duration == b.duration && a.beatLength == b.beatLength)
-                            return a.sortHack > b.sortHack;
-                        else if(a.duration == b.duration)
-                            return (a.beatLength < b.beatLength);
-                        else
-                            return (a.duration > b.duration);
-                    }
-                };
-                std::sort(aggregations.begin(), aggregations.end(), SortByDuration());
-
-                float mostCommonBPM = aggregations[0].beatLength;
-                {
-                    // convert from msPerBeat to BPM
-                    const float msPerMinute = 1.0f * 60.0f * 1000.0f;
-                    if(mostCommonBPM != 0.0f) mostCommonBPM = msPerMinute / mostCommonBPM;
-                }
-                return (int)std::round(mostCommonBPM);
-            }
-        };
-        databaseBeatmap->m_iMostCommonBPM = MostCommonBPMHelper::calculateMostCommonBPM(
-            uninheritedTimingpoints,
-            databaseBeatmap->m_timingpoints[databaseBeatmap->m_timingpoints.size() - 1].offset);
+        auto bpm = getBPM(databaseBeatmap->m_timingpoints,
+                          databaseBeatmap->m_timingpoints[databaseBeatmap->m_timingpoints.size() - 1].offset);
+        databaseBeatmap->m_iMinBPM = bpm.min;
+        databaseBeatmap->m_iMaxBPM = bpm.max;
+        databaseBeatmap->m_iMostCommonBPM = bpm.most_common;
     }
 
     // special case: old beatmaps have AR = OD, there is no ApproachRate stored
@@ -1576,52 +1466,47 @@ DatabaseBeatmap::LOAD_GAMEPLAY_RESULT DatabaseBeatmap::loadGameplay(DatabaseBeat
     return result;
 }
 
-void DatabaseBeatmap::setDifficulties(std::vector<DatabaseBeatmap *> &difficulties) {
-    m_difficulties = difficulties;
-
-    if(m_difficulties.size() > 0) {
-        // set representative values for this container (i.e. use values from first difficulty)
-        m_sTitle = m_difficulties[0]->m_sTitle;
-        m_sArtist = m_difficulties[0]->m_sArtist;
-        m_sCreator = m_difficulties[0]->m_sCreator;
-        m_sBackgroundImageFileName = m_difficulties[0]->m_sBackgroundImageFileName;
-
-        // also precalculate some largest representative values
-        m_iLengthMS = 0;
-        m_fCS = 0.0f;
-        m_fAR = 0.0f;
-        m_fOD = 0.0f;
-        m_fHP = 0.0f;
-        m_fStarsNomod = 0.0f;
-        m_iMinBPM = std::numeric_limits<int>::max();
-        m_iMaxBPM = 0;
-        m_iMostCommonBPM = 0;
-        last_modification_time = 0;
-        for(size_t i = 0; i < m_difficulties.size(); i++) {
-            if(m_difficulties[i]->getLengthMS() > m_iLengthMS) m_iLengthMS = m_difficulties[i]->getLengthMS();
-            if(m_difficulties[i]->getCS() > m_fCS) m_fCS = m_difficulties[i]->getCS();
-            if(m_difficulties[i]->getAR() > m_fAR) m_fAR = m_difficulties[i]->getAR();
-            if(m_difficulties[i]->getHP() > m_fHP) m_fHP = m_difficulties[i]->getHP();
-            if(m_difficulties[i]->getOD() > m_fOD) m_fOD = m_difficulties[i]->getOD();
-            if(m_difficulties[i]->getStarsNomod() > m_fStarsNomod) m_fStarsNomod = m_difficulties[i]->getStarsNomod();
-            if(m_difficulties[i]->getMinBPM() < m_iMinBPM) m_iMinBPM = m_difficulties[i]->getMinBPM();
-            if(m_difficulties[i]->getMaxBPM() > m_iMaxBPM) m_iMaxBPM = m_difficulties[i]->getMaxBPM();
-            if(m_difficulties[i]->getMostCommonBPM() > m_iMostCommonBPM)
-                m_iMostCommonBPM = m_difficulties[i]->getMostCommonBPM();
-            if(m_difficulties[i]->last_modification_time > last_modification_time)
-                last_modification_time = m_difficulties[i]->last_modification_time;
-        }
+void DatabaseBeatmap::setDifficulties(std::vector<DatabaseBeatmap *> &&difficulties) {
+    m_difficulties = std::move(difficulties);
+    if(m_difficulties.empty()) return;
+
+    // set representative values for this container (i.e. use values from first difficulty)
+    m_sTitle = m_difficulties[0]->m_sTitle;
+    m_sArtist = m_difficulties[0]->m_sArtist;
+    m_sCreator = m_difficulties[0]->m_sCreator;
+    m_sBackgroundImageFileName = m_difficulties[0]->m_sBackgroundImageFileName;
+
+    // also precalculate some largest representative values
+    m_iLengthMS = 0;
+    m_fCS = 0.0f;
+    m_fAR = 0.0f;
+    m_fOD = 0.0f;
+    m_fHP = 0.0f;
+    m_fStarsNomod = 0.0f;
+    m_iMinBPM = std::numeric_limits<int>::max();
+    m_iMaxBPM = 0;
+    m_iMostCommonBPM = 0;
+    last_modification_time = 0;
+    for(auto diff : m_difficulties) {
+        if(diff->getLengthMS() > m_iLengthMS) m_iLengthMS = diff->getLengthMS();
+        if(diff->getCS() > m_fCS) m_fCS = diff->getCS();
+        if(diff->getAR() > m_fAR) m_fAR = diff->getAR();
+        if(diff->getHP() > m_fHP) m_fHP = diff->getHP();
+        if(diff->getOD() > m_fOD) m_fOD = diff->getOD();
+        if(diff->getStarsNomod() > m_fStarsNomod) m_fStarsNomod = diff->getStarsNomod();
+        if(diff->getMinBPM() < m_iMinBPM) m_iMinBPM = diff->getMinBPM();
+        if(diff->getMaxBPM() > m_iMaxBPM) m_iMaxBPM = diff->getMaxBPM();
+        if(diff->getMostCommonBPM() > m_iMostCommonBPM) m_iMostCommonBPM = diff->getMostCommonBPM();
+        if(diff->last_modification_time > last_modification_time) last_modification_time = diff->last_modification_time;
     }
 }
 
-void DatabaseBeatmap::updateSetHeuristics() { setDifficulties(m_difficulties); }
-
 DatabaseBeatmap::TIMING_INFO DatabaseBeatmap::getTimingInfoForTime(unsigned long positionMS) {
     return getTimingInfoForTimeAndTimingPoints(positionMS, m_timingpoints);
 }
 
 DatabaseBeatmap::TIMING_INFO DatabaseBeatmap::getTimingInfoForTimeAndTimingPoints(
-    unsigned long positionMS, std::vector<TIMINGPOINT> &timingpoints) {
+    unsigned long positionMS, const zarray<TIMINGPOINT> &timingpoints) {
     TIMING_INFO ti;
     ti.offset = 0;
     ti.beatLengthBase = 1;
@@ -1679,51 +1564,6 @@ DatabaseBeatmap::TIMING_INFO DatabaseBeatmap::getTimingInfoForTimeAndTimingPoint
         ti.sampleSet = timingpoints[audioPoint].sampleSet;
     }
 
-    // old (McKay's algorithm)
-    // (doesn't work for all aspire maps, e.g. XNOR)
-    /*
-    // initial timing values (get first non-inherited timingpoint as base)
-    for (int i=0; i<timingpoints.size(); i++)
-    {
-            TIMINGPOINT *t = &timingpoints[i];
-            if (t->msPerBeat >= 0)
-            {
-                    ti.beatLength = t->msPerBeat;
-                    ti.beatLengthBase = ti.beatLength;
-                    ti.offset = t->offset;
-                    break;
-            }
-    }
-
-    // go through all timingpoints before positionMS
-    for (int i=0; i<timingpoints.size(); i++)
-    {
-            TIMINGPOINT *t = &timingpoints[i];
-            if (t->offset > (long)positionMS)
-                    break;
-
-            //debugLog("timingpoint %i msperbeat = %f\n", i, t->msPerBeat);
-
-            if (t->msPerBeat >= 0) // NOT inherited
-            {
-                    ti.beatLengthBase = t->msPerBeat;
-                    ti.beatLength = ti.beatLengthBase;
-                    ti.offset = t->offset;
-            }
-            else // inherited
-            {
-                    // note how msPerBeat is clamped
-                    ti.isNaN = std::isnan(t->msPerBeat);
-                    ti.beatLength = ti.beatLengthBase * (clamp<float>(!ti.isNaN ? std::abs(t->msPerBeat) : 1000, 10,
-    1000) / 100.0f); // sliderMultiplier of a timingpoint = (t->velocity / -100.0f)
-            }
-
-            ti.volume = t->volume;
-            ti.sampleType = t->sampleType;
-            ti.sampleSet = t->sampleSet;
-    }
-    */
-
     return ti;
 }
 

+ 89 - 24
src/App/Osu/DatabaseBeatmap.h

@@ -1,13 +1,4 @@
-//================ Copyright (c) 2020, PG, All rights reserved. =================//
-//
-// Purpose:		loader + container for raw beatmap files/data (v2 rewrite)
-//
-// $NoKeywords: $osudiff
-//===============================================================================//
-
-#ifndef OSUDATABASEBEATMAP_H
-#define OSUDATABASEBEATMAP_H
-
+#pragma once
 #include "DifficultyCalculator.h"
 #include "Osu.h"
 #include "Resource.h"
@@ -96,7 +87,7 @@ class DatabaseBeatmap {
     };
 
     DatabaseBeatmap(Osu *osu, std::string filePath, std::string folder, bool filePathIsInMemoryBeatmap = false);
-    DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &difficulties);
+    DatabaseBeatmap(Osu *osu, std::vector<DatabaseBeatmap *> &&difficulties);
     ~DatabaseBeatmap();
 
     static LOAD_DIFFOBJ_RESULT loadDifficultyHitObjects(const std::string &osuFilePath, float AR, float CS,
@@ -107,7 +98,7 @@ class DatabaseBeatmap {
     static bool loadMetadata(DatabaseBeatmap *databaseBeatmap);
     static LOAD_GAMEPLAY_RESULT loadGameplay(DatabaseBeatmap *databaseBeatmap, Beatmap *beatmap);
 
-    void setDifficulties(std::vector<DatabaseBeatmap *> &difficulties);
+    void setDifficulties(std::vector<DatabaseBeatmap *> &&difficulties);
 
     void setLengthMS(unsigned long lengthMS) { m_iLengthMS = lengthMS; }
 
@@ -120,8 +111,6 @@ class DatabaseBeatmap {
 
     void setLocalOffset(long localOffset) { m_iLocalOffset = localOffset; }
 
-    void updateSetHeuristics();
-
     inline Osu *getOsu() const { return m_osu; }
 
     inline std::string getFolder() const { return m_sFolder; }
@@ -135,7 +124,7 @@ class DatabaseBeatmap {
 
     TIMING_INFO getTimingInfoForTime(unsigned long positionMS);
     static TIMING_INFO getTimingInfoForTimeAndTimingPoints(unsigned long positionMS,
-                                                           std::vector<TIMINGPOINT> &timingpoints);
+                                                           const zarray<TIMINGPOINT> &timingpoints);
 
     // raw metadata
 
@@ -165,7 +154,7 @@ class DatabaseBeatmap {
     inline float getSliderTickRate() const { return m_fSliderTickRate; }
     inline float getSliderMultiplier() const { return m_fSliderMultiplier; }
 
-    inline const std::vector<TIMINGPOINT> &getTimingpoints() const { return m_timingpoints; }
+    inline const zarray<TIMINGPOINT> &getTimingpoints() const { return m_timingpoints; }
 
     std::string getFullSoundFilePath();
 
@@ -223,7 +212,7 @@ class DatabaseBeatmap {
     float m_fSliderTickRate;
     float m_fSliderMultiplier;
 
-    std::vector<TIMINGPOINT> m_timingpoints;  // necessary for main menu anim
+    zarray<TIMINGPOINT> m_timingpoints;  // necessary for main menu anim
 
     // redundant data (technically contained in metadata, but precomputed anyway)
 
@@ -295,7 +284,7 @@ class DatabaseBeatmap {
         std::vector<SPINNER> spinners;
         std::vector<BREAK> breaks;
 
-        std::vector<TIMINGPOINT> timingpoints;
+        zarray<TIMINGPOINT> timingpoints;
         std::vector<Color> combocolors;
 
         float stackLeniency;
@@ -328,12 +317,14 @@ class DatabaseBeatmap {
                                                     bool filePathIsInMemoryBeatmap = false);
     static PRIMITIVE_CONTAINER loadPrimitiveObjects(const std::string &osuFilePath, bool filePathIsInMemoryBeatmap,
                                                     const std::atomic<bool> &dead);
+    static CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT calculateSliderTimesClicksTicks(int beatmapVersion,
+                                                                                      std::vector<SLIDER> &sliders,
+                                                                                      zarray<TIMINGPOINT> &timingpoints,
+                                                                                      float sliderMultiplier,
+                                                                                      float sliderTickRate);
     static CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT calculateSliderTimesClicksTicks(
-        int beatmapVersion, std::vector<SLIDER> &sliders, std::vector<TIMINGPOINT> &timingpoints,
-        float sliderMultiplier, float sliderTickRate);
-    static CALCULATE_SLIDER_TIMES_CLICKS_TICKS_RESULT calculateSliderTimesClicksTicks(
-        int beatmapVersion, std::vector<SLIDER> &sliders, std::vector<TIMINGPOINT> &timingpoints,
-        float sliderMultiplier, float sliderTickRate, const std::atomic<bool> &dead);
+        int beatmapVersion, std::vector<SLIDER> &sliders, zarray<TIMINGPOINT> &timingpoints, float sliderMultiplier,
+        float sliderTickRate, const std::atomic<bool> &dead);
 
     Osu *m_osu;
 
@@ -442,4 +433,78 @@ class DatabaseBeatmapStarCalculator : public Resource {
     int m_iMaxPossibleCombo;
 };
 
-#endif
+struct BPMInfo {
+    u32 min;
+    u32 max;
+    u32 most_common;
+};
+
+template <typename T>
+struct BPMInfo getBPM(const zarray<T> &timing_points, long lastTime) {
+    if(timing_points.size() < 1) {
+        return BPMInfo{
+            .min = 0,
+            .max = 0,
+            .most_common = 0,
+        };
+    }
+
+    struct Tuple {
+        u32 bpm;
+        u32 duration;
+    };
+
+    zarray<Tuple> bpms;
+    bpms.reserve(timing_points.size());
+
+    for(size_t i = 0; i < timing_points.size(); i++) {
+        const T &t = timing_points[i];
+        if(t.offset > lastTime) break;
+        if(t.msPerBeat < 0) continue;
+
+        // "osu-stable forced the first control point to start at 0."
+        // "This is reproduced here to maintain compatibility around osu!mania scroll speed and song
+        // select display."
+        const long currentTime = (i == 0 ? 0 : t.offset);
+        const long nextTime = (i >= timing_points.size() - 1 ? lastTime : timing_points[i + 1].offset);
+
+        u32 bpm = t.msPerBeat / 60000;
+        u32 duration = std::max(nextTime - currentTime, (long)0);
+
+        bool found = false;
+        for(auto tuple : bpms) {
+            if(tuple.bpm == bpm) {
+                tuple.duration += duration;
+                found = true;
+                break;
+            }
+        }
+
+        if(!found) {
+            bpms.push_back(Tuple{
+                .bpm = bpm,
+                .duration = duration,
+            });
+        }
+    }
+
+    u32 min = 9001;
+    u32 max = 0;
+    u32 mostCommonBPM = 0;
+    u32 longestDuration = 0;
+    for(auto tuple : bpms) {
+        if(tuple.bpm > max) max = tuple.bpm;
+        if(tuple.bpm < min) min = tuple.bpm;
+
+        if(tuple.duration > longestDuration) {
+            longestDuration = tuple.duration;
+            mostCommonBPM = tuple.bpm;
+        }
+    }
+
+    return BPMInfo{
+        .min = min,
+        .max = max,
+        .most_common = mostCommonBPM,
+    };
+}

+ 14 - 14
src/App/Osu/Replay.cpp

@@ -149,30 +149,30 @@ Replay::Info Replay::from_bytes(u8* data, int s_data) {
     replay.memory = data;
     replay.size = s_data;
 
-    info.gamemode = read_u8(&replay);
+    info.gamemode = read<u8>(&replay);
     if(info.gamemode != 0) {
         debugLog("Replay has unexpected gamemode %d!", info.gamemode);
         return info;
     }
 
-    info.osu_version = read_u32(&replay);
+    info.osu_version = read<u32>(&replay);
     info.diff2_md5 = read_string(&replay);
     info.username = read_string(&replay);
     info.replay_md5 = read_string(&replay);
-    info.num300s = read_u16(&replay);
-    info.num100s = read_u16(&replay);
-    info.num50s = read_u16(&replay);
-    info.numGekis = read_u16(&replay);
-    info.numKatus = read_u16(&replay);
-    info.numMisses = read_u16(&replay);
-    info.score = read_u32(&replay);
-    info.comboMax = read_u16(&replay);
-    info.perfect = read_u8(&replay);
-    info.mod_flags = read_u32(&replay);
+    info.num300s = read<u16>(&replay);
+    info.num100s = read<u16>(&replay);
+    info.num50s = read<u16>(&replay);
+    info.numGekis = read<u16>(&replay);
+    info.numKatus = read<u16>(&replay);
+    info.numMisses = read<u16>(&replay);
+    info.score = read<u32>(&replay);
+    info.comboMax = read<u16>(&replay);
+    info.perfect = read<u8>(&replay);
+    info.mod_flags = read<u32>(&replay);
     info.life_bar_graph = read_string(&replay);
-    info.timestamp = read_u64(&replay) / 10;
+    info.timestamp = read<u64>(&replay) / 10;
 
-    i32 replay_size = read_u32(&replay);
+    i32 replay_size = read<u32>(&replay);
     if(replay_size <= 0) return info;
     auto replay_data = new u8[replay_size];
     read_bytes(&replay, replay_data, replay_size);

+ 18 - 18
src/App/Osu/RoomScreen.cpp

@@ -699,29 +699,29 @@ void RoomScreen::on_match_started(Room room) {
 }
 
 void RoomScreen::on_match_score_updated(Packet *packet) {
-    i32 update_tms = read_u32(packet);
-    u8 slot_id = read_u8(packet);
+    i32 update_tms = read<u32>(packet);
+    u8 slot_id = read<u8>(packet);
     if(slot_id > 15) return;
 
     auto slot = &bancho.room.slots[slot_id];
     slot->last_update_tms = update_tms;
-    slot->num300 = read_u16(packet);
-    slot->num100 = read_u16(packet);
-    slot->num50 = read_u16(packet);
-    slot->num_geki = read_u16(packet);
-    slot->num_katu = read_u16(packet);
-    slot->num_miss = read_u16(packet);
-    slot->total_score = read_u32(packet);
-    slot->max_combo = read_u16(packet);
-    slot->current_combo = read_u16(packet);
-    slot->is_perfect = read_u8(packet);
-    slot->current_hp = read_u8(packet);
-    slot->tag = read_u8(packet);
-
-    bool is_scorev2 = read_u8(packet);
+    slot->num300 = read<u16>(packet);
+    slot->num100 = read<u16>(packet);
+    slot->num50 = read<u16>(packet);
+    slot->num_geki = read<u16>(packet);
+    slot->num_katu = read<u16>(packet);
+    slot->num_miss = read<u16>(packet);
+    slot->total_score = read<u32>(packet);
+    slot->max_combo = read<u16>(packet);
+    slot->current_combo = read<u16>(packet);
+    slot->is_perfect = read<u8>(packet);
+    slot->current_hp = read<u8>(packet);
+    slot->tag = read<u8>(packet);
+
+    bool is_scorev2 = read<u8>(packet);
     if(is_scorev2) {
-        slot->sv2_combo = read_f64(packet);
-        slot->sv2_bonus = read_f64(packet);
+        slot->sv2_combo = read<f64>(packet);
+        slot->sv2_bonus = read<f64>(packet);
     }
 
     bancho.osu->m_hud->updateScoreboard(true);

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

@@ -1087,7 +1087,17 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
                     readdBeatmap(calculatedDiff);
 
                     // and update wrapper representative values (parent)
-                    if(m_backgroundStarCalcTempParent != NULL) m_backgroundStarCalcTempParent->updateSetHeuristics();
+                    if(m_backgroundStarCalcTempParent != NULL) {
+                        // lol @ useless getter setters
+                        m_backgroundStarCalcTempParent->setLengthMS(0);
+                        m_backgroundStarCalcTempParent->setStarsNoMod(0.0f);
+                        for(auto diff : m_backgroundStarCalcTempParent->getDifficulties()) {
+                            if(diff->getLengthMS() > m_backgroundStarCalcTempParent->getLengthMS())
+                                m_backgroundStarCalcTempParent->setLengthMS(diff->getLengthMS());
+                            if(diff->getStarsNomod() > m_backgroundStarCalcTempParent->getStarsNomod())
+                                m_backgroundStarCalcTempParent->setStarsNoMod(diff->getStarsNomod());
+                        }
+                    }
 
                     m_backgroundStarCalculator->kill();
                 }

+ 71 - 2
src/Util/cbase.h

@@ -3,11 +3,12 @@
 // STD INCLUDES
 
 #include <math.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
 
 #include <algorithm>
 #include <atomic>
-#include <cstdarg>
-#include <cstdint>
 #include <fstream>
 #include <functional>
 #include <iostream>
@@ -122,3 +123,71 @@ inline bool isInt(float f) { return (f == static_cast<float>(static_cast<int>(f)
 inline unsigned long &floatBits(float &f) { return *reinterpret_cast<unsigned long *>(&f); }
 
 inline bool isFinite(float f) { return ((floatBits(f) & 0x7F800000) != 0x7F800000); }
+
+// zero-initialized dynamic array, similar to std::vector but way faster when you don't need constructors
+// obviously don't use it on complex types :)
+template <class T>
+struct zarray {
+    zarray(size_t nb_initial = 0) {
+        if(nb_initial > 0) {
+            reserve(nb_initial);
+            nb = nb_initial;
+        }
+    }
+
+    void push_back(T t) {
+        if(nb + 1 > max) {
+            reserve(max + max / 2 + 1);
+        }
+
+        memory[nb] = t;
+        nb++;
+    }
+
+    void reserve(size_t new_max) {
+        if(max == 0) {
+            memory = (T *)calloc(new_max, sizeof(T));
+        } else {
+            memory = (T *)reallocarray(memory, new_max, sizeof(T));
+            memset(&memory[max], 0, (new_max - max) * sizeof(T));
+        }
+
+        max = new_max;
+    }
+
+    void resize(size_t new_nb) {
+        if(new_nb < nb) {
+            memset(&memory[new_nb], 0, (nb - new_nb) * sizeof(T));
+        } else if(new_nb > max) {
+            reserve(new_nb);
+        }
+
+        nb = new_nb;
+    }
+
+    void swap(zarray<T> other) {
+        size_t omax = max;
+        size_t onb = nb;
+        T *omemory = memory;
+
+        max = other.max;
+        nb = other.nb;
+        memory = other.memory;
+
+        other.max = omax;
+        other.nb = onb;
+        other.memory = omemory;
+    }
+
+    T &operator[](size_t index) const { return memory[index]; }
+    void clear() { nb = 0; }
+    T *begin() const { return memory; }
+    T *data() { return memory; }
+    T *end() const { return &memory[nb]; }
+    size_t size() const { return nb; }
+
+   private:
+    size_t max = 0;
+    size_t nb = 0;
+    T *memory = nullptr;
+};