mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 23:17:06 +02:00
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.
141 lines
8.4 KiB
C++
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()}));
|
|
}
|
|
}
|
|
}
|
|
}
|