mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-19 14:45:25 +02:00
Music: show the stations the content cache defines, with their names and art
The music menu listed only the playlists the game's own music assets declare. The licensed stations (San Van Soundtrack, 80's Mixtape, ...) live in the content cache's music chunk: a record with the station id (field 1), its tracks (repeated field 2, "Artist - Title") and a message with its display name and cdn:/ artwork (field 10, parts 10 and 11). Read them into Catalogs, merge their membership into the music catalogue (only songs the game registered, so no unknown id is referenced), and send the name and artwork with each playlist. The catalogue test pins two stations from the cache. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6dcbe96620
commit
2e8bb57d9e
7 files changed
+106
-5
No files matched your search
@@ -62,6 +62,15 @@ int main(int argc, char** argv) {
|
||||
check(stadium != travel.access_points.end() && stadium->second == std::vector<std::string>{"location_stadium"},
|
||||
"stadium entrance offers only the first stadium");
|
||||
check(travel.access_points.size() == 5, "five access points");
|
||||
check(catalogs.music_playlists.size() == 54, "54 music playlists");
|
||||
if (const auto station = catalogs.music_playlists.find("0_Licensed_All"); station != catalogs.music_playlists.end())
|
||||
check(station->second.name == "San Van Soundtrack" && station->second.tracks.size() == 107 &&
|
||||
station->second.tracks.front() == "2hollis - flash" &&
|
||||
station->second.artwork == "cdn:/0e63e2111f9088448ae261ab0f930a4a", "San Van Soundtrack station");
|
||||
else check(false, "San Van Soundtrack station");
|
||||
if (const auto ballroom = catalogs.music_playlists.find("1920sBallroom"); ballroom != catalogs.music_playlists.end())
|
||||
check(ballroom->second.name == "Ballroom 237" && ballroom->second.tracks.size() == 3, "Ballroom station");
|
||||
else check(false, "Ballroom station");
|
||||
if (argc > 2) {
|
||||
Json dump{{"items", catalogs.items}, {"challenges", catalogs.challenges}, {"entitlements", Json::array()}};
|
||||
for (const auto& id : catalogs.entitlements) dump["entitlements"].push_back(id);
|
||||
|
||||
@@ -54,6 +54,30 @@ void add_item(Catalogs& out, const Fields& record) {
|
||||
out.items[key] = std::move(entry);
|
||||
}
|
||||
|
||||
// Music playlists: field 1 names the playlist, repeated field 2 lists tracks
|
||||
// ("Artist - Title"), and field 10 is a message carrying the display name and
|
||||
// artwork. Owned-item records put a varint in field 10, so requiring the byte
|
||||
// wire separates the two. First record for an id wins.
|
||||
void add_music_playlist(Catalogs& out, const std::string& id, const Fields& record) {
|
||||
const auto content = message(record, 10);
|
||||
if (!content) return;
|
||||
const auto name = text(*content, 10);
|
||||
if (!name) return;
|
||||
std::vector<std::string> tracks;
|
||||
for (const auto& field : record) {
|
||||
if (field.number != 2 || field.wire != Wire::bytes) continue;
|
||||
const auto value = text(&field);
|
||||
if (!value || value->find(" - ") == std::string::npos) continue;
|
||||
tracks.push_back(*value);
|
||||
}
|
||||
if (tracks.size() < 2) return;
|
||||
Catalogs::MusicPlaylistEntry entry;
|
||||
entry.name = *name;
|
||||
entry.artwork = text(*content, 11).value_or("");
|
||||
entry.tracks = std::move(tracks);
|
||||
out.music_playlists.try_emplace(id, std::move(entry));
|
||||
}
|
||||
|
||||
// Grant lists: repeated `number` entries, each {1: kind, 2: id}.
|
||||
struct Grant { std::string kind, id; };
|
||||
std::vector<Grant> grants(const Fields& fields, std::uint32_t number) {
|
||||
@@ -221,6 +245,7 @@ Catalogs read_catalogs(const std::filesystem::path& folder) {
|
||||
if (!id) continue;
|
||||
if (text(*fields, 3) == "entitlement") entitlements.insert(*id);
|
||||
add_item(out, *fields);
|
||||
add_music_playlist(out, *id, *fields);
|
||||
add_challenge(out, *id, *fields);
|
||||
add_object_category(out, *id, *fields);
|
||||
add_travel(out, *id, *fields);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Engine/Core/Json/json.h"
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -40,6 +41,14 @@ struct Catalogs {
|
||||
std::vector<TravelLocation> travel_locations;
|
||||
// Access point id (accesspoint_*) -> the location ids it offers.
|
||||
std::vector<std::pair<std::string, std::vector<std::string>>> travel_access_points;
|
||||
// Music playlist id -> {display name, artwork id, track ids} from the music
|
||||
// chunk. A music record carries a field-10 message (10.10 name, 10.11
|
||||
// artwork) and repeated field-2 tracks; owned items use a varint field 10.
|
||||
struct MusicPlaylistEntry {
|
||||
std::string name, artwork;
|
||||
std::vector<std::string> tracks;
|
||||
};
|
||||
std::map<std::string, MusicPlaylistEntry, std::less<>> music_playlists;
|
||||
bool reserved(const std::string& key) const;
|
||||
};
|
||||
// Read once, on first use. Empty (available == false) when no pack is installed.
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "Extension/Customization/local_customization_runtime.h"
|
||||
#include "local_music_assets.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/local_music.h"
|
||||
#include "Engine/Vfs/content_catalogs.h"
|
||||
#include "Extension/Profile/runtime_internal.h"
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
// Snapshot the game's registered music assets, as the cosmetic catalog does
|
||||
@@ -102,6 +106,46 @@ bool read_music_catalog(MusicCatalog& result) {
|
||||
if (visited.size() != count || !read(base + addr::local_music::asset_manager, current) || current != manager ||
|
||||
!read(manager + 0x78, current_buckets) || current_buckets != buckets ||
|
||||
!read(manager + 0x84, current_count) || current_count != count) return false;
|
||||
// The registered assets only declare the playlists the game ships offline
|
||||
// (the Universal production library); the licensed stations live in the
|
||||
// content-cache music chunk. Merge that membership, intersected with the
|
||||
// songs actually registered, so no unregistered id can ever be referenced.
|
||||
const auto native_playlists = playlists.size();
|
||||
const auto& cache = content_cache::catalogs();
|
||||
std::map<std::string, std::string, std::less<>> playlist_names, playlist_artwork;
|
||||
std::size_t cache_playlists = 0, matched = 0, unmatched = 0, added = 0;
|
||||
if (!cache.music_playlists.empty()) {
|
||||
std::map<std::string, std::size_t, std::less<>> index;
|
||||
for (std::size_t i = 0; i < snapshot.songs.size(); ++i) index.emplace(snapshot.songs[i].id, i);
|
||||
for (const auto& [id, entry] : cache.music_playlists) {
|
||||
++cache_playlists;
|
||||
auto& members = playlists[id];
|
||||
for (const auto& track : entry.tracks) {
|
||||
const auto found = index.find(track);
|
||||
if (found == index.end()) { ++unmatched; continue; }
|
||||
++matched;
|
||||
auto& song = snapshot.songs[found->second];
|
||||
if (std::find(song.playlists.begin(), song.playlists.end(), id) != song.playlists.end())
|
||||
continue;
|
||||
song.playlists.push_back(id);
|
||||
members.push_back(song.id);
|
||||
++added;
|
||||
}
|
||||
if (!members.empty() && !entry.name.empty()) playlist_names[id] = entry.name;
|
||||
if (!members.empty() && !entry.artwork.empty()) playlist_artwork[id] = entry.artwork;
|
||||
}
|
||||
for (auto it = playlists.begin(); it != playlists.end();)
|
||||
it = it->second.empty() ? playlists.erase(it) : std::next(it);
|
||||
}
|
||||
{
|
||||
std::ostringstream event;
|
||||
event << "{\"event\":\"music_catalog_membership\",\"songs\":" << snapshot.songs.size()
|
||||
<< ",\"native_playlists\":" << native_playlists
|
||||
<< ",\"cache_playlists\":" << cache_playlists
|
||||
<< ",\"matched\":" << matched << ",\"unmatched\":" << unmatched
|
||||
<< ",\"added\":" << added << ",\"playlists\":" << playlists.size() << "}";
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::music, event.str());
|
||||
}
|
||||
// Registry order is bucket order, not authored ordering. Use stable IDs for
|
||||
// deterministic UI order; do not claim this is the original service order.
|
||||
std::sort(snapshot.songs.begin(), snapshot.songs.end(), [](const auto& a, const auto& b) {
|
||||
@@ -109,7 +153,14 @@ bool read_music_catalog(MusicCatalog& result) {
|
||||
});
|
||||
for (auto& [id, songs] : playlists) {
|
||||
std::sort(songs.begin(), songs.end());
|
||||
snapshot.playlists.push_back({id, std::move(songs)});
|
||||
MusicPlaylist row;
|
||||
row.id = id;
|
||||
row.songs = std::move(songs);
|
||||
if (const auto found = playlist_names.find(id); found != playlist_names.end())
|
||||
row.name = found->second;
|
||||
if (const auto found = playlist_artwork.find(id); found != playlist_artwork.end())
|
||||
row.artwork = found->second;
|
||||
snapshot.playlists.push_back(std::move(row));
|
||||
}
|
||||
result = std::move(snapshot);
|
||||
return true;
|
||||
|
||||
@@ -9,6 +9,8 @@ struct MusicSong {
|
||||
|
||||
struct MusicPlaylist {
|
||||
std::string id;
|
||||
std::string name; // display name from the content cache; empty for native groups
|
||||
std::string artwork; // cdn:/ artwork id from the content cache; empty when unknown
|
||||
std::vector<std::string> songs;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/engine.h"
|
||||
#include "Engine/Game/Build/20260929/local_music.h"
|
||||
#include "Engine/Game/World/location_travel.h"
|
||||
#include "Extension/Profile/runtime_internal.h"
|
||||
|
||||
namespace dingosdk::profile_runtime {
|
||||
@@ -106,14 +107,18 @@ bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favo
|
||||
}
|
||||
|
||||
std::string music_ui_wire(std::string_view id, std::string_view artist, std::string_view title,
|
||||
const std::vector<std::string>* members) {
|
||||
const std::vector<std::string>* members, std::string_view name, std::string_view artwork) {
|
||||
std::string body, presentation, framed;
|
||||
cosmetic_wire_string(body, 1, id);
|
||||
if (members) {
|
||||
for (const auto& song : *members) cosmetic_wire_string(body, 2, song);
|
||||
// A local shelf choice for real authored groups, NOT recovered AMP classification.
|
||||
cosmetic_wire_number(body, 3, 1);
|
||||
cosmetic_wire_string(presentation, 10, id);
|
||||
// The catalogue's display name when known; the raw id otherwise.
|
||||
cosmetic_wire_string(presentation, 10, name.empty() ? id : name);
|
||||
// Artwork: the catalogue's cdn:/ id, resolved to the CDN rendition.
|
||||
if (const auto url = travel_artwork_url(artwork); !url.empty())
|
||||
cosmetic_wire_string(presentation, 11, url);
|
||||
cosmetic_wire_number(presentation, 12, 0);
|
||||
} else {
|
||||
cosmetic_wire_string(presentation, 10, artist);
|
||||
@@ -236,7 +241,7 @@ void update_music_catalog() {
|
||||
ui.functions.allocator[1] = arena;
|
||||
MusicUiMessages playlists, songs;
|
||||
playlists.items.reserve(catalog.playlists.size()); songs.items.reserve(catalog.songs.size());
|
||||
for (const auto& playlist : catalog.playlists) playlists.items.push_back(music_ui_message(music_ui_wire(playlist.id, {}, {}, &playlist.songs), true));
|
||||
for (const auto& playlist : catalog.playlists) playlists.items.push_back(music_ui_message(music_ui_wire(playlist.id, {}, {}, &playlist.songs, playlist.name, playlist.artwork), true));
|
||||
for (const auto& song : catalog.songs) songs.items.push_back(music_ui_message(music_ui_wire(song.id, song.artist, song.title, nullptr), false));
|
||||
auto& model = game::native_data().models; auto& field = game::native_data().models.field;
|
||||
if (!music_ui_current(*pending)) { retry = false; return; }
|
||||
|
||||
@@ -61,7 +61,7 @@ bool music_ui_text(std::string_view value, std::size_t limit = 255);
|
||||
bool music_ui_catalog_valid(const MusicCatalog& catalog, const std::string& favorites);
|
||||
|
||||
std::string music_ui_wire(std::string_view id, std::string_view artist, std::string_view title,
|
||||
const std::vector<std::string>* members);
|
||||
const std::vector<std::string>* members, std::string_view name = {}, std::string_view artwork = {});
|
||||
|
||||
CosmeticShared music_ui_message(const std::string& wire, bool playlist);
|
||||
|
||||
|
||||
Reference in new issue
Block a user