diff --git a/Engine/Core/Console/command_registry.h b/Engine/Core/Console/command_registry.h index 33be1c1..841aa0b 100644 --- a/Engine/Core/Console/command_registry.h +++ b/Engine/Core/Console/command_registry.h @@ -215,6 +215,8 @@ template class Registry { token += ' '; token += args[index++]; } + // "toggle" on an on/off switch: the other of what it is now. + if (arg.type == Type::boolean && equal(token, "toggle")) token = current.value && *current.value == "1" ? "0" : "1"; auto value = parse_value(arg.type, token); if (!value) { output("error: Invalid " + arg.name + ": '" + token + "'. Usage: " + usage(*entry)); diff --git a/Engine/Game/Input/controller_bindings.h b/Engine/Game/Input/controller_bindings.h index 6b5ca87..639ed1e 100644 --- a/Engine/Game/Input/controller_bindings.h +++ b/Engine/Game/Input/controller_bindings.h @@ -84,6 +84,21 @@ struct ControllerInput { ControllerStyle style{}; std::array keys{}; }; +// Switches a player can put on a button or key: each runs a console command when pressed. +// `key` is its name in the profile, `name` what "bind " takes. +struct ActionBind { + std::string_view key, name, label, command; +}; +inline constexpr std::array action_binds{{ + {"first_person", "firstperson", "First person", "firstperson toggle"}, + {"hide_hud", "hidehud", "Hide HUD", "hideui toggle"}, + {"voice_chat", "voicechat", "Voice chat on / off", "mp voice-chat toggle"}, + {"time_of_day", "tod", "Next time of day", "tod next"}, + {"no_bail", "nobail", "No Bail", "nobail toggle"}, + {"challenges", "challenges", "Show / hide challenges", "challenges toggle"}, + {"nametags", "nametags", "Player nametags", "mp nametags toggle"}, + {"nametag_dots", "nametagdots", "Player dots", "mp nametag-dots toggle"}, +}}; struct ControllerBindingsModel { bool available{}; bool freecam_controller{}; @@ -95,6 +110,7 @@ struct ControllerBindingsModel { std::uint32_t up_velocity_combo{}; std::uint32_t offboard_up_velocity_combo{}; std::uint32_t vote_yes_combo{}, vote_no_combo{}; // answering a dedicated server's vote + std::array action_combos{}; // by action_binds std::string status; }; inline std::string controller_combo_label(std::uint32_t value, ControllerStyle style = ControllerStyle::xbox) { diff --git a/Extension/Multiplayer/Session/session_commands.cpp b/Extension/Multiplayer/Session/session_commands.cpp index 2336a69..e497a3e 100644 --- a/Extension/Multiplayer/Session/session_commands.cpp +++ b/Extension/Multiplayer/Session/session_commands.cpp @@ -616,7 +616,7 @@ bool queue_command(std::string_view action, std::string_view argument, std::stri if ((action != "host" && action != "host-config" && action != "join" && action != "join-lobby" && action != "join-friend-lobby" && action != "stop" && action != "distances" && action != "object-placement" && action != "object-limit" && action != "kick" && action != "clear-objects" && action != "nametags" && action != "chat-visible" && action != "chat-filter" && - action != "nametag-distance" && action != "nametag-dots" && action != "nametags-friends" && action != "player-distance" && action != "direct-connections" && action != "pose-dump" && action != "vote" && + action != "nametag-distance" && action != "nametag-dots" && action != "nametags-friends" && action != "player-distance" && action != "direct-connections" && action != "pose-dump" && action != "vote" && action != "voice-chat" && action != "chat-bubbles" && action != "chat-bubbles-own" && action != "chat-bubbles-distance" && action != "chat-bubbles-duration" && action != "chat-bubbles-history" && !own_mark_command(action) && @@ -741,6 +741,18 @@ std::string command(std::string_view action, std::string_view argument, std::str publish(s); return value.enabled ? (value.open_mic ? "Open microphone enabled." : "Push-to-talk voice enabled.") : "Voice chat disabled."; } + if (action == "voice-chat") { + // The player's own voice chat, on or off; the rest of their voice settings stay. + if (argument != "on" && argument != "off" && argument != "toggle") return "Choose on, off or toggle."; + auto value = s.voice_settings; + value.enabled = argument == "toggle" ? !value.enabled : argument == "on"; + s.voice_settings = value; + s.voice.configure(value); + publish(s); + const std::string result = value.enabled ? "Voice chat on." : "Voice chat off."; + add_chat(s, 0, "ReSkate", result); + return result; + } if (action == "tp") return teleport_self(s, argument); if (action == "tpall" || action == "tphere") return teleport_players(s, action, argument); if (action == "voice-allow") { diff --git a/Extension/Multiplayer/console_commands.cpp b/Extension/Multiplayer/console_commands.cpp index 9a34114..2962ea0 100644 --- a/Extension/Multiplayer/console_commands.cpp +++ b/Extension/Multiplayer/console_commands.cpp @@ -33,6 +33,7 @@ void register_multiplayer_commands(Commands ®istry) { Command{"nametags", "Show or hide player nametags (on, off, toggle)"}, Command{"pose-dump", "Research: record your own poses for a number of seconds (1-600) to logs/poses-*.bin"}, Command{"vote", "Answer the vote a dedicated server is running (yes, no)"}, + Command{"voice-chat", "Your own voice chat (on, off, toggle)"}, Command{"direct-connections", "Connect straight to servers that offer it, not through Steam's relays (on, off, toggle)"}, Command{"player-distance", "How far away another player still gets a skater, in metres (50-1000; 1000: every player). Past it: a nametag or dot"}, Command{"nametag-distance", "How far away a player's name still shows, in metres (10-500); past it they are a dot"}, @@ -61,6 +62,7 @@ void register_multiplayer_commands(Commands ®istry) { std::string_view(c.name) == "chat-bubbles" || std::string_view(c.name) == "chat-bubbles-own" || std::string_view(c.name) == "nametag-dots" || std::string_view(c.name) == "nametags-friends" || std::string_view(c.name) == "direct-connections" || std::string_view(c.name) == "vote" || + std::string_view(c.name) == "voice-chat" || std::string_view(c.name) == "score-check") args.push_back(argument("choice")); if (std::string_view(c.name) == "chat-bubbles-distance" || std::string_view(c.name) == "nametag-distance") diff --git a/Extension/Profile/local_profile.h b/Extension/Profile/local_profile.h index a1752d2..2380032 100644 --- a/Extension/Profile/local_profile.h +++ b/Extension/Profile/local_profile.h @@ -117,6 +117,8 @@ bool freecam_controller(const Snapshot&); std::uint32_t freecam_binding(const Snapshot&); std::uint32_t tp_to_freecam_binding(const Snapshot&); std::uint32_t vote_yes_binding(const Snapshot&); +// One of action_binds, by its key; 0: not bound. +std::uint32_t action_binding(const Snapshot&, std::string_view key); std::uint32_t vote_no_binding(const Snapshot&); std::uint32_t noclip_binding(const Snapshot&); std::uint32_t forward_velocity_binding(const Snapshot&); @@ -195,6 +197,8 @@ public: void save_tp_to_freecam_binding(std::uint32_t); std::uint32_t vote_binding(bool yes) const; void save_vote_binding(bool yes, std::uint32_t); + std::uint32_t action_binding(std::string_view key) const; + void save_action_binding(std::string_view key, std::uint32_t); std::uint32_t noclip_binding() const; void save_noclip_binding(std::uint32_t); std::uint32_t forward_velocity_binding() const; diff --git a/Extension/Profile/local_profile_preferences.cpp b/Extension/Profile/local_profile_preferences.cpp index 6ec8d98..352ea2c 100644 --- a/Extension/Profile/local_profile_preferences.cpp +++ b/Extension/Profile/local_profile_preferences.cpp @@ -19,6 +19,7 @@ ControllerBindingsModel local_profile_controller_bindings() { result.offboard_up_velocity_combo = s.store->offboard_up_velocity_binding(); result.vote_yes_combo = s.store->vote_binding(true); result.vote_no_combo = s.store->vote_binding(false); + for (std::size_t i = 0; i < action_binds.size(); ++i) result.action_combos[i] = s.store->action_binding(action_binds[i].key); result.available = true; return result; } @@ -68,6 +69,18 @@ bool set_local_freecam_binding(std::uint32_t combo) { } catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; } } +bool set_local_action_binding(std::size_t index, std::uint32_t combo) { + auto& s = local_runtime(); std::lock_guard lock(s.native_mutex); + if (!s.active || !s.store || index >= action_binds.size() || !valid_action_binding(combo)) return false; + try { + const auto& slot = action_binds[index]; + s.store->save_action_binding(slot.key, combo); + binding_feedback = std::string(slot.label) + (combo ? " binding saved." : " binding cleared."); + dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action",std::string(slot.key)},{"combo",combo}}.dump().c_str()); + return true; + } catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; } +} + bool set_local_vote_binding(bool yes, std::uint32_t combo) { auto& s = local_runtime(); std::lock_guard lock(s.native_mutex); if (!s.active || !s.store || !valid_action_binding(combo)) return false; diff --git a/Extension/Profile/local_profile_runtime.h b/Extension/Profile/local_profile_runtime.h index 07d0b70..6f72898 100644 --- a/Extension/Profile/local_profile_runtime.h +++ b/Extension/Profile/local_profile_runtime.h @@ -67,6 +67,8 @@ bool set_local_freecam_binding(std::uint32_t); bool set_local_tp_to_freecam_binding(std::uint32_t); // The player's binding for Yes (or No) in a dedicated server's vote; 0 clears it. bool set_local_vote_binding(bool yes, std::uint32_t); +// One of action_binds (controller_bindings.h), by its place there; 0 clears it. +bool set_local_action_binding(std::size_t index, std::uint32_t); bool set_local_noclip_binding(std::uint32_t); bool set_local_forward_velocity_binding(std::uint32_t); bool set_local_up_velocity_binding(std::uint32_t); diff --git a/Extension/Profile/local_profile_store.cpp b/Extension/Profile/local_profile_store.cpp index c9d3b17..4df82de 100644 --- a/Extension/Profile/local_profile_store.cpp +++ b/Extension/Profile/local_profile_store.cpp @@ -217,6 +217,18 @@ void Store::save_forward_velocity_binding(std::uint32_t combo) { update.commit(); } +std::uint32_t Store::action_binding(std::string_view key) const { + std::lock_guard lock(mutex_); + return profile::action_binding(value_, key); +} +void Store::save_action_binding(std::string_view key, std::uint32_t combo) { + require(valid_action_binding(combo), "Unsupported controller combo or keyboard key"); + const std::string name(key); + std::lock_guard lock(mutex_); + Update update(*this); + update.json(value_.settings, {"bindings", name.c_str()}) = combo; + update.commit(); +} std::uint32_t Store::vote_binding(bool yes) const { std::lock_guard lock(mutex_); return yes ? profile::vote_yes_binding(value_) : profile::vote_no_binding(value_); diff --git a/Extension/Profile/profile_update.cpp b/Extension/Profile/profile_update.cpp index 12231dc..eed85b6 100644 --- a/Extension/Profile/profile_update.cpp +++ b/Extension/Profile/profile_update.cpp @@ -70,6 +70,7 @@ void Store::Update::encode(Json& patch) { (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"; }); diff --git a/Extension/Settings/input_bindings.cpp b/Extension/Settings/input_bindings.cpp index a8e9d6c..bab6567 100644 --- a/Extension/Settings/input_bindings.cpp +++ b/Extension/Settings/input_bindings.cpp @@ -95,6 +95,7 @@ static std::uint32_t vote_binding(const Snapshot& s, const char* key, std::uint3 "Unsupported controller or keyboard key in a vote binding"); return static_cast(mask); } +std::uint32_t action_binding(const Snapshot& s, std::string_view key) { return vote_binding(s, std::string(key).c_str(), 0); } std::uint32_t vote_yes_binding(const Snapshot& s) { return vote_binding(s, "vote_yes", default_vote_yes_binding); } std::uint32_t vote_no_binding(const Snapshot& s) { return vote_binding(s, "vote_no", default_vote_no_binding); } std::uint32_t offboard_up_velocity_binding(const Snapshot& s) { diff --git a/Extension/Skater/console_commands.cpp b/Extension/Skater/console_commands.cpp index b4c55fb..6e5de87 100644 --- a/Extension/Skater/console_commands.cpp +++ b/Extension/Skater/console_commands.cpp @@ -20,6 +20,8 @@ void register_movement_commands(Commands ®istry) { &overlay::DebugModel::no_bail, &overlay::DebugModel::no_bail_available}, {"freecam", "debug.freecam", "Move the camera independently of the skater", Debug::set_free_camera, &overlay::DebugModel::free_camera, &overlay::DebugModel::camera_available}, + {"firstperson", "debug.first_person", "See through the skater's eyes", Debug::set_first_person, + &overlay::DebugModel::first_person, &overlay::DebugModel::camera_available}, {"hideui", "debug.game_ui_hidden", "Hide the game's interface", Debug::set_game_ui_hidden, &overlay::DebugModel::game_ui_hidden, &overlay::DebugModel::ui_available}}; for (const auto &setting : settings) { @@ -247,6 +249,21 @@ void register_movement_commands(Commands ®istry) { }; registry.add(std::move(vote_bind)); } + // The switches a player can put on a button (action_binds): each bind runs its command. + for (std::size_t i = 0; i < action_binds.size(); ++i) { + const auto &slot = action_binds[i]; + auto entry = action("bind " + std::string(slot.name), + "Bind a controller combo or keyboard key to: " + std::string(slot.label) + " (" + std::string(slot.command) + "); 0 clears the binding", + Group::movement, {mask}); + entry.inspect = [](const Model &m) { + return State{m.bindings.available, {}, "Controller bindings are unavailable.", {}, false}; + }; + entry.run = [i](const Model &, const Values &args, const Output &out) { + const bool saved = set_local_action_binding(i, static_cast(std::get(args[0]))); + out(saved ? local_profile_controller_bindings().status : "error: Invalid binding or save failed."); + }; + registry.add(std::move(entry)); + } } void register_ai_commands(Commands ®istry) { const auto ready = [](const Model &m) { diff --git a/Extension/UI/Overlay/skate_menu_settings.cpp b/Extension/UI/Overlay/skate_menu_settings.cpp index ee55ddf..69ea8dd 100644 --- a/Extension/UI/Overlay/skate_menu_settings.cpp +++ b/Extension/UI/Overlay/skate_menu_settings.cpp @@ -72,7 +72,7 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback const auto save = [&](int action, std::uint32_t combo) { std::array result{}; const auto command = std::string("bind ") + - (action == 1 ? "freecamcontroller " : action == 2 ? "freecam " : action == 3 ? "noclip " : action == 4 ? "forwardvelocity " : action == 5 ? "upvelocity " : action == 6 ? "tptofreecam " : action == 8 ? "voteyes " : action == 9 ? "voteno " : "offboardupvelocity ") + std::to_string(combo); + (action >= 10 ? std::string(action_binds[static_cast(action - 10)].name) + " " : action == 1 ? "freecamcontroller " : action == 2 ? "freecam " : action == 3 ? "noclip " : action == 4 ? "forwardvelocity " : action == 5 ? "upvelocity " : action == 6 ? "tptofreecam " : action == 8 ? "voteyes " : action == 9 ? "voteno " : "offboardupvelocity ") + std::to_string(combo); const bool queued = callbacks.queue_console_command(callbacks.user, command.c_str(), result.data(), result.size()); result.back() = '\0'; feedback(menu, result[0] ? result.data() : queued ? "Saving binding..." : "Could not queue binding."); @@ -129,6 +129,8 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback row(7, "Off-board Up Boost", model.bindings.offboard_up_velocity_combo); row(8, "Vote yes", model.bindings.vote_yes_combo); row(9, "Vote no", model.bindings.vote_no_combo); + for (std::size_t i = 0; i < action_binds.size(); ++i) + row(10 + static_cast(i), action_binds[i].label.data(), model.bindings.action_combos[i]); ImGui::EndTable(); } ImGui::EndDisabled(); diff --git a/Extension/World/console_commands.cpp b/Extension/World/console_commands.cpp index 6831e49..2063679 100644 --- a/Extension/World/console_commands.cpp +++ b/Extension/World/console_commands.cpp @@ -190,8 +190,9 @@ void register_world_commands(Commands ®istry) { // and the rest off, or all seven go back to the level's own choice. static constexpr std::array times{"default", "morning", "noon", "afternoon", "evening", "night", "weatherday", "weathernight"}; - auto time = argument("default|morning|noon|afternoon|evening|night|weatherday|weathernight"); + auto time = argument("default|morning|noon|afternoon|evening|night|weatherday|weathernight|next"); time.choices.assign(times.begin(), times.end()); + time.choices.emplace_back("next"); // the one after the current: for a button that steps through them auto tod = variable("tod", "Time of day on the current map", Group::world, time); // The current map's time layers in slot order, or empty when it has none. const auto time_layers = [](const Model &m) { @@ -219,7 +220,18 @@ void register_world_commands(Commands ®istry) { "Saved for this map", value != "default"}; }; tod.run = [time_layers](const Model &m, const Values &args, const Output &out) { - const auto wanted = lower(std::get(args[0])); + auto wanted = lower(std::get(args[0])); + if (wanted == "next") { + // As inspect reads it: the one layer forced on, or the level's own. + const auto now_layers = time_layers(m); + std::size_t forced{}; + for (std::size_t i = 0; i < now_layers.size(); ++i) { + const auto &choice = m.world.choices[now_layers[i]]; + if (choice == "on") forced = forced ? times.size() : i + 1; + else if (choice != "off") forced = times.size(); + } + wanted = std::string(times[forced >= times.size() ? 1 : (forced + 1) % times.size()]); + } const auto found = std::find(times.begin(), times.end(), wanted); if (found == times.end()) { out("error: Choose default, morning, noon, afternoon, evening, night, weatherday or weathernight."); return; } const auto slot = static_cast(found - times.begin()); diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index 1de17b9..faa5fa2 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -385,6 +385,8 @@ void update_model(std::uintptr_t client, TickState& frame) { std::uint32_t freecam_controller_combo{}, freecam_combo{}, tp_to_freecam_combo{}, noclip_combo{}, forward_velocity_combo{}, up_velocity_combo{}, offboard_up_velocity_combo{}; // Yes and No in a dedicated server's vote: read only while one is running. std::uint32_t vote_yes_combo{}, vote_no_combo{}; + // The switches on buttons (action_binds): each runs its console command. + std::array action_combos{}; const bool vote_open = dingosdk::multiplayer::server_vote_open(); bool freecam_controller = dingosdk::local_freecam_controller(); { @@ -398,6 +400,7 @@ void update_model(std::uintptr_t client, TickState& frame) { forward_velocity_combo = r.model.bindings.available ? r.model.bindings.forward_velocity_combo : 0; up_velocity_combo = r.model.bindings.available ? r.model.bindings.up_velocity_combo : 0; offboard_up_velocity_combo = r.model.bindings.available ? r.model.bindings.offboard_up_velocity_combo : 0; + if (r.model.bindings.available) action_combos = r.model.bindings.action_combos; if (vote_open && r.model.bindings.available) { vote_yes_combo = r.model.bindings.vote_yes_combo; vote_no_combo = r.model.bindings.vote_no_combo; @@ -409,7 +412,15 @@ void update_model(std::uintptr_t client, TickState& frame) { DingoSDKOverlaySetFreecamInputCapture(dingosdk::client_free_camera_active() && freecam_controller); dingosdk::ControllerInput controller; if (freecam_controller_combo || freecam_combo || tp_to_freecam_combo || noclip_combo || forward_velocity_combo || up_velocity_combo || offboard_up_velocity_combo || - vote_yes_combo || vote_no_combo) DingoSDKOverlayReadControllerInput(&controller); + vote_yes_combo || vote_no_combo || std::ranges::any_of(action_combos, [](auto combo) { return combo != 0; })) + DingoSDKOverlayReadControllerInput(&controller); + for (std::size_t i = 0; i < action_combos.size(); ++i) + if (r.action_bind_latches[i].update(action_combos[i], controller, r.observer_failed)) { + std::array result{}; + const std::string command(dingosdk::action_binds[i].command); + if (queue_console_command(nullptr, command.c_str(), result.data(), result.size())) + record(("{\"event\":\"controller_binding_triggered\",\"action\":\"" + std::string(dingosdk::action_binds[i].key) + "\"}").c_str()); + } // (The input reads as nothing while the menu, the console or the chat box is open, so typing // a bound key answers no vote.) if (r.vote_yes_bind_latch.update(vote_yes_combo, controller, !vote_open) && dingosdk::multiplayer::queue_command("vote", "yes", "")) diff --git a/Runtime/runtime_internal.h b/Runtime/runtime_internal.h index 6afd809..61ce5ec 100644 --- a/Runtime/runtime_internal.h +++ b/Runtime/runtime_internal.h @@ -77,6 +77,7 @@ struct Runtime { dingosdk::ControllerComboLatch up_velocity_bind_latch; dingosdk::ControllerComboLatch offboard_up_velocity_bind_latch; dingosdk::ControllerComboLatch vote_yes_bind_latch, vote_no_bind_latch; + std::array action_bind_latches; ULONGLONG next_offline{}; bool transition_seen{}, initialized{}, catalog_logged{}, native_tick_ready{}, native_loading_logging{}, fixed_stop_entitlement_route_ready{}, menu_load_queued{}, startup_load_queued{};