Files
ReSkate/Extension/Profile/profile_update.cpp
T
AyeZeeBB 574860da72 Binds for first person, the HUD, voice chat, time of day, No Bail, challenges, nametags and dots
Binds for first person, the HUD, voice chat, time of day, No Bail, challenges, nametags and dots

Eight more actions can be put on a keyboard key, a chord or a controller
combo, in the binds list beside the others. None is bound until the player
binds it.

- First person, Hide HUD, Voice chat on / off, Next time of day, No Bail,
  Show / hide challenges, Player nametags, Player dots.
- Each bind runs the console switch for it, so it follows the same rules as
  typing it: No Bail still answers to a server that has it off, and the time
  of day goes to the server for an admin where the server sets it.
- They are one table (action_binds): a bind is its name in the profile, the
  name "bind <name>" takes, its label and its command.
- "bind firstperson|hidehud|voicechat|tod|nobail|challenges|nametags|
  nametagdots <n>" sets one from the console; 0 clears it.
- They are not read while the menu, the console or the chat box is open.

Console, which the binds use and anyone can type:
- Every on/off switch takes "toggle" ("nobail toggle", "hideui toggle").
- "firstperson 0|1", which was only in the menu.
- "tod next": the time of day after the current one.
- "mp voice-chat on|off|toggle": the player's own voice chat, leaving the
  rest of their voice settings; it says which way it went in their chat.
2026-10-08 22:39:05 -05:00

141 lines
8.4 KiB
C++

