#include "content_catalogs.h" #include "content_cache.h" #include "Engine/Resource/protobuf_wire.h" #include #include #include #include namespace dingosdk::content_cache { namespace { using protobuf::Field; using protobuf::Wire; using Fields = std::vector; // Catalogue strings are UTF-8 text without control characters. std::optional text(const Field* field) { if (!field) return std::nullopt; const auto value = protobuf::text(field->bytes); if (std::any_of(value.begin(), value.end(), [](char c) { const auto byte = static_cast(c); return byte < 0x20 ? c != '\t' && c != '\n' && c != '\r' : byte == 0x7f; })) return std::nullopt; return std::string(value); } std::optional text(const Fields& fields, std::uint32_t number) { return text(protobuf::first(fields, number, Wire::bytes)); } std::optional message(const Fields& fields, std::uint32_t number) { const auto* field = protobuf::first(fields, number, Wire::bytes); return field ? protobuf::parse(field->bytes) : std::nullopt; } std::string lower(std::string value) { for (auto& c : value) if (c >= 'A' && c <= 'Z') c = static_cast(c + ('a' - 'A')); return value; } // Owned items: the presentation message (6) names the owned asset (6.10), the // title (6.2) and description (6.3); the rarity is field 23. First record wins. void add_item(Catalogs& out, const Fields& record) { const auto presentation = message(record, 6); if (!presentation) return; const auto asset = text(*presentation, 10); if (!asset || lower(asset->substr(0, 4)) != "own_") return; const auto key = lower(*asset); if (out.items.contains(key)) return; if (const auto* flag = protobuf::first(record, 10, Wire::varint); flag && flag->integer == 1) out.open_items.insert(key); Json entry = Json::object(); if (const auto description = text(*presentation, 3)) entry["description"] = *description; if (const auto rarity = text(record, 23)) entry["rarity_id"] = *rarity; if (const auto title = text(*presentation, 2)) entry["title"] = *title; if (const auto group = text(*presentation, 5)) entry["group"] = *group; if (const auto type = text(*presentation, 6)) entry["object_type"] = *type; 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 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)); } // Songs: field 1 is "Artist - Title" and field 10 a message with the artist (10), title (11) and // cdn:/ cover art (12). First record for an id wins. void add_music_song(Catalogs& out, const std::string& id, const Fields& record) { if (id.find(" - ") == std::string::npos) return; const auto content = message(record, 10); if (!content || !text(*content, 10) || !text(*content, 11)) return; if (const auto artwork = text(*content, 12); artwork && artwork->starts_with("cdn:/")) out.music_song_artwork.try_emplace(id, *artwork); } // Grant lists: repeated `number` entries, each {1: kind, 2: id}. struct Grant { std::string kind, id; }; std::vector grants(const Fields& fields, std::uint32_t number) { std::vector out; for (const auto& field : fields) { if (field.number != number || field.wire != Wire::bytes) continue; const auto list = protobuf::parse(field.bytes); if (!list) continue; for (const auto& entry : *list) { if (entry.number != 1 || entry.wire != Wire::bytes) continue; const auto grant = protobuf::parse(entry.bytes); if (!grant) continue; if (auto kind = text(*grant, 1), id = text(*grant, 2); kind && id) out.push_back({std::move(*kind), std::move(*id)}); } } return out; } bool free_offer(const Fields& offer) { const auto price = message(offer, 4); const auto costs = price ? grants(*price, 2) : std::vector{}; if (costs.empty() || !std::all_of(costs.begin(), costs.end(), [](const Grant& cost) { return cost.kind == "res" && (cost.id == "influence" || cost.id == "skatepass_currency"); })) return false; const auto prerequisites = grants(offer, 8); return std::none_of(prerequisites.begin(), prerequisites.end(), [](const Grant& prerequisite) { return lower(prerequisite.id).find("premium") != std::string::npos; }); } void add_source(std::set& open, const std::string& id, const Fields& record) { const auto open_grants = [&](std::uint32_t number) { for (const auto& grant : grants(record, number)) if (grant.kind == "own-create" && lower(grant.id.substr(0, 4)) == "own_") open.insert(lower(grant.id)); }; if (id.ends_with("Price")) { if (free_offer(record)) open_grants(11); return; } open_grants(8); const auto kind = text(record, 3).value_or(""); if (kind == "progression-item") open_grants(7); if (kind.find("-event-") != std::string::npos) open_grants(4); if (text(record, 4).value_or("").starts_with("neighbourhood_rank")) open_grants(20); } std::vector texts(const Fields& fields, std::uint32_t number) { std::vector result; for (const auto& field : fields) if (field.number == number && field.wire == Wire::bytes) if (auto value = text(&field)) result.push_back(std::move(*value)); return result; } std::uint32_t priority(const Fields& fields) { const auto* field = protobuf::first(fields, 2, Wire::varint); return field ? static_cast(field->integer) : 0; } // Category service records: id (1), priority (2), sub-categories (5, each with // id 1, priority 2 and presentation 6 holding the title in 1), tags (6) and a // presentation (7) with the title (1) and icon (3). Build-kit categories carry // the "buildkit" tag; the Object Browser lists the ones tagged "qdbuildkit". void add_object_category(Catalogs& out, const std::string& id, const Fields& record) { const auto tags = texts(record, 6); if (std::find(tags.begin(), tags.end(), "buildkit") == tags.end()) return; const auto presentation = message(record, 7); if (!presentation) return; ObjectCategory category{id, text(*presentation, 1).value_or(id), text(*presentation, 3).value_or(""), priority(record), std::find(tags.begin(), tags.end(), "qdbuildkit") != tags.end(), {}}; for (const auto& field : record) { if (field.number != 5 || field.wire != Wire::bytes) continue; const auto group = protobuf::parse(field.bytes); if (!group) return; const auto group_id = text(*group, 1); if (!group_id) return; const auto title = message(*group, 6); category.groups.push_back({*group_id, (title ? text(*title, 1) : std::nullopt).value_or(*group_id), priority(*group)}); } for (const auto& known : out.object_categories) if (known.id == id) return; out.object_categories.push_back(std::move(category)); } // Challenges are the records with goals (21). Type is field 2, neighbourhood 7, // the title key 30.10 (30.11 is the type's name, 30.12 the short description). Each goal has its id (1), order (2), optional flag (5) // and text keys in 10: title 10, feed 11, short description 12, description 13. void add_challenge(Catalogs& out, const std::string& id, const Fields& record) { struct Goal { std::uint64_t order; Json value; }; std::vector goals; for (const auto& field : record) { if (field.number != 21 || field.wire != Wire::bytes) continue; const auto goal = protobuf::parse(field.bytes); if (!goal) return; const auto goal_id = text(*goal, 1); if (!goal_id) return; const auto* order = protobuf::first(*goal, 2, Wire::varint); const auto* optional = protobuf::first(*goal, 5, Wire::varint); Json value{{"id", *goal_id}, {"optional", optional && optional->integer != 0}}; if (const auto keys = message(*goal, 10)) { for (const auto& [number, name] : {std::pair{10u, "title_key"}, {11u, "feed_key"}, {12u, "short_description_key"}, {13u, "description_key"}}) if (const auto key = text(*keys, number)) value[name] = *key; } goals.push_back({order ? order->integer : 0, std::move(value)}); } const auto type = text(record, 2); if (goals.empty() || !type) return; std::stable_sort(goals.begin(), goals.end(), [](const Goal& a, const Goal& b) { return a.order < b.order; }); Json entry{{"type", *type}, {"available", true}, {"asset", "activities/challenges/prefabs/" + lower(*type) + "/" + lower(id) + "_ecsprefab"}}; auto list = Json::array(); for (auto& goal : goals) list.push_back(std::move(goal.value)); entry["goals"] = std::move(list); if (const auto presentation = message(record, 30)) { if (const auto title = text(*presentation, 10)) entry["title_key"] = *title; if (const auto description = text(*presentation, 12)) entry["description_key"] = *description; } if (const auto neighborhood = text(record, 7)) entry["neighborhood"] = *neighborhood; out.challenges[id] = std::move(entry); } // Travel: locations carry the level (3) and a presentation (30) with the title // (2), description (3), image (5), medium (6) and white/black icons (7, 8). // Access points list their destinations in field 2. void add_travel(Catalogs& out, const std::string& id, const Fields& record) { if (id.starts_with("accesspoint_")) { for (const auto& known : out.travel_access_points) if (known.first == id) return; auto targets = texts(record, 2); if (!targets.empty()) out.travel_access_points.emplace_back(id, std::move(targets)); return; } if (!id.starts_with("location_")) return; const auto level = text(record, 3); const auto presentation = message(record, 30); if (!level || !presentation) return; for (const auto& known : out.travel_locations) if (known.id == id) return; const auto field = [&](std::uint32_t number) { return text(*presentation, number).value_or(""); }; out.travel_locations.push_back({id, *level, field(2), field(3), field(6), field(7), field(8), field(5)}); } std::vector read_file(const std::filesystem::path& path) { std::ifstream input(path, std::ios::binary); return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; } } Catalogs read_catalogs(const std::filesystem::path& folder) { Catalogs out; std::vector bodies; for (const auto& entry : std::filesystem::directory_iterator(folder)) if (entry.is_regular_file() && entry.path().extension() == L".cache") bodies.push_back(entry.path()); std::sort(bodies.begin(), bodies.end()); std::set entitlements; for (const auto& path : bodies) { const auto bytes = read_file(path); // Image bodies (DDS, PNG) and anything else that is not a framed // record stream is skipped as a whole. const auto records = protobuf::frames(bytes); if (!records) continue; for (const auto record : *records) { const auto fields = protobuf::parse(record); if (!fields) continue; const auto id = text(*fields, 1); if (!id) continue; if (text(*fields, 3) == "entitlement") entitlements.insert(*id); add_item(out, *fields); add_music_playlist(out, *id, *fields); add_music_song(out, *id, *fields); add_challenge(out, *id, *fields); add_object_category(out, *id, *fields); add_travel(out, *id, *fields); add_source(out.open_items, *id, *fields); } } out.entitlements.assign(entitlements.begin(), entitlements.end()); out.available = true; return out; } bool Catalogs::reserved(const std::string& key) const { if (!items.contains(key) || open_items.contains(key)) return false; return !key.starts_with("dev_") && key.find("_dev_") == std::string::npos && !key.ends_with("_dev"); } const Catalogs& catalogs() { static const Catalogs value = [] { try { if (installed()) return read_catalogs(directory()); } catch (...) {} return Catalogs{}; }(); return value; } }