mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 11:57:01 +02:00
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.
This commit is contained in:
1 parent
8fc28a19da
commit
21d9b0d993
11 files changed
+142
-110
No files matched your search
@@ -161,10 +161,8 @@ CosmeticShared make_cosmetic_catalog(const profile::Snapshot& snapshot) {
|
||||
// EASTL's iteration scans past empty buckets until this non-null sentinel.
|
||||
owner->buckets.back() = &owner->sentinel;
|
||||
owner->map = {owner->buckets.data(), bucket_count, static_cast<std::uint32_t>(items.size())};
|
||||
const auto inventory = snapshot.customization.value("inventory", dingosdk::Json::object());
|
||||
const auto catalog = snapshot.customization.value("catalog", dingosdk::Json::object());
|
||||
const auto object_dropper = snapshot.extensions.value("object_dropper", dingosdk::Json::object());
|
||||
const auto object_inventory = object_dropper.value("inventory", dingosdk::Json::object());
|
||||
const auto object_catalog = object_dropper.value("catalog", dingosdk::Json::object());
|
||||
const auto groups = objects::object_groups(content_cache::catalogs());
|
||||
std::set<std::uint32_t> hashes;
|
||||
@@ -184,10 +182,8 @@ CosmeticShared make_cosmetic_catalog(const profile::Snapshot& snapshot) {
|
||||
}
|
||||
auto& node = owner->nodes[index];
|
||||
node.hash = info.hash;
|
||||
const auto& item_inventory = info.build_kit ? object_inventory : inventory;
|
||||
const auto& item_catalog = info.build_kit ? object_catalog : catalog;
|
||||
const auto saved = item_inventory.find(key);
|
||||
const bool owned = !cosmetic_runtime().ownership_unavailable && saved != item_inventory.end() && saved->get<bool>();
|
||||
const bool owned = installed_item_owned(snapshot, key, info.hash, info.build_kit);
|
||||
auto metadata = profile::item_display_metadata(key, item_catalog.value(key, dingosdk::Json::object()));
|
||||
if (info.build_kit) {
|
||||
// The same tile and category the browser layout gives this object.
|
||||
@@ -267,16 +263,8 @@ void publish_cosmetic_catalog() {
|
||||
|
||||
std::vector<std::uint32_t> cosmetic_owned_ids(const profile::Snapshot& snapshot) {
|
||||
std::vector<std::uint32_t> ids;
|
||||
if (cosmetic_runtime().ownership_unavailable) return ids;
|
||||
const auto inventory = snapshot.customization.value("inventory", dingosdk::Json::object());
|
||||
const auto object_dropper = snapshot.extensions.value("object_dropper", dingosdk::Json::object());
|
||||
const auto object_inventory = object_dropper.value("inventory", dingosdk::Json::object());
|
||||
for (const auto& [key, info] : cosmetic_runtime().items) {
|
||||
const auto& item_inventory = info.build_kit ? object_inventory : inventory;
|
||||
const auto saved = item_inventory.find(key);
|
||||
if (saved != item_inventory.end() && saved->is_boolean() && saved->get<bool>())
|
||||
ids.push_back(info.hash);
|
||||
}
|
||||
for (const auto& [key, info] : cosmetic_runtime().items)
|
||||
if (installed_item_owned(snapshot, key, info.hash, info.build_kit)) ids.push_back(info.hash);
|
||||
return ids;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,22 @@ bool reserved_cosmetic_hash(std::uint32_t hash) {
|
||||
std::lock_guard lock(reserved_hash_mutex);
|
||||
return hash && reserved_hashes.contains(hash);
|
||||
}
|
||||
bool installed_item_owned(const profile::Snapshot& snapshot, std::string_view key, std::uint32_t hash, bool build_kit) {
|
||||
if (cosmetic_runtime().ownership_unavailable || reserved_cosmetic_hash(hash)) return false;
|
||||
const auto option = snapshot.bool_options.find(build_kit ? profile::unlock_objects_option : profile::unlock_cosmetics_option);
|
||||
if (option != snapshot.bool_options.end() && option->second) return true;
|
||||
const dingosdk::Json* section = &snapshot.customization;
|
||||
if (build_kit) {
|
||||
const auto objects = snapshot.extensions.find("object_dropper");
|
||||
if (objects == snapshot.extensions.end() || !objects->is_object()) return false;
|
||||
section = &*objects;
|
||||
}
|
||||
const auto inventory = section->find("inventory");
|
||||
if (inventory == section->end() || !inventory->is_object()) return false;
|
||||
const auto saved = inventory->find(key);
|
||||
return saved != inventory->end() && saved->is_boolean() && saved->get<bool>();
|
||||
}
|
||||
|
||||
bool refresh_cosmetic_catalog() {
|
||||
auto& s = local_runtime(); auto& c = cosmetic_runtime();
|
||||
std::uintptr_t manager{}, vtable{}, buckets{};
|
||||
@@ -135,30 +151,24 @@ bool refresh_cosmetic_catalog() {
|
||||
!read(manager + 0x3c, current_count) || current_count != count) return false;
|
||||
const auto& catalogs = content_cache::catalogs();
|
||||
c.ownership_unavailable = !catalogs.available;
|
||||
std::vector<std::string> keys, objects, reserved_keys, reserved_objects;
|
||||
for (const auto& [key, info] : items) {
|
||||
std::string folded = key;
|
||||
for (auto& letter : folded) if (letter >= 'A' && letter <= 'Z') letter = static_cast<char>(letter + ('a' - 'A'));
|
||||
const bool held = catalogs.reserved(folded);
|
||||
(info.build_kit ? (held ? reserved_objects : objects) : (held ? reserved_keys : keys)).push_back(key);
|
||||
}
|
||||
if (!c.ownership_unavailable) {
|
||||
s.store->seed_cosmetic_inventory(keys);
|
||||
s.store->seed_object_inventory(objects);
|
||||
s.store->reconcile_inventory(reserved_keys, reserved_objects);
|
||||
}
|
||||
// What is owned is worked out from the items installed (installed_item_owned), not saved item
|
||||
// by item: the rows earlier versions saved go, once.
|
||||
s.store->forget_unlocked_inventory();
|
||||
std::size_t objects{};
|
||||
{
|
||||
std::set<std::uint32_t> hashes;
|
||||
for (const auto& [key, info] : items)
|
||||
for (const auto& [key, info] : items) {
|
||||
objects += info.build_kit;
|
||||
if (reserved_cosmetic(key)) hashes.insert(info.hash);
|
||||
}
|
||||
std::lock_guard lock(reserved_hash_mutex);
|
||||
reserved_hashes = std::move(hashes);
|
||||
}
|
||||
c.items = std::move(items); c.item_count = count; c.item_manager = manager;
|
||||
++c.items_generation;
|
||||
std::ostringstream event;
|
||||
event << "{\"event\":\"local_cosmetic_catalog\",\"installed\":" << keys.size()
|
||||
<< ",\"objects\":" << objects.size() << '}';
|
||||
event << "{\"event\":\"local_cosmetic_catalog\",\"installed\":" << items.size() - objects
|
||||
<< ",\"objects\":" << objects << '}';
|
||||
dingosdk::logging::event(dingosdk::logging::Channel::customization, event.str().c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -344,12 +354,6 @@ bool load_cosmetic_hook(const void* wrapper, void* destination) {
|
||||
std::vector<CosmeticBorrowedArray<CosmeticNativeItem>> item_arrays;
|
||||
words.reserve(16 * 257); item_arrays.reserve(16);
|
||||
std::vector<CosmeticRuntime::HeldSlot> held;
|
||||
// (Looked up once: Json::value returns a copy, and this was asked for every slot, a copy
|
||||
// of the whole inventory each time, in the game's own load call.)
|
||||
static const auto no_inventory = dingosdk::Json::object();
|
||||
const auto inventory_found = snapshot.customization.find("inventory");
|
||||
const auto& inventory = inventory_found != snapshot.customization.end() && inventory_found->is_object()
|
||||
? *inventory_found : no_inventory;
|
||||
for (std::size_t i = 0; i < saved->recipes.size(); ++i) {
|
||||
const auto& recipe = saved->recipes[i]; const auto& expected = defaults.recipes[i];
|
||||
if (recipe.template_key != expected.template_key || recipe.template_version != expected.template_version ||
|
||||
@@ -395,7 +399,7 @@ bool load_cosmetic_hook(const void* wrapper, void* destination) {
|
||||
held.push_back({i, *slot, expected.items[j].asset});
|
||||
slot = &expected.items[j];
|
||||
} else {
|
||||
if (inventory.contains(slot->asset) && !inventory.at(slot->asset).get<bool>())
|
||||
if (!installed_item_owned(snapshot, slot->asset, found->second.hash, found->second.build_kit))
|
||||
slot = &expected.items[j];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ bool cosmetic_words(std::uintptr_t p, std::size_t limit, std::vector<std::uint32
|
||||
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{};
|
||||
|
||||
@@ -84,9 +84,10 @@ bool read_player_card(std::uintptr_t manager, profile::CosmeticLoadout& result)
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool player_card_item_owned(const profile::CosmeticSlot& slot, const dingosdk::Json& inventory) {
|
||||
bool player_card_item_owned(const profile::CosmeticSlot& slot, const profile::Snapshot& snapshot) {
|
||||
const auto it = cosmetic_runtime().items.find(slot.asset);
|
||||
if (it == cosmetic_runtime().items.end() || !inventory.value(slot.asset, false)) return false;
|
||||
if (it == cosmetic_runtime().items.end() || !installed_item_owned(snapshot, slot.asset, it->second.hash, it->second.build_kit))
|
||||
return false;
|
||||
// Each card category is also its recipe slot hash in the installed template.
|
||||
return std::find(it->second.categories.begin(), it->second.categories.end(), slot.slot) !=
|
||||
it->second.categories.end();
|
||||
@@ -96,9 +97,8 @@ bool player_card_item_owned(const profile::CosmeticSlot& slot, const dingosdk::J
|
||||
bool player_card_owned(const profile::CosmeticLoadout& value) {
|
||||
const auto snapshot_shared = local_runtime().store->shared_snapshot();
|
||||
const auto& snapshot = *snapshot_shared;
|
||||
const auto& inventory = snapshot.customization.at("inventory");
|
||||
for (const auto& slot : value.recipes.front().items)
|
||||
if (!player_card_item_owned(slot, inventory)) return false;
|
||||
if (!player_card_item_owned(slot, snapshot)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -264,10 +264,9 @@ void update_player_card() {
|
||||
// slot. The saved card keeps it until the player changes the card.
|
||||
const auto snapshot_shared = s.store->shared_snapshot();
|
||||
const auto& snapshot = *snapshot_shared;
|
||||
const auto& inventory = snapshot.customization.at("inventory");
|
||||
const auto& defaults = current.recipes.front().items;
|
||||
for (auto& slot : restore->recipes.front().items) {
|
||||
if (player_card_item_owned(slot, inventory)) continue;
|
||||
if (player_card_item_owned(slot, snapshot)) continue;
|
||||
const auto fallback = std::find_if(defaults.begin(), defaults.end(),
|
||||
[&](const profile::CosmeticSlot& item) { return item.slot == slot.slot; });
|
||||
if (fallback == defaults.end()) { restore.reset(); break; }
|
||||
|
||||
@@ -219,6 +219,10 @@ CosmeticRuntime &cosmetic_runtime() {
|
||||
static CosmeticRuntime state;
|
||||
return state;
|
||||
}
|
||||
// (No profile here grants an object: nothing is owned, as with the empty inventory this stood in for.)
|
||||
bool installed_item_owned(const profile::Snapshot &, std::string_view, std::uint32_t, bool) {
|
||||
return false;
|
||||
}
|
||||
bool placement_session_ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -166,13 +166,12 @@ void refresh_catalog() {
|
||||
if (e.assets && e.catalog_revision == snapshot.revision && e.catalog_manager == c.item_manager)
|
||||
return;
|
||||
const auto config = snapshot.extensions.value("object_dropper", Json::object());
|
||||
const auto inventory = config.value("inventory", Json::object());
|
||||
const auto catalog = config.value("catalog", Json::object());
|
||||
const auto &cache = content_cache::catalogs();
|
||||
const auto groups = objects::object_groups(cache);
|
||||
auto assets = std::make_shared<std::vector<EditorAsset>>();
|
||||
for (const auto &[key, item] : c.items) {
|
||||
if (!item.build_kit || !inventory.value(key, false) || assets->size() >= 4096)
|
||||
if (!item.build_kit || !profile_runtime::installed_item_owned(snapshot, key, item.hash, true) || assets->size() >= 4096)
|
||||
continue;
|
||||
std::string title = key.starts_with("own_") ? key.substr(4) : key;
|
||||
std::replace(title.begin(), title.end(), '_', ' ');
|
||||
|
||||
@@ -59,11 +59,10 @@ std::string object_title(const std::string& key, const dingosdk::Json& overrides
|
||||
|
||||
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 inventory = section.value("inventory", 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 && inventory.value(key, false))
|
||||
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.
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
#include "Extension/Profile/local_profile.h"
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
std::size_t saved_items(const dingosdk::profile::Snapshot& snapshot, bool objects) {
|
||||
const auto& section = objects ? snapshot.extensions.value("object_dropper", dingosdk::Json::object()) : snapshot.customization;
|
||||
return section.value("inventory", dingosdk::Json::object()).size();
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
using namespace dingosdk;
|
||||
if (argc < 2) {
|
||||
@@ -12,15 +19,61 @@ int main(int argc, char** argv) {
|
||||
return 0;
|
||||
}
|
||||
const auto save = std::filesystem::absolute(argv[1]) / "reskate.sqlite3";
|
||||
profile::Store store(save);
|
||||
const auto snapshot = store.snapshot();
|
||||
const auto policy = profile::challenge_policy(snapshot);
|
||||
const auto& section = snapshot.extensions.at("challenges");
|
||||
std::size_t available{};
|
||||
for (const auto& [id, definition] : policy.catalog) available += definition.available;
|
||||
std::cout << "challenges: " << policy.catalog.size() << " in catalogue (" << available << " available), "
|
||||
<< section.at("catalog").size() << " stored changes, " << section.at("progress").size()
|
||||
<< " with progress\n"
|
||||
<< "entitlements: " << snapshot.entitlements.size() << "\n";
|
||||
return policy.catalog.empty() ? 1 : 0;
|
||||
int failures = 0;
|
||||
const auto check = [&](bool condition, const char* message) {
|
||||
if (!condition) { std::cerr << "FAIL: " << message << '\n'; ++failures; }
|
||||
};
|
||||
{
|
||||
profile::Store store(save);
|
||||
const auto snapshot = store.snapshot();
|
||||
const auto policy = profile::challenge_policy(snapshot);
|
||||
const auto& section = snapshot.extensions.at("challenges");
|
||||
std::size_t available{};
|
||||
for (const auto& [id, definition] : policy.catalog) available += definition.available;
|
||||
std::cout << "challenges: " << policy.catalog.size() << " in catalogue (" << available << " available), "
|
||||
<< section.at("catalog").size() << " stored changes, " << section.at("progress").size()
|
||||
<< " with progress\n"
|
||||
<< "entitlements: " << snapshot.entitlements.size() << "\n";
|
||||
check(!policy.catalog.empty(), "the challenge catalogue loads");
|
||||
|
||||
// Ownership is a rule while a kind's unlock option is on: the rows earlier versions saved
|
||||
// for every item they saw go, and nothing else in the profile changes. Off, they stay.
|
||||
const auto cosmetics = store.bool_option(profile::unlock_cosmetics_option);
|
||||
const auto objects = store.bool_option(profile::unlock_objects_option);
|
||||
std::cout << "saved ownership: " << saved_items(snapshot, false) << " cosmetic(s), " << saved_items(snapshot, true)
|
||||
<< " object(s)\n";
|
||||
store.set_bool_option(profile::unlock_cosmetics_option, false);
|
||||
store.set_bool_option(profile::unlock_objects_option, false);
|
||||
store.forget_unlocked_inventory();
|
||||
check(saved_items(store.snapshot(), false) == saved_items(snapshot, false) &&
|
||||
saved_items(store.snapshot(), true) == saved_items(snapshot, true),
|
||||
"with the unlock options off, saved ownership stays");
|
||||
store.set_bool_option(profile::unlock_cosmetics_option, true);
|
||||
store.set_bool_option(profile::unlock_objects_option, true);
|
||||
store.forget_unlocked_inventory();
|
||||
// (The options as they were, so only the inventory differs from the save that was copied.)
|
||||
if (cosmetics) store.set_bool_option(profile::unlock_cosmetics_option, *cosmetics);
|
||||
if (objects) store.set_bool_option(profile::unlock_objects_option, *objects);
|
||||
}
|
||||
{
|
||||
profile::Store reopened(save);
|
||||
const auto after = reopened.snapshot();
|
||||
check(saved_items(after, false) == 0 && saved_items(after, true) == 0, "with the unlock options on, saved ownership is gone after a restart");
|
||||
profile::Store original(std::filesystem::absolute(argv[1]) / "original" / "reskate.sqlite3");
|
||||
const auto before = original.snapshot();
|
||||
auto expected = before, actual = after;
|
||||
expected.customization.erase("inventory");
|
||||
actual.customization.erase("inventory");
|
||||
if (expected.extensions.contains("object_dropper")) expected.extensions.at("object_dropper").erase("inventory");
|
||||
if (actual.extensions.contains("object_dropper")) actual.extensions.at("object_dropper").erase("inventory");
|
||||
expected.revision = actual.revision = 0;
|
||||
check(expected.customization == actual.customization, "outfits, the player card and the rest of customization are unchanged");
|
||||
check(expected.extensions == actual.extensions, "every other saved section is unchanged");
|
||||
check(expected.bool_options == actual.bool_options && expected.entitlements == actual.entitlements &&
|
||||
expected.quests == actual.quests && expected.settings == actual.settings,
|
||||
"options, entitlements, quests and settings are unchanged");
|
||||
}
|
||||
if (failures) return 1;
|
||||
std::cout << "profile migration: ok\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -178,9 +178,11 @@ public:
|
||||
void save_cosmetic_loadout(std::string_view, const CosmeticLoadout&);
|
||||
std::optional<CosmeticLoadout> player_card() const;
|
||||
void save_player_card(const CosmeticLoadout&);
|
||||
void seed_cosmetic_inventory(const std::vector<std::string>&);
|
||||
void seed_object_inventory(const std::vector<std::string>&);
|
||||
void reconcile_inventory(const std::vector<std::string>& cosmetics, const std::vector<std::string>& objects);
|
||||
// With a kind's unlock option on, what the player owns is a rule, not a list
|
||||
// (profile_runtime::installed_item_owned): every installed item but a paid store copy. The
|
||||
// ownership rows earlier versions saved for every item they saw (every mod's, removed mods'
|
||||
// too) are dropped here, once, for each kind whose option is on.
|
||||
void forget_unlocked_inventory();
|
||||
std::uint32_t selected_cosmetic_preset() const;
|
||||
void set_selected_cosmetic_preset(std::uint32_t);
|
||||
void save_park_choice(unsigned lot, std::string_view id);
|
||||
|
||||
@@ -42,59 +42,27 @@ void Store::save_player_card(const CosmeticLoadout& value) {
|
||||
update.commit();
|
||||
|
||||
}
|
||||
void Store::seed_cosmetic_inventory(const std::vector<std::string>& keys) {
|
||||
void Store::forget_unlocked_inventory() {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto enabled = value_.bool_options.find(unlock_cosmetics_option);
|
||||
if (enabled == value_.bool_options.end() || !enabled->second) return;
|
||||
Update update(*this);
|
||||
update.json(value_.customization, {"catalog"}, true);
|
||||
const auto& inventory = update.json(value_.customization, {"inventory"}, true);
|
||||
for (const auto& key : keys) {
|
||||
require(valid_text(key), "Invalid installed cosmetic key");
|
||||
const auto found = inventory.find(key);
|
||||
if (found == inventory.end() || !found->is_boolean() || !found->get<bool>())
|
||||
update.json(value_.customization, {"inventory", key}) = true;
|
||||
}
|
||||
update.commit();
|
||||
|
||||
}
|
||||
void Store::seed_object_inventory(const std::vector<std::string>& keys) {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto enabled = value_.bool_options.find(unlock_objects_option);
|
||||
if (enabled == value_.bool_options.end() || !enabled->second) return;
|
||||
Update update(*this);
|
||||
update.json(value_.extensions, {"object_dropper", "catalog"}, true);
|
||||
const auto& inventory = update.json(value_.extensions, {"object_dropper", "inventory"}, true);
|
||||
for (const auto& key : keys) {
|
||||
require(valid_text(key), "Invalid installed object key");
|
||||
const auto found = inventory.find(key);
|
||||
if (found == inventory.end() || !found->is_boolean() || !found->get<bool>())
|
||||
update.json(value_.extensions, {"object_dropper", "inventory", key}) = true;
|
||||
}
|
||||
update.commit();
|
||||
|
||||
}
|
||||
void Store::reconcile_inventory(const std::vector<std::string>& cosmetics, const std::vector<std::string>& objects) {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto cosmetic_inventory = value_.customization.value("inventory", dingosdk::Json::object());
|
||||
const auto object_inventory =
|
||||
value_.extensions.value("object_dropper", dingosdk::Json::object()).value("inventory", dingosdk::Json::object());
|
||||
const auto owned = [](const dingosdk::Json& inventory, const std::string& key) {
|
||||
const auto found = inventory.find(key);
|
||||
return found != inventory.end() && found->is_boolean() && found->get<bool>();
|
||||
const auto unlocked = [&](std::string_view option) {
|
||||
const auto found = value_.bool_options.find(option);
|
||||
return found != value_.bool_options.end() && found->second;
|
||||
};
|
||||
const auto saved = [](const dingosdk::Json& section) {
|
||||
const auto found = section.find("inventory");
|
||||
return found != section.end() && found->is_object() && !found->empty();
|
||||
};
|
||||
Update update(*this);
|
||||
bool changed{};
|
||||
for (const auto& key : cosmetics)
|
||||
if (owned(cosmetic_inventory, key)) {
|
||||
update.json(value_.customization, {"inventory", key}) = false;
|
||||
changed = true;
|
||||
}
|
||||
for (const auto& key : objects)
|
||||
if (owned(object_inventory, key)) {
|
||||
update.json(value_.extensions, {"object_dropper", "inventory", key}) = false;
|
||||
changed = true;
|
||||
}
|
||||
if (unlocked(unlock_cosmetics_option) && saved(value_.customization)) {
|
||||
update.json(value_.customization, {"inventory"}) = dingosdk::Json::object();
|
||||
changed = true;
|
||||
}
|
||||
const auto objects = value_.extensions.find("object_dropper");
|
||||
if (unlocked(unlock_objects_option) && objects != value_.extensions.end() && objects->is_object() && saved(*objects)) {
|
||||
update.json(value_.extensions, {"object_dropper", "inventory"}) = dingosdk::Json::object();
|
||||
changed = true;
|
||||
}
|
||||
if (changed) update.commit();
|
||||
}
|
||||
std::uint32_t Store::selected_cosmetic_preset() const {
|
||||
|
||||
@@ -28,6 +28,7 @@ void Store::Update::encode(Json& patch) {
|
||||
}
|
||||
std::set<std::string> sections, loadouts, challenge_rows, challenge_catalog;
|
||||
std::map<std::string, std::set<std::string>> inventory;
|
||||
std::set<std::string> replaced_inventory; // a kind's whole inventory set at once
|
||||
bool settings{}, card{}, score{};
|
||||
for (const auto& change : json_edits_) {
|
||||
auto path = change.path;
|
||||
@@ -41,6 +42,8 @@ void Store::Update::encode(Json& patch) {
|
||||
if (section == "customization" && path[0] == "player_card") card = true;
|
||||
if ((section == "customization" || section == "object_dropper") && path[0] == "inventory" && path.size() > 1)
|
||||
inventory[section == "customization" ? "cosmetic" : "object"].insert(path[1]);
|
||||
if ((section == "customization" || section == "object_dropper") && path[0] == "inventory" && path.size() == 1)
|
||||
replaced_inventory.insert(section == "customization" ? "cosmetic" : "object");
|
||||
if (section == "progress" && path[0] == "rip_score") score = true;
|
||||
if (section == "challenges" && path.size() > 1) {
|
||||
if (path[0] == "progress") challenge_rows.insert(path[1]);
|
||||
@@ -88,6 +91,14 @@ void Store::Update::encode(Json& patch) {
|
||||
flatten(flatten, residual, "/settings");
|
||||
add("profile_extensions", Json::array({"settings", residual.dump()}));
|
||||
}
|
||||
for (const auto& kind : replaced_inventory) {
|
||||
// Every row of the kind goes, and the rows of what it holds now come back.
|
||||
remove("inventory", [&](const Json& row) { return row[0] == kind; });
|
||||
const auto& rows = kind == "cosmetic" ? value.customization.at("inventory") :
|
||||
value.extensions.at("object_dropper").at("inventory");
|
||||
detail::require(rows.is_object() && rows.size() <= saved_item_limit, "Inventory exceeds limit");
|
||||
for (const auto& [key, owned] : rows.items()) inventory[kind].insert(key);
|
||||
}
|
||||
for (const auto& [kind, keys] : inventory) {
|
||||
const auto& rows = kind == "cosmetic" ? value.customization.at("inventory") :
|
||||
value.extensions.at("object_dropper").at("inventory");
|
||||
|
||||
Reference in new issue
Block a user