#include "profile_update.h"
#include <algorithm>
#include <cmath>
namespace dingosdk::profile {
void Store::Update::encode(Json& patch) {
const auto& value = store_.value_;
const auto& previous = store_.database_->document();
const auto add = [&](std::string_view table, Json row, unsigned keys = 1) {
const auto key = storage::row_key(row, keys);
patch[table][key] = std::move(row);
};
const auto remove = [&](std::string_view table, const auto& predicate) {
for (const auto& [key, row] : previous.at(table).items())
if (predicate(row)) patch[table][key] = nullptr;
};
for (const auto& [table, keys] : records_) for (const auto& key : keys) {
if (table == "player_settings") add(table, Json::array({"options", key, Json(value.bool_options.at(key)).dump()}), 2);
else if (table == "entitlements") add(table, Json::array({key, value.entitlements.at(key) ? 1 : 0}));
else if (table == "quests") add(table, Json::array({key, value.quests.at(key)}));
else if (table == "neighborhood_ranks") add(table, Json::array({key, value.neighborhood_ranks.at(key)}));
else if (table == "play_events") {
const auto& event = value.play_events.at(key);
detail::require(event.id == key && detail::valid_text(key) &&
detail::valid_text(event.context, true) && event.count >= 0, "Invalid play event");
add(table, Json::array({key, event.context, std::to_string(event.timestamp), event.count}));
} else throw std::runtime_error("Unknown profile record update");
}
std::set<std::string> sections, loadouts, challenge_rows, challenge_catalog;
std::map<std::string, std::set<std::string>> inventory;
bool settings{}, card{}, score{};
for (const auto& change : json_edits_) {
auto path = change.path;
if (change.root == &value.settings) { settings = true; continue; }
std::string section;
if (change.root == &value.customization) section = "customization";
else { section = path.front(); path.erase(path.begin()); }
sections.insert(section);
if (path.empty()) continue;
if (section == "customization" && path[0] == "loadouts" && path.size() > 1) loadouts.insert(path[1]);
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 == "progress" && path[0] == "rip_score") score = true;
if (section == "challenges" && path.size() > 1) {
if (path[0] == "progress") challenge_rows.insert(path[1]);
if (path[0] == "catalog") challenge_catalog.insert(path[1]);
}
}
// Preserve the structural/unknown fields without copying any item catalog,
// inventory, loadout collection, or unrelated progression records.
for (const auto& section : sections) {
const auto& source = section == "customization" ? value.customization : value.extensions.at(section);
Json residual = Json::object();
for (const auto& [key, field] : source.items()) {
const bool empty = ((section == "customization" || section == "object_dropper") &&
(key == "inventory" || key == "catalog")) ||
(section == "customization" && key == "loadouts") ||
(section == "challenges" && (key == "catalog" || key == "progress"));
if (empty) residual[key] = Json::object();
else if (!((section == "customization" && key == "player_card") ||
(section == "progress" && key == "rip_score"))) residual[key] = field;
}
if (section == "progress") for (auto key : {"quests", "play_events", "neighborhood_ranks"}) residual[key] = Json::object();
add("profile_extensions", Json::array({section, residual.dump()}));
}
if (settings) {
detail::validate_settings(value);
(void)park_choices(value); (void)world_layer_choices(value);
(void)world_controls(value); (void)graphics_controls(value); (void)profile::freecam_controller(value); (void)profile::freecam_controller_binding(value); (void)profile::freecam_binding(value); (void)profile::tp_to_freecam_binding(value); (void)profile::noclip_binding(value);
(void)profile::forward_velocity_binding(value);
(void)profile::vote_yes_binding(value); (void)profile::vote_no_binding(value);
for (const auto& slot : action_binds) (void)profile::action_binding(value, slot.key);
(void)profile::up_velocity_binding(value);
(void)profile::offboard_up_velocity_binding(value);
remove("player_settings", [](const Json& row) { return row[0] != "options"; });
auto residual = value.settings;
const auto flatten = [&](auto&& self, Json& node, const std::string& scope) -> void {
for (auto it = node.items().begin(); it != node.items().end();) {
if (it->second.is_object()) {
self(self, it->second, scope + "/" + database::escape(it->first)); ++it;
} else {
add("player_settings", Json::array({scope, it->first, it->second.dump()}), 2);
it = node.items().erase(it);
}
}
};
flatten(flatten, residual, "/settings");
add("profile_extensions", Json::array({"settings", residual.dump()}));
}
for (const auto& [kind, keys] : inventory) {
const auto& rows = kind == "cosmetic" ? value.customization.at("inventory") :
value.extensions.at("object_dropper").at("inventory");
detail::require(rows.is_object() && rows.size() <= 8192, "Inventory exceeds limit");
for (const auto& key : keys) {
detail::require(detail::valid_text(key) && rows.at(key).is_boolean(), "Invalid inventory item");
add("inventory", Json::array({kind, key, rows.at(key).get<bool>() ? 1 : 0}), 2);
}
}
const auto outfit = [&](std::string_view table, const Json& key, Json row) {
(void)decode_loadout(row);
const auto format = row.at("recipe_format"), recipes = row.at("recipes");
row.erase("recipe_format"); row.erase("recipes");
add(table, Json::array({key, format, recipes.dump(), row.dump()}));
};
if (!loadouts.empty()) {
const auto& rows = value.customization.at("loadouts");
detail::require(rows.size() <= 10, "Invalid local preset collection");
for (const auto& id : loadouts) outfit("cosmetic_loadouts", id, rows.at(id));
}
if (card) {
validate_player_card(decode_loadout(value.customization.at("player_card")));
outfit("player_card", 1, value.customization.at("player_card"));
}
if (score) {
(void)rip_score(value);
auto row = value.extensions.at("progress").at("rip_score");
const auto total = row.at("value"), cap = row.at("cap"), level = row.at("level");
row.erase("value"); row.erase("cap"); row.erase("level");
add("rip_score", Json::array({1, total, cap, level, row.dump()}));
}
if (!challenge_rows.empty() || !challenge_catalog.empty()) detail::validate_challenges(value);
for (const auto& id : challenge_catalog)
add("catalog_entries", Json::array({"challenge", id, value.extensions.at("challenges").at("catalog").at(id).dump()}), 2);
for (const auto& id : challenge_rows) {
const auto& row = value.extensions.at("challenges").at("progress").at(id);
auto extra = row; for (auto key : {"attempt", "completed_criteria", "receipt"}) extra.erase(key);
add("challenge_progress", Json::array({id, row.at("attempt"), extra.dump()}));
remove("challenge_goals", [&](const Json& old) { return old[0] == id; });
const auto& goals = row.at("completed_criteria");
for (std::size_t i = 0; i < goals.size(); ++i) add("challenge_goals", Json::array({id, goals[i], i}), 2);
if (row.contains("receipt")) {
const auto& receipt = row.at("receipt"); auto extras = receipt;
for (auto key : {"attempt", "status", "completed_criteria", "grants"}) extras.erase(key);
add("challenge_receipts", Json::array({id, receipt.at("attempt"), receipt.at("status"),
receipt.at("completed_criteria").dump(), receipt.at("grants").dump(), extras.dump()}));
}
}
}
}