Files
ReSkate/Extension/Customization/local_customization_runtime.h
AyeZeeBB 21d9b0d993 Profile: cosmetic ownership is a rule, not a list of every item ever seen
With "unlock all cosmetics" (and objects) on, the default, ReSkate wrote every installed item into
the profile as owned and never removed one, so every mod a player ever tried stayed in the save:
8765 cosmetics and 520 objects in one profile, past the limit that left a player with no items.

What is owned is now worked out from what is installed (installed_item_owned): a paid store copy
never, every other item while the kind's unlock option is on, and with it off what the profile
saved. The catalog given to the game, the owned list, saved outfits, the player card, the Build
object browser and the park editor all ask it. Nothing per item is saved any more, and a profile
with an option on drops the rows earlier versions saved, once, on the next start.

profile_migration_tests checks that drop on a copy of a real save: the rows stay with the options
off, are gone after a restart with them on, and nothing else in the profile changes.
2026-10-17 00:29:27 -05:00

128 lines
5.3 KiB
C++

#pragma once
#include "Extension/Profile/runtime_internal.h"
namespace dingosdk::profile_runtime {
struct CosmeticItemInfo {
std::uint32_t hash{};
std::vector<std::uint32_t> categories;
std::string category;
std::int32_t sort_priority{};
bool build_kit{};
};
struct CosmeticRuntime {
void (*initialize_starter)(bool, const void*, const void*){};
std::uintptr_t item_manager{};
std::uint32_t item_count{};
std::map<std::string, CosmeticItemInfo, std::less<>> items;
// Presets whose last load was refused. A save to one is refused too, so the
// standard skater the game shows in its place is never written over the
// outfit on disk.
std::set<std::string> blocked_loadouts;
// Saved outfit items that were not installed when their preset loaded (a
// disabled or removed costume mod). The slot shows its default this session;
// saves keep the original item unless the player changes that slot. Also
// saved slots the template did not have then (a mod that adds slots,
// switched off): those have no slot to show in, and saves carry them along.
struct HeldSlot { std::size_t recipe{}; profile::CosmeticSlot saved; std::string fallback; };
std::map<std::string, std::vector<HeldSlot>, std::less<>> held_slots;
std::set<std::string> diagnostics;
DWORD update_thread{};
// Bumped whenever `items` is rebuilt (a live mod apply changes what is
// installed); the published catalog is replaced when it falls behind.
std::uint64_t items_generation{};
std::uint64_t published_generation{};
// Items the last catalog built holds: `items` less any left out of it (make_cosmetic_catalog).
std::size_t catalog_items{};
std::uintptr_t published_manager{};
std::uintptr_t published_inventory{};
std::uintptr_t inventory_data_model{};
std::uint32_t published_inventory_count{};
bool catalog_failed{};
bool ownership_unavailable{};
std::int32_t observed_selection{-1};
};
CosmeticRuntime& cosmetic_runtime();
bool cosmetic_diagnostic(const char* operation, const char* reason, std::string_view id = {}) noexcept;
std::uint32_t cosmetic_hash(std::string_view value);
bool cosmetic_text(std::uintptr_t p, std::string& value);
bool cosmetic_array(std::uintptr_t p, std::size_t stride, std::size_t limit, std::uint32_t& count);
bool cosmetic_words(std::uintptr_t p, std::size_t limit, std::vector<std::uint32_t>& result);
bool refresh_cosmetic_catalog();
bool reserved_cosmetic(std::string_view asset);
bool reserved_cosmetic_hash(std::uint32_t hash);
// Whether the player owns an installed cosmetic or Build object (`build_kit`). A paid store copy
// never; with the kind's unlock option on (the default) every other one, with nothing per item
// saved; off, what the profile saved. Nothing while the content cache that names the paid copies
// is unavailable.
bool installed_item_owned(const profile::Snapshot& snapshot, std::string_view key, std::uint32_t hash, bool build_kit);
struct CosmeticNativeItem {
const char* asset{};
const std::uint32_t* parameters{};
std::uint32_t hash{}, slot{};
};
struct CosmeticNativeRecipe {
std::uintptr_t resource{};
const std::uint32_t* scalars{};
const CosmeticNativeItem* items{};
const std::uint32_t* transient_mask{};
std::uint32_t transient_word{};
std::uint8_t transient_flag{};
std::array<std::byte, 3> padding{};
};
static_assert(sizeof(CosmeticNativeItem) == 0x18 && sizeof(CosmeticNativeRecipe) == 0x28);
bool read_cosmetic_recipes(const void* vector, profile::CosmeticLoadout& result,
std::vector<CosmeticNativeRecipe>& native);
template<class T> struct CosmeticBorrowedArray {
std::vector<std::uint64_t> storage;
explicit CosmeticBorrowedArray(std::size_t count) : storage((8 + count * sizeof(T) + 7) / 8) {
const auto size = static_cast<std::uint32_t>(count);
std::memcpy(storage.data(), &size, 4);
std::memcpy(reinterpret_cast<std::byte*>(storage.data()) + 4, &size, 4);
}
T* data() { return reinterpret_cast<T*>(reinterpret_cast<std::byte*>(storage.data()) + 8); }
};
bool cosmetic_slot_categories(std::uintptr_t resource, std::map<std::uint32_t, std::uint32_t>& result);
struct StarterTemplate {
const char* name;
std::uint32_t key, version;
};
inline constexpr std::array starter_templates{
StarterTemplate{"CASRecipeTemplate", 2759515148U, 2},
StarterTemplate{"SkateboardRecipeTemplate", 1583459055U, 1},
StarterTemplate{"MiscUserDataRecipeTemplate", 995494888U, 1},
};
bool complete_starter_recipes(const profile::CosmeticLoadout& value);
bool initialize_missing_starter(const std::vector<CosmeticNativeRecipe>& recipes);
void initialize_starter_from_expression(std::uintptr_t vm, std::uint32_t pc) noexcept;
bool recover_card_only_preset(std::string_view id, const profile::CosmeticLoadout& saved,
const profile::CosmeticLoadout& defaults);
// The game calls this with a temporary of its own (a slot's default recipes) and
// builds the slot from what is left in it: the saved outfit when this returns
// true, the standard skater when it returns false. It asks once, when the slot
// is first needed after a level load, and there is no record to fill in later.
bool load_cosmetic_hook(const void* wrapper, void* destination);
void save_cosmetic_hook(std::uintptr_t manager, void* record, const char* raw_id, const char* reason, bool changed_only);
}