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.
233 lines
11 KiB
C++
233 lines
11 KiB
C++
#pragma once
|
|
#include <algorithm>
|
|
#include <bit>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
namespace dingosdk {
|
|
// XInput button bits, plus two synthetic trigger bits (above the dead zone).
|
|
inline constexpr std::uint32_t controller_button_mask = 0x3f3ff;
|
|
// Single keys retain their saved format. Chords pack up to five sorted key IDs
|
|
// into the remaining 30 bits of a distinct tag in the same profile field.
|
|
inline constexpr std::uint32_t keyboard_binding_tag = 0x80000000u;
|
|
inline constexpr std::uint32_t keyboard_chord_tag = 0x40000000u;
|
|
// Yes and No in a server's vote until the player binds them otherwise: F1 and F2.
|
|
inline constexpr std::uint32_t default_vote_yes_binding = keyboard_binding_tag | 0x70u, default_vote_no_binding = keyboard_binding_tag | 0x71u;
|
|
inline constexpr std::uint8_t keyboard_key_id(std::uint32_t key) noexcept {
|
|
if (key >= '0' && key <= '9') return static_cast<std::uint8_t>(key - '0' + 1);
|
|
if (key >= 'A' && key <= 'Z') return static_cast<std::uint8_t>(key - 'A' + 11);
|
|
if (key == 0x20) return 37; // Space
|
|
if (key >= 0x70 && key <= 0x7b) return static_cast<std::uint8_t>(key - 0x70 + 38); // F1-F12
|
|
if (key == 0x11) return 50; // Ctrl
|
|
if (key == 0x10) return 51; // Shift
|
|
if (key == 0x12) return 52; // Alt
|
|
return 0;
|
|
}
|
|
inline constexpr std::uint8_t keyboard_key_from_id(std::uint32_t id) noexcept {
|
|
if (id >= 1 && id <= 10) return static_cast<std::uint8_t>('0' + id - 1);
|
|
if (id >= 11 && id <= 36) return static_cast<std::uint8_t>('A' + id - 11);
|
|
if (id == 37) return 0x20;
|
|
if (id >= 38 && id <= 49) return static_cast<std::uint8_t>(0x70 + id - 38);
|
|
if (id == 50) return 0x11;
|
|
if (id == 51) return 0x10;
|
|
if (id == 52) return 0x12;
|
|
return 0;
|
|
}
|
|
inline constexpr bool bindable_keyboard_key(std::uint32_t key) noexcept {
|
|
return keyboard_key_id(key) != 0;
|
|
}
|
|
inline constexpr bool keyboard_binding(std::uint32_t value) noexcept {
|
|
if ((value & ~0xffu) == keyboard_binding_tag) return bindable_keyboard_key(value & 0xffu);
|
|
if ((value & 0xc0000000u) != keyboard_chord_tag) return false;
|
|
unsigned count = 0, previous = 0;
|
|
bool ended = false;
|
|
for (unsigned slot = 0; slot < 5; ++slot) {
|
|
const unsigned id = (value >> (slot * 6)) & 0x3fu;
|
|
if (!id) { ended = true; continue; }
|
|
if (ended || !keyboard_key_from_id(id) || id <= previous) return false;
|
|
previous = id;
|
|
++count;
|
|
}
|
|
return count >= 2;
|
|
}
|
|
inline std::uint32_t keyboard_binding_from_keys(const std::array<std::uint64_t, 4>& keys) noexcept {
|
|
std::uint32_t value = keyboard_chord_tag;
|
|
unsigned count = 0;
|
|
for (unsigned id = 1; id <= 52; ++id) {
|
|
const auto key = keyboard_key_from_id(id);
|
|
if (!(keys[key / 64] & (std::uint64_t{1} << (key % 64)))) continue;
|
|
if (count == 5) return 0;
|
|
value |= id << (count++ * 6);
|
|
}
|
|
if (count == 1) {
|
|
const auto id = value & 0x3fu;
|
|
return keyboard_binding_tag | keyboard_key_from_id(id);
|
|
}
|
|
return count >= 2 ? value : 0;
|
|
}
|
|
inline constexpr bool valid_controller_combo(std::uint32_t value) noexcept {
|
|
return (value & ~controller_button_mask) == 0;
|
|
}
|
|
inline constexpr bool valid_action_binding(std::uint32_t value) noexcept {
|
|
return valid_controller_combo(value) || keyboard_binding(value);
|
|
}
|
|
// Button names shown to the player. Controller chords keep their XInput bits.
|
|
enum class ControllerStyle : std::uint8_t { xbox, dualshock4, dualsense };
|
|
struct ControllerInput {
|
|
bool available{};
|
|
std::uint32_t buttons{};
|
|
// Identifies the connected source set; zero means unavailable. Any change
|
|
// (a pad connecting, disconnecting or switching source) resets latches.
|
|
unsigned device{};
|
|
ControllerStyle style{};
|
|
std::array<std::uint64_t, 4> 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 <name>" takes.
|
|
struct ActionBind {
|
|
std::string_view key, name, label, command;
|
|
};
|
|
inline constexpr std::array<ActionBind, 8> 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{};
|
|
std::uint32_t freecam_controller_combo{};
|
|
std::uint32_t freecam_combo{};
|
|
std::uint32_t tp_to_freecam_combo{};
|
|
std::uint32_t noclip_combo{};
|
|
std::uint32_t forward_velocity_combo{};
|
|
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<std::uint32_t, action_binds.size()> action_combos{}; // by action_binds
|
|
std::string status;
|
|
};
|
|
inline std::string controller_combo_label(std::uint32_t value, ControllerStyle style = ControllerStyle::xbox) {
|
|
if (!value) return "Not bound";
|
|
if (keyboard_binding(value)) {
|
|
const auto key_name = [](std::uint32_t key) {
|
|
if (key == 0x20) return std::string("Space");
|
|
if (key == 0x11) return std::string("Ctrl");
|
|
if (key == 0x10) return std::string("Shift");
|
|
if (key == 0x12) return std::string("Alt");
|
|
if (key >= 0x70) return "F" + std::to_string(key - 0x6fu);
|
|
return std::string(1, static_cast<char>(key));
|
|
};
|
|
if ((value & 0xc0000000u) == keyboard_binding_tag) return key_name(value & 0xffu);
|
|
std::string label;
|
|
const auto append = [&](unsigned wanted) {
|
|
for (unsigned slot = 0; slot < 5; ++slot) {
|
|
const auto id = (value >> (slot * 6)) & 0x3fu;
|
|
if (!id) break;
|
|
if (id != wanted) continue;
|
|
if (!label.empty()) label += " + ";
|
|
label += key_name(keyboard_key_from_id(id));
|
|
}
|
|
};
|
|
for (unsigned id = 50; id <= 52; ++id) append(id);
|
|
for (unsigned id = 1; id < 50; ++id) append(id);
|
|
return label;
|
|
}
|
|
const bool playstation = style != ControllerStyle::xbox;
|
|
struct Button { std::uint32_t bit; const char* xbox; const char* playstation; };
|
|
constexpr Button buttons[]{
|
|
{0x100,"LB","L1"}, {0x200,"RB","R1"}, {0x10000,"LT","L2"}, {0x20000,"RT","R2"},
|
|
{0x40,"L3","L3"}, {0x80,"R3","R3"},
|
|
{0x1000,"A","Cross"}, {0x2000,"B","Circle"}, {0x4000,"X","Square"}, {0x8000,"Y","Triangle"},
|
|
{1,"D-pad Up","D-pad Up"}, {2,"D-pad Down","D-pad Down"}, {4,"D-pad Left","D-pad Left"}, {8,"D-pad Right","D-pad Right"},
|
|
{0x10,"Start","Options"}, {0x20,"Back",nullptr}};
|
|
std::string label;
|
|
for (const auto& button : buttons) if (value & button.bit) {
|
|
if (!label.empty()) label += " + ";
|
|
label += !playstation ? button.xbox : button.playstation ? button.playstation :
|
|
style == ControllerStyle::dualsense ? "Create" : "Share";
|
|
}
|
|
return label;
|
|
}
|
|
// A held combo never repeats. Returning from menus, reconnecting, changing a
|
|
// binding, and changing level all require releasing the bound inputs first.
|
|
struct ControllerComboLatch {
|
|
std::uint32_t combo{};
|
|
unsigned device{};
|
|
bool armed{};
|
|
bool update(std::uint32_t binding, ControllerInput input, bool inhibited) noexcept {
|
|
const bool keyboard = keyboard_binding(binding);
|
|
const unsigned source = keyboard ? 0 : input.device;
|
|
if (binding != combo || source != device) {
|
|
combo = binding; device = source; armed = false;
|
|
}
|
|
if (inhibited || (!keyboard && !input.available) || !combo || !valid_action_binding(combo)) {
|
|
armed = false; return false;
|
|
}
|
|
if (keyboard) {
|
|
bool any = false, all = true;
|
|
for (unsigned slot = 0; slot < 5; ++slot) {
|
|
const auto key = (combo & 0xc0000000u) == keyboard_binding_tag ?
|
|
(slot ? 0u : combo & 0xffu) : keyboard_key_from_id((combo >> (slot * 6)) & 0x3fu);
|
|
if (!key) break;
|
|
const bool held = (input.keys[key / 64] & (std::uint64_t{1} << (key % 64))) != 0;
|
|
any |= held; all &= held;
|
|
}
|
|
if (!any) armed = true;
|
|
if (!armed || !all) return false;
|
|
armed = false;
|
|
return true;
|
|
}
|
|
const auto held = input.buttons & combo;
|
|
if (!held) armed = true;
|
|
if (!armed || held != combo) return false;
|
|
armed = false;
|
|
return true;
|
|
}
|
|
};
|
|
// Record the largest simultaneous chord, not a union of sequential presses.
|
|
// Begin neutral so the input that opened Record cannot become the binding.
|
|
struct ControllerComboCapture {
|
|
std::uint32_t chord{};
|
|
unsigned device{};
|
|
bool ready{};
|
|
std::array<std::uint64_t, 4> keyboard_chord{};
|
|
std::optional<std::uint32_t> update(ControllerInput input, bool allow_keyboard = false) noexcept {
|
|
if (input.device != device || (!input.available && !allow_keyboard)) {
|
|
chord = 0; ready = false; keyboard_chord = {}; device = input.device;
|
|
}
|
|
if (!input.available && !allow_keyboard) return {};
|
|
const auto buttons = input.buttons & controller_button_mask;
|
|
const bool keys_held = std::any_of(input.keys.begin(), input.keys.end(), [](auto word) { return word != 0; });
|
|
if (!ready) { ready = buttons == 0 && (!allow_keyboard || !keys_held); return {}; }
|
|
if (allow_keyboard && keys_held && !chord) {
|
|
if (std::popcount(input.keys[0]) + std::popcount(input.keys[1]) +
|
|
std::popcount(input.keys[2]) + std::popcount(input.keys[3]) >
|
|
std::popcount(keyboard_chord[0]) + std::popcount(keyboard_chord[1]) +
|
|
std::popcount(keyboard_chord[2]) + std::popcount(keyboard_chord[3])) keyboard_chord = input.keys;
|
|
}
|
|
if (std::any_of(keyboard_chord.begin(), keyboard_chord.end(), [](auto word) { return word != 0; })) {
|
|
if (!keys_held) {
|
|
const auto result = keyboard_binding_from_keys(keyboard_chord);
|
|
keyboard_chord = {}; ready = false;
|
|
if (result) return result;
|
|
}
|
|
return {};
|
|
}
|
|
if (!input.available) return {};
|
|
if (std::popcount(buttons) > std::popcount(chord)) chord = buttons;
|
|
if (!buttons && chord) {
|
|
const auto result = chord; chord = 0; ready = false;
|
|
return result;
|
|
}
|
|
return {};
|
|
}
|
|
};
|
|
}
|