Files
ReSkate/Extension/Objects/local_object_runtime.cpp
T
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

151 lines
8.8 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/local_object.h"
#include "Extension/Customization/local_customization_runtime.h"
#include "Engine/Game/Abi/native_data.h"
#include "local_object_runtime.h"
#include "Extension/Profile/runtime_internal.h"
#include "Extension/Progression/local_entitlement_trigger_runtime.h"
#include "Extension/Objects/object_categories.h"
#include "Extension/Customization/local_customization.h"
namespace dingosdk::profile_runtime {
// Build-kit category subscription: the browser needs this separately from ownership.
ObjectRuntime& object_runtime() { static auto* r = new ObjectRuntime; return *r; }
bool local_object_browser_ready() noexcept {
PreserveError preserve;
if (!executing_expression || !local_runtime().active.load(std::memory_order_acquire)) return false;
std::uintptr_t instance{}, resource{}, current{};
std::uint32_t key{};
std::array<std::uint32_t, 10> layout{};
// Asked by every online check the game's flow scripts make: the running graph's key is
// peeked first, and the rest read only for the object browser's own graph.
if (!memory::peek(executing_expression + 0x38, current) || !memory::peek(current + 0x10, key) || key != 0x7f3a3f0b ||
!read(executing_expression + 0x30, instance) || !read(instance, resource) || current != resource ||
!read(resource + 0x20, layout) || layout != std::array<std::uint32_t, 10>{
80, 360, 127, 304, 5, 0, 1, 327704, 1441814, 16910336}) return false;
// This constructor can use the local category subscription already installed.
// Its callback waits for inventory; startup/backend and other UI graphs retain
// the offline result. No global connection state is changed.
dingosdk::logging::event(dingosdk::logging::Channel::objects, "{\"event\":\"local_object_browser_ready\"}");
return true;
}
void initialize_object_functions(std::uintptr_t base) {
auto& f = object_runtime().functions;
f.allocate = reinterpret_cast<decltype(f.allocate)>(base + object_allocate_contract.rva);
f.construct = reinterpret_cast<decltype(f.construct)>(base + object_ctor_contract.rva);
f.initialize = reinterpret_cast<decltype(f.initialize)>(base + object_register_contract.rva);
f.parse = reinterpret_cast<decltype(f.parse)>(base + object_parse_contract.rva);
f.release = reinterpret_cast<decltype(f.release)>(base + object_release_contract.rva);
}
// Read-only category service DTOs consumed synchronously by the native parser.
// Native conversion deep-copies strings and nested arrays into its result class.
bool accept_object_category(const void*, const void*) { return true; }
std::string object_title(const std::string& key, const dingosdk::Json& overrides) {
const auto metadata = profile::item_display_metadata(key, overrides);
if (metadata.contains("title") && metadata.at("title").is_string()) return metadata.at("title").string();
std::string title = key.starts_with("own_") ? key.substr(4) : key;
std::replace(title.begin(), title.end(), '_', ' ');
return title;
}
void fill_object_categories(std::uintptr_t result, const profile::Snapshot& snapshot) {
const auto section = snapshot.extensions.value("object_dropper", dingosdk::Json::object());
const auto catalog = section.value("catalog", dingosdk::Json::object());
std::vector<objects::InstalledObject> owned;
for (const auto& [key, info] : cosmetic_runtime().items)
if (info.build_kit && installed_item_owned(snapshot, key, info.hash, true))
owned.push_back({key, info.category, object_title(key, catalog.value(key, dingosdk::Json::object()))});
// Titles come from the cache's category records. Their icons are cdn:/ links
// that cannot load offline, and the browser would draw the link as text.
const auto layout = objects::browser_layout(owned, content_cache::catalogs());
std::vector<ObjectServiceCategory> categories(layout.size());
std::vector<std::vector<ObjectServiceSubcategory>> subcategories(layout.size());
// The DTOs borrow these strings until the native parser has copied them.
std::vector<std::vector<std::string>> icons(layout.size());
for (std::size_t index = 0; index < layout.size(); ++index) {
const auto& group = layout[index];
auto& parent = categories[index]; auto& children = subcategories[index];
parent.id.set(group.id); parent.title.set(group.title);
parent.hash = cosmetic_hash(group.id);
parent.priority = static_cast<std::uint32_t>(index);
children.resize(group.tiles.size());
icons[index].reserve(group.tiles.size());
for (std::size_t item = 0; item < group.tiles.size(); ++item) {
auto& child = children[item];
// Each subcategory produces one selectable tile. Its identity must
// match OwnableData.presentation.category (local_cosmetic_catalog.cpp).
const auto& tile = group.tiles[item];
// "baseitem:<asset key>" resolves to the object's own thumbnail
// from the game data (the DelMarBaseItemAsset Thumbnail chain).
icons[index].push_back("baseitem:" + tile.items.front());
child.id.set(tile.id); child.title.set(tile.title); child.icon.set(icons[index].back());
child.hash = cosmetic_hash(tile.id);
child.priority = static_cast<std::uint32_t>(item);
}
const auto begin = reinterpret_cast<std::uintptr_t>(children.data());
const auto end = begin + children.size() * sizeof(ObjectServiceSubcategory);
parent.subcategories = {begin, end, end};
}
const auto begin = reinterpret_cast<std::uintptr_t>(categories.data());
const std::array<std::uintptr_t, 3> vector{begin, begin + categories.size() * sizeof(ObjectServiceCategory),
begin + categories.size() * sizeof(ObjectServiceCategory)};
std::array<std::uintptr_t, 2> shared{reinterpret_cast<std::uintptr_t>(&vector), 0};
const std::array<std::uintptr_t, 4> predicate{0, 0, 0, reinterpret_cast<std::uintptr_t>(&accept_object_category)};
// The native converter consumes this additional reference, including on an empty list.
std::uintptr_t consumed{};
game::native_data().values.null_reference(&consumed, result & ~std::uintptr_t{4});
object_runtime().functions.parse(shared.data(), &consumed, predicate.data(), predicate.data());
}
void* object_categories_hook(void* destination, const void* callback, const void* group) {
auto& s = local_runtime(); auto& o = object_runtime(); auto& f = game::native_data().values;
if (!s.active.load(std::memory_order_acquire)) return o.functions.subscribe(destination, callback, group);
{
PreserveError preserve;
std::lock_guard lock(s.native_mutex);
try {
std::string id;
if (identifier(group, id) && id == "qdbuildkit" && o.pending.available()) {
game::NativeDelegateGuard retained;
f.copy_delegate(&retained.value, callback);
if (retained.value) {
o.pending.push({retained.value}); retained.value = 0;
dingosdk::logging::event(dingosdk::logging::Channel::objects, "{\"event\":\"local_object_categories_queued\"}");
}
return f.null_reference(destination, 0);
}
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::objects, "{\"event\":\"local_object_categories_failed\",\"operation\":\"subscribe\"}"); }
}
return o.functions.subscribe(destination, callback, group);
}
void update_object_categories() {
auto& o = object_runtime();
if (o.pending.empty() || !cosmetic_runtime().published_inventory) return;
std::uintptr_t allocator{};
if (!read(local_runtime().base + addr::engine::ui_allocator, allocator) || !allocator) return;
o.pending.deliver([&](const ObjectRuntime::Pending&, std::uintptr_t callback) {
try {
auto* raw = o.functions.allocate(0x20, 8, allocator, 0, 1);
if (!raw) throw std::bad_alloc();
o.functions.construct(raw);
o.functions.initialize(raw, local_runtime().base + addr::local_object::category_result_type, 0, 0, 1);
ObjectReferenceGuard result;
game::native_data().values.null_reference(&result.value, reinterpret_cast<std::uintptr_t>(raw));
fill_object_categories(result.value, (*local_runtime().store->shared_snapshot()));
game::native_data().values.invoke(&callback, &result.value);
dingosdk::logging::event(dingosdk::logging::Channel::objects, "{\"event\":\"local_object_categories_published\"}");
} catch (...) { dingosdk::logging::event(dingosdk::logging::Channel::objects, "{\"event\":\"local_object_categories_failed\",\"operation\":\"callback\"}"); }
});
}
}