Files
Valiantsin DzerakhandClaude Opus 5.5 3676e299c5 Add a shake key that detaches a fused part held with Ultrahand
G (bind_SHAKE, keys only) swings the right stick left and right for 0.8 s, longer while held, so a part comes off
without shaking the arrow keys by hand. The pad keeps shaking its own stick. Remappable in the Controls tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:35:12 +03:00

250 lines
11 KiB
C++

// The remapping rows of the Controls tab (binds_page.h): edits a copy of the input tables (input_map.h), writes the TOTK_BIND_* lines that
// give the copy, and input_win.c applies them at its next poll; totk.ini follows a moment later (opt_save_soon). The
// gamepad cells show and change the table of the button layout in use (TOTK_PAD_LAYOUT).
#include <stdio.h>
#include <string.h>
#include "imgui.h"
#include "ui/binds_page.h"
#include "ui/option_rows.h"
extern "C" {
#include "cfg.h"
#include "input.h"
#include "input_map.h"
#include "platform/platform.h"
#include "ui_text.h"
// host.h pulls in cpu.h, which is C only: the one function needed from it, as declared there.
void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
}
static_assert(UI_BIND_SHAKE - UI_BIND_A == ACTION_COUNT - 1 && B_SHAKE == ACTION_COUNT - 1, "UI_BIND_* follow the actions");
static_assert(UI_BIND_DESC_MINUS - UI_BIND_DESC_A == B_MINUS, "UI_BIND_DESC_* follow the actions");
static_assert(UI_KEY_PAGEDOWN - UI_KEY_SPACE == KEY_PAGEDOWN - KEY_SPACE, "UI_KEY_* follow the key ids");
static_assert(UI_PAD_RPADDLE2 - UI_PAD_SOUTH == PAD_RPADDLE2 - PAD_SOUTH && PAD_RPADDLE2 == PAD_COUNT - 1,
"UI_PAD_* follow the pad ids");
enum { CAPTURE_SECONDS = 5 };
// The capture window stays this long after the press: the press and its click reach ImGui a frame or two later and must
// land on the window, not on a cell (a new capture), on Continue or as Esc closing the menu.
static const uint64_t kDoneNs = 350000000ull;
static const int kButtons[] = {B_A, B_B, B_X, B_Y, B_L, B_R, B_ZL, B_ZR, B_STICK_L, B_STICK_R, B_PLUS, B_MINUS,
B_UP, B_DOWN, B_LEFT, B_RIGHT};
static const int kMove[] = {B_SL_UP, B_SL_DOWN, B_SL_LEFT, B_SL_RIGHT};
static const int kCamera[] = {B_SR_UP, B_SR_DOWN, B_SR_LEFT, B_SR_RIGHT};
static struct {
int action = -1, cell; // the cell waiting for a press (0, 1 keys, 2 the controller); action -1 none
bool reset; // the next capture poll takes what is held as the start
uint64_t start, done; // plat_now_ns of the start and of the press, timeout or Esc (0 still waiting)
} g_cap;
static char g_note[160]; // what the last change took or swapped, after its row's description
static int g_note_action = -1;
static uint64_t g_note_until;
static const char *action_label(int a) { return menu_text((UiTextId)(UI_BIND_A + a)); }
static UiTextId action_desc(int a) {
if (a <= B_MINUS) return (UiTextId)(UI_BIND_DESC_A + a);
if (a <= B_DOWN) return UI_BIND_DESC_DPAD;
if (a == B_SHAKE) return UI_BIND_DESC_SHAKE;
return a <= B_SL_DOWN ? UI_BIND_DESC_MOVE : UI_BIND_DESC_CAMERA;
}
static void utf8(uint32_t c, char *out, size_t n) {
if (n < 5) { out[0] = 0; return; }
if (c < 0x80) { out[0] = (char)c; out[1] = 0; }
else if (c < 0x800) { out[0] = (char)(0xC0 | c >> 6); out[1] = (char)(0x80 | (c & 0x3F)); out[2] = 0; }
else {
out[0] = (char)(0xE0 | c >> 12); out[1] = (char)(0x80 | (c >> 6 & 0x3F)); out[2] = (char)(0x80 | (c & 0x3F));
out[3] = 0;
}
}
// A key as the player sees it: its catalog name, the numpad format, the character on a punctuation key in the keyboard
// layout in use, else its totk.ini name in capitals (letters, digits, F keys).
static const char *key_label(int k, char *buf, size_t n) {
if (k == KEY_NONE) return "";
if (k >= KEY_SPACE && k <= KEY_PAGEDOWN) return menu_text((UiTextId)(UI_KEY_SPACE + (k - KEY_SPACE)));
if (k >= KEY_NUM0 && k <= KEY_NUM_DECIMAL) {
static const char *const ops[] = {"+", "-", "*", "/", "."};
char d[4];
if (k <= KEY_NUM9) snprintf(d, sizeof d, "%d", k - KEY_NUM0);
else snprintf(d, sizeof d, "%s", ops[k - KEY_NUM_ADD]);
snprintf(buf, n, menu_text(UI_KEY_NUM_FMT), d);
return buf;
}
if (uint32_t c = input_key_char(k)) {
utf8(c, buf, n);
return buf;
}
snprintf(buf, n, "%s", input_key_name(k));
for (char *p = buf; *p; p++)
if (*p >= 'a' && *p <= 'z') *p = (char)(*p - 'a' + 'A');
return buf;
}
// A gamepad button: the label the open pad has on a face button, else its place on the pad.
static const char *pad_label(int p) {
if (p == PAD_NONE) return "";
switch (input_pad_label(p)) {
case PAD_LABEL_A: return "A";
case PAD_LABEL_B: return "B";
case PAD_LABEL_X: return "X";
case PAD_LABEL_Y: return "Y";
case PAD_LABEL_CROSS: return menu_text(UI_PAD_CROSS);
case PAD_LABEL_CIRCLE: return menu_text(UI_PAD_CIRCLE);
case PAD_LABEL_SQUARE: return menu_text(UI_PAD_SQUARE);
case PAD_LABEL_TRIANGLE: return menu_text(UI_PAD_TRIANGLE);
}
return menu_text((UiTextId)(UI_PAD_SOUTH + (p - PAD_SOUTH)));
}
// The layout the gamepad cells show: the one in effect, or without a gamepad the one the setting names (Xbox for auto).
static int shown_layout(void) {
int l = input_pad_layout();
return l >= 0 ? l : input_pad_layout_pick(cfg_value("TOTK_PAD_LAYOUT"), 0);
}
// Writes the bind_ line of every action whose line for m differs from the one set; a line at the defaults goes away.
static void write_lines(const InputMap *m) {
for (int a = 0; a < ACTION_COUNT; a++) {
char var[40], line[256];
snprintf(var, sizeof var, "TOTK_BIND_%s", input_action_name(a));
input_map_line(m, a, line, sizeof line);
const char *now = cfg_get(var);
if (!strcmp(line, now ? now : "")) continue;
if (line[0]) {
cfg_set(var, line);
} else {
const char *k = var + 5;
cfg_reset(&k, 1);
}
rt_log("menu: bind_%s = %s", input_action_name(a), line[0] ? line : "(default)");
}
opt_save_soon();
}
static void set_note(int action, UiTextId fmt, const char *a, const char *b) {
snprintf(g_note, sizeof g_note, menu_text(fmt), a, b);
g_note_action = action;
g_note_until = plat_now_ns() + 5000000000ull;
}
// cell 0, 1: a key (KEY_NONE clears it); 2: a gamepad button (PAD_NONE clears it).
static void change(int action, int cell, int id) {
InputMap m;
input_map_snapshot(&m);
char buf[48];
if (cell < 2) {
int lost = input_map_set_key(&m, action, cell, id);
if (lost >= 0) set_note(action, UI_BINDS_TAKEN, key_label(id, buf, sizeof buf), action_label(lost));
} else {
int other = input_map_set_pad(&m, action, shown_layout(), id);
if (other >= 0) set_note(action, UI_BINDS_SWAPPED, action_label(action), action_label(other));
}
write_lines(&m);
}
static void reset_binds(void) {
char names[ACTION_COUNT][32];
const char *keys[ACTION_COUNT];
for (int a = 0; a < ACTION_COUNT; a++) {
snprintf(names[a], sizeof names[a], "BIND_%s", input_action_name(a));
keys[a] = names[a];
}
int n = cfg_reset(keys, ACTION_COUNT);
opt_save_soon();
g_note_action = -1;
rt_log("menu: reset %d button bindings", n);
}
static void row_action(int a, const InputMap &m, int layout) {
uint8_t k[INPUT_BINDING_MAX], p[INPUT_PAD_BINDING_MAX];
int nk = input_map_keys_of(&m, a, k, INPUT_BINDING_MAX);
int np = a < B_SL_LEFT ? input_map_pads_of(&m, a, layout, p, INPUT_PAD_BINDING_MAX) : 0;
char b0[48], b1[48];
const char *cells[3] = {nk > 0 ? key_label(k[0], b0, sizeof b0) : "", nk > 1 ? key_label(k[1], b1, sizeof b1) : "",
a >= B_SL_LEFT ? menu_text(a <= B_SL_DOWN ? UI_PAD_LSTICK : UI_PAD_RSTICK)
: np ? pad_label(p[0])
: ""};
const bool enabled[3] = {true, true, a < B_SL_LEFT}; // the sticks stay sticks (and the shake is the right stick's)
char key[32];
snprintf(key, sizeof key, "BIND_%s", input_action_name(a));
const char *note = g_note_action == a && plat_now_ns() < g_note_until ? g_note : nullptr;
int r = opt_bind_row(key, action_label(a), action_desc(a), cells, enabled, note);
if (g_cap.action >= 0 || r < 0) return;
if (r < 3) {
g_cap.action = a;
g_cap.cell = r;
g_cap.reset = true;
g_cap.start = plat_now_ns();
g_cap.done = 0;
} else {
change(a, r - 10, 0); // KEY_NONE / PAD_NONE
}
}
// The window over the page while a cell waits: the press is polled from the devices (input_capture), so the menu's own
// keys (Enter, arrows, Q / E) bind like any other. Esc or the timeout cancels; a controller button cancels a key cell.
static void capture_window(void) {
if (g_cap.action < 0) return;
if (!ImGui::IsPopupOpen("##bind_capture")) ImGui::OpenPopup("##bind_capture");
uint64_t now = plat_now_ns();
bool pad_cell = g_cap.cell == 2;
if (!g_cap.done) {
int id = 0, got = input_capture(g_cap.reset, &id);
g_cap.reset = false;
if (got == INPUT_CAPTURE_KEY && !pad_cell) change(g_cap.action, g_cap.cell, id);
if (got == INPUT_CAPTURE_PAD && pad_cell) change(g_cap.action, g_cap.cell, id);
if (got != INPUT_CAPTURE_NONE && !(got == INPUT_CAPTURE_KEY && pad_cell)) g_cap.done = now;
if (now - g_cap.start > CAPTURE_SECONDS * 1000000000ull) g_cap.done = now;
}
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##bind_capture", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize)) {
char t[256];
snprintf(t, sizeof t, menu_text(pad_cell ? UI_BINDS_PRESS_PAD : UI_BINDS_PRESS_KEY), action_label(g_cap.action));
ImGui::TextUnformatted(t);
int left = CAPTURE_SECONDS - (int)((now - g_cap.start) / 1000000000ull);
snprintf(t, sizeof t, menu_text(UI_BINDS_CANCEL_HINT), left < 1 ? 1 : left);
ImGui::TextDisabled("%s", t);
if (g_cap.done && now - g_cap.done > kDoneNs) {
ImGui::CloseCurrentPopup();
char id[48];
snprintf(id, sizeof id, "BIND_%s#%d", input_action_name(g_cap.action), g_cap.cell);
opt_focus_key(id); // back on the cell it came from
g_cap.action = -1;
}
ImGui::EndPopup();
}
}
bool binds_modal(void) { return g_cap.action >= 0; }
static bool g_reset_asked; // the reset row was chosen this frame (acted on after the page)
void binds_rows(float scale) {
InputMap m;
input_map_snapshot(&m);
int layout = shown_layout();
opt_group(UI_BINDS_GROUP_BUTTONS);
opt_text(UI_BINDS_HELP);
opt_bind_header(UI_BINDS_COL_KEY1, UI_BINDS_COL_KEY2, UI_BINDS_COL_PAD);
for (int a : kButtons) row_action(a, m, layout);
opt_group(UI_BINDS_GROUP_MOVE);
for (int a : kMove) row_action(a, m, layout);
opt_group(UI_BINDS_GROUP_CAMERA);
for (int a : kCamera) row_action(a, m, layout);
row_action(B_SHAKE, m, layout); // keys only; its controller cell shows the right stick, which the pad shakes itself
ImGui::Dummy(ImVec2(0, 6 * scale));
g_reset_asked = opt_button("##binds_reset", UI_BINDS_RESET, UI_BINDS_RESET_DESC);
}
void binds_after_page(void) {
if (g_reset_asked && g_cap.action < 0) reset_binds();
g_reset_asked = false;
capture_window();
}