mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-12 18:08:45 -10:00
480 lines
19 KiB
C++
480 lines
19 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include "menu_pointer.h"
|
|
#include "input.h"
|
|
#include "pad.h"
|
|
#include "../bbport_settings.h"
|
|
#include "../engine/engine_hooks.h"
|
|
#include "../engine/engine_state.h"
|
|
#include "../engine/option_menu.h"
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
|
|
namespace MenuPointer {
|
|
namespace {
|
|
using BbEngine::Call;
|
|
|
|
constexpr std::size_t kListUpdate = 0x1acff30;
|
|
|
|
constexpr std::uint8_t kListUpdateEntry[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55,
|
|
0x41, 0x54, 0x53, 0x48, 0x81, 0xec, 0x98, 0x00, 0x00, 0x00};
|
|
constexpr std::size_t kSetCursor = 0x1acf6b0, kScrollBy = 0x1ad0dd0, kHideCursors = 0x1acf0e0;
|
|
constexpr std::size_t kChildClip = 0x1adb220, kCursorClipName = 0x4934044;
|
|
constexpr std::size_t kStageMatrix = 0x2040;
|
|
constexpr std::size_t kCodeEnd = 0x50d96dc;
|
|
|
|
constexpr std::size_t kListener = 0x30, kItems = 0x70, kCursor = 0x74, kCursorOverride = 0x78;
|
|
constexpr std::size_t kCells = 0x7c, kLines = 0x80, kAlong = 0x84, kScrollLines = 0x24c;
|
|
constexpr std::size_t kRootType = 0xc8, kRootData = 0xd0, kProvider = 0x258, kWraps = 0x2b0;
|
|
|
|
constexpr std::size_t kValue = 0x30, kValueInterface = 0x40, kValueType = 0x48, kValueData = 0x50;
|
|
constexpr std::size_t kDisplayObject = 0x88, kParent = 0x38, kFlagsHigh = 0x6b, kBoundsSlot = 0x1f0;
|
|
constexpr std::uint8_t kVisible = 0x40;
|
|
|
|
constexpr std::size_t kMenuFlow = 0x5580e38, kFlowBlocked = 0xa78, kFlowMenu = 0xa88, kFlowStarted = 0xb11;
|
|
constexpr std::size_t kInputBlocker = 0x5562878, kBlockerBusy = 0x2e4;
|
|
|
|
constexpr std::size_t kRegion = 0x5128948;
|
|
constexpr int kRegionCircleDecides = 5;
|
|
|
|
constexpr std::size_t kMenuSound = 0x557fc60, kMenuSoundLabel = 0x557fc68, kCursorMoveLabel = 0x49344e4;
|
|
|
|
std::uint8_t* g_image = nullptr;
|
|
bool g_log = false;
|
|
|
|
std::atomic<std::int64_t> g_focus_ms{-100000};
|
|
std::atomic<std::int64_t> g_choice_ms{-100000};
|
|
std::atomic<bool> g_holding_row{false};
|
|
std::atomic<bool> g_click{false};
|
|
std::atomic<std::int64_t> g_click_ms{0};
|
|
std::atomic<std::uint32_t> g_press{0};
|
|
constexpr std::int64_t kClickMs = 250, kInMenuMs = 400, kNavigatingMs = 70;
|
|
|
|
std::int64_t NowMs() {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
|
|
.count();
|
|
}
|
|
|
|
std::uint64_t Image(std::size_t offset) { return reinterpret_cast<std::uint64_t>(g_image) + offset; }
|
|
|
|
template <typename T> bool Read(std::uint64_t at, T& out) {
|
|
return at >= 0x10000 && BbEngine::host_read_safe(reinterpret_cast<const void*>(at), &out, sizeof out);
|
|
}
|
|
template <typename T> T Field(const std::uint8_t* object, std::size_t offset) {
|
|
T v;
|
|
std::memcpy(&v, object + offset, sizeof v);
|
|
return v;
|
|
}
|
|
|
|
bool GameFunction(std::uint64_t at) { return g_image && at >= Image(0) && at < Image(kCodeEnd); }
|
|
std::uint64_t VirtualFunction(std::uint64_t object, std::size_t slot) {
|
|
std::uint64_t vtable = 0, fn = 0;
|
|
return object && Read(object, vtable) && Read(vtable + slot, fn) && GameFunction(fn) ? fn : 0;
|
|
}
|
|
|
|
class Wrapped {
|
|
public:
|
|
Wrapped() = default;
|
|
Wrapped(const Wrapped&) = delete;
|
|
Wrapped& operator=(const Wrapped&) = delete;
|
|
~Wrapped() {
|
|
if (!(Field<std::uint32_t>(bytes_, kValueType) & 0x40)) return;
|
|
const auto interface = Field<std::uint64_t>(bytes_, kValueInterface);
|
|
if (const std::uint64_t release = VirtualFunction(interface, 0x18))
|
|
Call(release, interface, bytes_ + kValue, Field<std::uint64_t>(bytes_, kValueData));
|
|
}
|
|
std::uint8_t* Bytes() { return bytes_; }
|
|
|
|
std::uint64_t Object() const {
|
|
std::uint64_t object = 0;
|
|
const auto data = Field<std::uint64_t>(bytes_, kValueData);
|
|
return (Field<std::uint32_t>(bytes_, kValueType) & 0x8f) == 10 && Read(data + kDisplayObject, object) ? object : 0;
|
|
}
|
|
|
|
private:
|
|
alignas(16) std::uint8_t bytes_[0x80]{};
|
|
};
|
|
|
|
bool ItemAt(std::uint8_t* list, std::uint64_t cell, Wrapped& out) {
|
|
const auto provider = Field<std::uint64_t>(list, kProvider);
|
|
const std::uint64_t make = VirtualFunction(provider, 0x18);
|
|
if (!make) return false;
|
|
Call(make, out.Bytes(), provider, &cell);
|
|
return out.Object() != 0;
|
|
}
|
|
void Child(Wrapped& parent, const char* path, Wrapped& out) { Call(Image(kChildClip), out.Bytes(), parent.Bytes(), path); }
|
|
void CursorClip(Wrapped& item, Wrapped& out) {
|
|
Call(Image(kChildClip), out.Bytes(), item.Bytes(), Image(kCursorClipName));
|
|
}
|
|
|
|
bool Shown(std::uint64_t object) {
|
|
for (int depth = 0; object && depth < 32; ++depth) {
|
|
std::uint8_t flags = 0;
|
|
if (!Read(object + kFlagsHigh, flags) || !(flags & kVisible) || !Read(object + kParent, object)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct Rect {
|
|
float left = 0, top = 0, right = 0, bottom = 0;
|
|
float CentreX() const { return (left + right) / 2; }
|
|
float CentreY() const { return (top + bottom) / 2; }
|
|
};
|
|
|
|
bool Contains(std::uint64_t object, float x, float y, float grow, Rect* on_stage) {
|
|
const std::uint64_t bounds_fn = VirtualFunction(object, kBoundsSlot);
|
|
if (!bounds_fn) return false;
|
|
alignas(16) float identity[8] = {1, 0, 0, 0, 0, 1, 0, 0};
|
|
alignas(16) float b[4] = {};
|
|
Call(bounds_fn, b, object, identity);
|
|
if (!(b[2] > b[0] && b[3] > b[1])) return false;
|
|
alignas(16) float m[8] = {1, 0, 0, 0, 0, 1, 0, 0};
|
|
Call(Image(kStageMatrix), object, m);
|
|
if (on_stage) {
|
|
|
|
Rect r{1e9f, 1e9f, -1e9f, -1e9f};
|
|
for (const float bx : {b[0], b[2]}) {
|
|
for (const float by : {b[1], b[3]}) {
|
|
const float sx = (m[0] * bx + m[1] * by + m[3]) / 20, sy = (m[4] * bx + m[5] * by + m[7]) / 20;
|
|
r = {std::min(r.left, sx), std::min(r.top, sy), std::max(r.right, sx), std::max(r.bottom, sy)};
|
|
}
|
|
}
|
|
*on_stage = r;
|
|
}
|
|
const float det = m[0] * m[5] - m[1] * m[4];
|
|
if (std::fabs(det) < 1e-12f) return false;
|
|
|
|
const float px = x * 20 - m[3], py = y * 20 - m[7];
|
|
const float lx = (m[5] * px - m[1] * py) / det, ly = (m[0] * py - m[4] * px) / det;
|
|
const float g = grow * 20 / std::sqrt(std::fabs(det));
|
|
return lx >= b[0] - g && lx <= b[2] + g && ly >= b[1] - g && ly <= b[3] + g;
|
|
}
|
|
|
|
int CursorOf(const std::uint8_t* list) {
|
|
const auto over = Field<std::int32_t>(list, kCursorOverride);
|
|
return over >= 0 ? over : Field<std::int32_t>(list, kCursor);
|
|
}
|
|
|
|
struct Pointed {
|
|
int index = -1;
|
|
std::uint64_t cell = 0;
|
|
};
|
|
|
|
Pointed ItemUnder(std::uint8_t* list, float x, float y, bool log) {
|
|
const int items = Field<std::int32_t>(list, kItems), cells = Field<std::int32_t>(list, kCells);
|
|
const int lines = Field<std::int32_t>(list, kLines);
|
|
if (items <= 0 || cells <= 0 || lines <= 0 || cells > 64 || lines > 64) return {};
|
|
const int along = Field<std::int32_t>(list, kAlong), scrolled = Field<std::int32_t>(list, kScrollLines);
|
|
const bool wraps = Field<std::uint8_t>(list, kWraps) != 0;
|
|
|
|
const int line_length = lines == 1 ? std::max({cells, items, 1}) : cells;
|
|
Pointed best;
|
|
float best_distance = 0;
|
|
for (int line = 0; line < lines; ++line) {
|
|
for (int cell = 0; cell < cells; ++cell) {
|
|
int index = (scrolled + line) * line_length + along + cell;
|
|
if (wraps) index %= items;
|
|
if (index < 0 || index >= items) continue;
|
|
const std::uint64_t packed = std::uint32_t(cell) | std::uint64_t(std::uint32_t(line)) << 32;
|
|
Wrapped item;
|
|
if (!ItemAt(list, packed, item) || !Shown(item.Object())) continue;
|
|
Wrapped highlight;
|
|
CursorClip(item, highlight);
|
|
const std::uint64_t target = highlight.Object() ? highlight.Object() : item.Object();
|
|
Rect where;
|
|
bool on = Contains(target, x, y, 0, &where);
|
|
for (const char* widget : {"Widgets/ComboBox", "Widgets/Slider", "Slider"}) {
|
|
if (on) break;
|
|
Wrapped part;
|
|
Child(item, widget, part);
|
|
Rect widget_where;
|
|
if (part.Object() && Shown(part.Object()) && Contains(part.Object(), x, y, 0, &widget_where)) {
|
|
on = true;
|
|
where = widget_where;
|
|
}
|
|
}
|
|
if (log)
|
|
host_log("pointer: item %d (cell %d, line %d) %s at %.0f,%.0f-%.0f,%.0f%s", index, cell, line,
|
|
target == item.Object() ? "itself" : "its Cursor", where.left, where.top, where.right,
|
|
where.bottom, on ? " <- pointer" : "");
|
|
if (!on) continue;
|
|
const float dx = where.CentreX() - x, dy = where.CentreY() - y, distance = dx * dx + dy * dy;
|
|
if (best.index < 0 || distance < best_distance) {
|
|
best = {index, packed};
|
|
best_distance = distance;
|
|
}
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
std::uint32_t ClickMeaning(std::uint8_t* list, const Pointed& p, float x, float y) {
|
|
const std::uint32_t decide = menu_confirm_button();
|
|
if (BbEngine::Options::ClickDecides(list, p.index)) return decide;
|
|
Wrapped item;
|
|
if (!ItemAt(list, p.cell, item)) return decide;
|
|
const auto on = [&](const char* path, float grow, Rect* where) {
|
|
Wrapped part;
|
|
Child(item, path, part);
|
|
const bool shown = part.Object() && Shown(part.Object());
|
|
const bool hit = shown && Contains(part.Object(), x, y, grow, where);
|
|
if (g_log && part.Object())
|
|
host_log("pointer: %s %s at %.0f,%.0f-%.0f,%.0f%s", path, shown ? "shown" : "hidden", where->left,
|
|
where->top, where->right, where->bottom, hit ? " <- click" : "");
|
|
return hit;
|
|
};
|
|
Rect where;
|
|
if (on("Widgets/Slider/LeftArrow", 14, &where)) return PadBits::kLeft;
|
|
if (on("Widgets/Slider/RightArrow", 14, &where)) return PadBits::kRight;
|
|
if (on("Widgets/ComboBox", 0, &where)) return x < where.CentreX() ? PadBits::kLeft : PadBits::kRight;
|
|
if (on("Slider", 0, &where)) return x < where.CentreX() ? PadBits::kLeft : PadBits::kRight;
|
|
return decide;
|
|
}
|
|
|
|
void ShowCursorOf(std::uint8_t* list, std::uint64_t cell) {
|
|
Wrapped item;
|
|
if (!ItemAt(list, cell, item)) return;
|
|
Wrapped highlight;
|
|
CursorClip(item, highlight);
|
|
if (!highlight.Object()) return;
|
|
if (const std::uint64_t set_visible = VirtualFunction(reinterpret_cast<std::uint64_t>(highlight.Bytes()), 8))
|
|
Call(set_visible, highlight.Bytes(), 1);
|
|
}
|
|
|
|
void CursorMoved(std::uint8_t* list, int index) {
|
|
const auto listener = Field<std::uint64_t>(list, kListener);
|
|
if (const std::uint64_t told = VirtualFunction(listener, 0x10)) {
|
|
std::int32_t at = index;
|
|
Call(told, listener, &at);
|
|
}
|
|
auto* sound = reinterpret_cast<std::int32_t*>(Image(kMenuSound));
|
|
if (sound[1] <= 0) {
|
|
sound[0] = 0;
|
|
sound[1] = 0;
|
|
const std::uint64_t label = Image(kCursorMoveLabel);
|
|
std::memcpy(reinterpret_cast<void*>(Image(kMenuSoundLabel)), &label, sizeof label);
|
|
}
|
|
}
|
|
|
|
std::uint64_t Flow() {
|
|
std::uint64_t flow = 0;
|
|
return g_image && Read(Image(kMenuFlow), flow) ? flow : 0;
|
|
}
|
|
|
|
bool MenuOpen() {
|
|
std::uint64_t menu = 0;
|
|
const std::uint64_t flow = Flow();
|
|
return flow && Read(flow + kFlowMenu, menu) && menu;
|
|
}
|
|
|
|
bool InputBlocked() {
|
|
std::uint64_t blocked = 0, blocker = 0;
|
|
std::uint32_t busy = 0;
|
|
const std::uint64_t flow = Flow();
|
|
if (flow && Read(flow + kFlowBlocked, blocked) && blocked) return true;
|
|
return g_image && Read(Image(kInputBlocker), blocker) && blocker && Read(blocker + kBlockerBusy, busy) && busy;
|
|
}
|
|
|
|
bool RegionDecidesWithCircle() {
|
|
std::int32_t region = -1;
|
|
return g_image && Read(Image(kRegion), region) && region == kRegionCircleDecides;
|
|
}
|
|
|
|
bool InMenu() {
|
|
return FrontEnd() || MenuOpen() || NowMs() - g_focus_ms.load(std::memory_order_relaxed) < kInMenuMs;
|
|
}
|
|
|
|
bool DirectionHeld() {
|
|
const PadState pad = BbInput::LastPad();
|
|
if (pad.buttons & PadBits::kDpad) return true;
|
|
if (BbSettings::Get().menu_move && !FrontEnd()) return false;
|
|
const auto pushed = [](std::uint8_t v) { return v < 64 || v > 192; };
|
|
return pushed(pad.lx) || pushed(pad.ly);
|
|
}
|
|
|
|
struct Seen {
|
|
const std::uint8_t* list = nullptr;
|
|
float x = 0, y = 0;
|
|
std::int64_t ms = 0;
|
|
bool described = false;
|
|
};
|
|
Seen g_seen[32];
|
|
unsigned g_seen_next = 0;
|
|
constexpr std::int64_t kSeenMs = 250;
|
|
|
|
Seen& SeenBy(const std::uint8_t* list, bool& fresh) {
|
|
const std::int64_t now = NowMs();
|
|
for (Seen& s : g_seen) {
|
|
if (s.list != list) continue;
|
|
fresh = now - s.ms > kSeenMs;
|
|
if (fresh) s.described = false;
|
|
s.ms = now;
|
|
return s;
|
|
}
|
|
Seen& s = g_seen[g_seen_next++ % std::size(g_seen)];
|
|
s = {list, 0, 0, now, false};
|
|
fresh = true;
|
|
return s;
|
|
}
|
|
|
|
bool ClickWaiting() {
|
|
if (!g_click.load(std::memory_order_relaxed)) return false;
|
|
if (NowMs() - g_click_ms.load(std::memory_order_relaxed) <= kClickMs) return true;
|
|
g_click.store(false, std::memory_order_relaxed);
|
|
return false;
|
|
}
|
|
|
|
void SpinnerClick(std::uint8_t* list, float x, float y) {
|
|
std::uint64_t root = 0;
|
|
const auto data = Field<std::uint64_t>(list, kRootData);
|
|
if ((Field<std::uint32_t>(list, kRootType) & 0x8f) != 10 || !Read(data + kDisplayObject, root) || !root) return;
|
|
Rect where;
|
|
const bool on = Shown(root) && Contains(root, x, y, 0, &where);
|
|
if (g_log)
|
|
host_log("pointer: spinner %p at %.0f,%.0f-%.0f,%.0f%s", static_cast<void*>(list), where.left, where.top,
|
|
where.right, where.bottom, on ? " <- click" : "");
|
|
if (on && g_click.exchange(false))
|
|
g_press.store(x < where.CentreX() ? PadBits::kLeft : PadBits::kRight, std::memory_order_relaxed);
|
|
}
|
|
|
|
bool OnScreen(const std::uint8_t* list) {
|
|
std::uint64_t root = 0;
|
|
const auto data = Field<std::uint64_t>(list, kRootData);
|
|
if ((Field<std::uint32_t>(list, kRootType) & 0x8f) != 10 || !Read(data + kDisplayObject, root) || !root) return true;
|
|
return Shown(root);
|
|
}
|
|
|
|
std::atomic<const std::uint8_t*> g_choice_list{nullptr};
|
|
|
|
void PointerStep(std::uint8_t* list, const std::uint8_t* focus) {
|
|
if (!focus || !*focus || InputBlocked()) return;
|
|
const std::int64_t focused_at = NowMs();
|
|
g_focus_ms.store(focused_at, std::memory_order_relaxed);
|
|
if (Field<std::int32_t>(list, kItems) > 1 && OnScreen(list)) {
|
|
g_choice_ms.store(focused_at, std::memory_order_relaxed);
|
|
g_choice_list.store(list, std::memory_order_relaxed);
|
|
}
|
|
if (g_holding_row.load(std::memory_order_relaxed)) return;
|
|
float x, y;
|
|
if (!BbInput::MouseIsMenuPointer() || !BbInput::PointerOnStage(x, y)) return;
|
|
bool fresh;
|
|
Seen& seen = SeenBy(list, fresh);
|
|
const bool moved = !fresh && (x != seen.x || y != seen.y);
|
|
seen.x = x;
|
|
seen.y = y;
|
|
|
|
if (BbInput::TextEntryOpen()) return;
|
|
if (g_log && !seen.described) {
|
|
seen.described = true;
|
|
host_log("pointer: list %p: %d items, %d cells x %d lines, cursor %d, scrolled %d/%d", static_cast<void*>(list),
|
|
Field<std::int32_t>(list, kItems), Field<std::int32_t>(list, kCells), Field<std::int32_t>(list, kLines),
|
|
CursorOf(list), Field<std::int32_t>(list, kScrollLines), Field<std::int32_t>(list, kAlong));
|
|
ItemUnder(list, x, y, true);
|
|
}
|
|
const bool click = ClickWaiting();
|
|
if (Field<std::int32_t>(list, kLines) == 1 && Field<std::int32_t>(list, kCells) == 1 &&
|
|
Field<std::int32_t>(list, kItems) > 1) {
|
|
if (click) SpinnerClick(list, x, y);
|
|
return;
|
|
}
|
|
if (!moved && !click) return;
|
|
if (DirectionHeld()) {
|
|
static int told = 0;
|
|
if (g_log && told++ < 60) host_log("pointer: list %p: not hovered, a direction is held", static_cast<void*>(list));
|
|
return;
|
|
}
|
|
const Pointed p = ItemUnder(list, x, y, false);
|
|
if (p.index < 0) return;
|
|
if (click) g_click.store(false, std::memory_order_relaxed);
|
|
const int was = CursorOf(list);
|
|
int now = was;
|
|
if (p.index != was) {
|
|
|
|
const int along = Field<std::int32_t>(list, kAlong), lines = Field<std::int32_t>(list, kScrollLines);
|
|
const auto answer = std::int32_t(Call(Image(kSetCursor), list, p.index));
|
|
const int back_along = along - Field<std::int32_t>(list, kAlong);
|
|
const int back_lines = lines - Field<std::int32_t>(list, kScrollLines);
|
|
if (answer == 1 && (back_along || back_lines)) {
|
|
Call(Image(kScrollBy), list, back_along, back_lines);
|
|
|
|
Call(Image(kHideCursors), list);
|
|
ShowCursorOf(list, p.cell);
|
|
}
|
|
now = CursorOf(list);
|
|
if (g_log) host_log("pointer: list %p hover %d -> %d (SetCursor %d)", static_cast<void*>(list), was, now, answer);
|
|
}
|
|
|
|
if (click && now == p.index) {
|
|
const std::uint32_t press = ClickMeaning(list, p, x, y);
|
|
g_press.store(press, std::memory_order_relaxed);
|
|
if (g_log) host_log("pointer: list %p click on item %d -> pad %04x", static_cast<void*>(list), p.index, press);
|
|
}
|
|
if (now != was) CursorMoved(list, now);
|
|
}
|
|
|
|
std::int64_t __attribute__((sysv_abi)) OnListUpdate(std::uint64_t, const std::uint64_t* saved) {
|
|
auto* list = reinterpret_cast<std::uint8_t*>(saved[5]);
|
|
const auto* focus = reinterpret_cast<const std::uint8_t*>(saved[4]);
|
|
PointerStep(list, focus);
|
|
BbEngine::Options::ListUpdate(list, focus && *focus);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
bool Install(std::uint8_t* image, std::size_t size) {
|
|
g_image = image;
|
|
const char* log = std::getenv("BB_POINTER_LOG");
|
|
g_log = log && log[0] == '1';
|
|
BbEngine::Hook hook;
|
|
if (!BbEngine::Prepare(hook, image, size, kListUpdate, kListUpdateEntry, OnListUpdate)) {
|
|
host_log("pointer: off, the menus' list update is not 1.09's");
|
|
return false;
|
|
}
|
|
BbEngine::Commit(std::span(&hook, 1));
|
|
host_log("pointer: the menus' lists follow the mouse");
|
|
return true;
|
|
}
|
|
|
|
bool FrontEnd() {
|
|
std::uint8_t started = 1;
|
|
const std::uint64_t flow = Flow();
|
|
return flow && Read(flow + kFlowStarted, started) && !started;
|
|
}
|
|
|
|
bool Navigating() {
|
|
const bool front = FrontEnd(), open = !front && MenuOpen();
|
|
const bool choice = !front && !open && NowMs() - g_choice_ms.load(std::memory_order_relaxed) < kNavigatingMs;
|
|
const int why = front ? 1 : open ? 2 : choice ? 3 : 0;
|
|
static int last = -1, told = 0;
|
|
if (why != last && told < 40) {
|
|
++told;
|
|
static const char* const kWhy[] = {"off", "the front end", "a menu the game opened", "a list with a choice"};
|
|
if (why == 3) host_log("menu keys: %s (%p)", kWhy[why], static_cast<const void*>(g_choice_list.load(std::memory_order_relaxed)));
|
|
else host_log("menu keys: %s", kWhy[why]);
|
|
}
|
|
last = why;
|
|
return why != 0;
|
|
}
|
|
|
|
void HoldRow(bool hold) { g_holding_row.store(hold, std::memory_order_relaxed); }
|
|
|
|
void Click() {
|
|
g_click_ms.store(NowMs(), std::memory_order_relaxed);
|
|
g_click.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
std::uint32_t TakePress() { return g_press.exchange(0); }
|
|
}
|
|
|
|
std::uint32_t menu_confirm_button() {
|
|
return MenuPointer::RegionDecidesWithCircle() ? PadBits::kCircle : PadBits::kCross;
|
|
}
|
|
|
|
std::uint32_t menu_back_button() { return menu_confirm_button() == PadBits::kCircle ? PadBits::kCross : PadBits::kCircle; }
|
|
|
|
bool menu_pointer_in_menu() { return MenuPointer::InMenu(); }
|