mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Swing the shake key's stick by the game's updates instead of the clock, two updates a side at any frame rate.
shake_stick picked the side from the time since the press, 67 ms a side (two frames at the game's 30 fps), so above 30 fps each side lasted more of the game's updates: 8 at 120 fps and 16 at 240, slower swings than the game reads at its own rate. The side now comes from the game's present count, which frame_rate.c already kept for the flight recorder and now gives through frame_step_presents, two presents a side. The press still swings for 800 ms of the clock and a hold keeps it going, as before; while no present comes (a load) the stick keeps its side. Not reproduced before the change (read from the code). The new tests/shake_key_check.py builds shake_stick from input.c and passes (it fails 118 times on the old code); the runtime compile check and the -Wall comparison of input.c and frame_rate.c show no new warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -756,6 +756,11 @@ files.
|
||||
A change to how a gamepad's axes are read (`runtime/input.c` `pad_axis`: an axis counts once it has been seen at rest)
|
||||
passes `python tests\pad_rest_check.py`, which builds that function with stand-ins for SDL. No game files.
|
||||
|
||||
A change to the shake key (`runtime/input.c` `shake_stick`) passes `python tests\shake_key_check.py`, which builds that
|
||||
function with stand-ins for the clock and the game's present count: each side lasts two of the game's updates from 20
|
||||
to 240 fps, also with two reads an update, a press swings for 800 ms, a hold keeps swinging until its release, and a
|
||||
swing without presents keeps its side. No game files.
|
||||
|
||||
A change to HD rumble's encoding or report schedule (`runtime/rumble_hd.c`: a vibration value as the bytes of a Switch
|
||||
controller's rumble report, and which reports are written when) passes `python tests\rumble_hd_check.py`, which builds
|
||||
that file alone: known values and the neutral one, the frequency and amplitude clamps (no amplitude step above the
|
||||
|
||||
@@ -425,7 +425,10 @@ static double ocarina_step_s(void) {
|
||||
return base * speed / 60.0;
|
||||
}
|
||||
|
||||
static _Atomic uint64_t guest_presents; // frame_step_present calls so far (frame_step_flight)
|
||||
static _Atomic uint64_t guest_presents; // frame_step_present calls so far (frame_step_flight, frame_step_presents)
|
||||
|
||||
// The game's presents so far, one per update of the game at any frame rate (input.c shake_stick counts its updates).
|
||||
uint64_t frame_step_presents(void) { return atomic_load_explicit(&guest_presents, memory_order_relaxed); }
|
||||
|
||||
// The flight recorder's view of the step at a host present (flight.c, render thread): the newest step the patch page
|
||||
// holds in 60 Hz frames per update (set ostep; -1 without), the game's own step from its time manager in 60 Hz frames
|
||||
|
||||
@@ -469,6 +469,7 @@ void frame_rate_sound_gate_host(Cpu *c); // the sound job at wh
|
||||
void frame_rate_sound_step_host(Cpu *c); // the step the sound job hands on (set osound, page +0xf00)
|
||||
float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1
|
||||
void frame_step_flight(float *page, float *game, uint64_t *presents); // flight.c: the step at a host present
|
||||
uint64_t frame_step_presents(void); // the game's presents so far: one per update of the game
|
||||
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs
|
||||
void rtss_limit_want(int64_t mhz); // the pacer's limit in millihertz: >0, 0 none; or why RTSS is
|
||||
// kept out (rtss_decide.h RTSS_OUT_*)
|
||||
|
||||
@@ -1032,21 +1032,23 @@ static void pick(int32_t *x, int32_t *y, int32_t kx, int32_t ky) {
|
||||
}
|
||||
|
||||
// The shake key (B_SHAKE): the game detaches a part fused to the held object when the right stick swings left and
|
||||
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging; each side lasts SHAKE_SIDE_NS, two
|
||||
// frames at the game's 30 fps, so every side is read even at 20 fps. Returns the stick x, 0 while not shaking.
|
||||
// right a few times. A press swings it for SHAKE_NS, holding keeps it swinging. Each side lasts SHAKE_SIDE_STEPS
|
||||
// updates of the game, counted by its presents (frame_rate.c frame_step_presents), as two frames at the game's 30 fps
|
||||
// did, so the game sees the same swings at any frame rate. A side of a fixed time (two 30 fps frames, 67 ms) lasted 8
|
||||
// updates at 120 fps and 16 at 240. Returns the stick x, 0 while not shaking.
|
||||
// Under lock.
|
||||
#define SHAKE_NS 800000000ull
|
||||
#define SHAKE_SIDE_NS 66666667ull
|
||||
#define SHAKE_SIDE_STEPS 2
|
||||
static int32_t shake_stick(int held) {
|
||||
static int was;
|
||||
static uint64_t from, until;
|
||||
uint64_t now = plat_now_ns();
|
||||
if (held && !was && now >= until) from = now;
|
||||
uint64_t now = plat_now_ns(), steps = frame_step_presents();
|
||||
if (held && !was && now >= until) from = steps;
|
||||
if (held && !was) until = now + SHAKE_NS;
|
||||
if (held && until < now) until = now;
|
||||
was = held;
|
||||
if (now >= until && !held) return 0;
|
||||
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
|
||||
return (steps - from) / SHAKE_SIDE_STEPS % 2 ? STICK_MAX : -STICK_MAX;
|
||||
}
|
||||
|
||||
// Under lock and inside sdl_enter (the gamepads' state is up to date), state 1: the open gamepad's inputs by position
|
||||
|
||||
129
tests/shake_key_check.py
Normal file
129
tests/shake_key_check.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""The shake key swings the right stick by the game's updates, not by the clock (runtime/input.c shake_stick): builds
|
||||
shake_stick, cut from input.c by its text, with stand-ins for the clock and the present count
|
||||
(frame_rate.c frame_step_presents), and checks that
|
||||
|
||||
- each side lasts two updates at 20, 30, 60, 120, 144 and 240 fps, starting left, also when the stick is read
|
||||
twice an update;
|
||||
- a press swings it for 800 ms, a hold keeps it swinging and the release ends it at once;
|
||||
- a press inside the swing keeps its side, and a swing without presents (a load) keeps one side.
|
||||
|
||||
No game files, no SDL. Usage: python tests/shake_key_check.py [--cc <clang>]
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
HARNESS = r'''
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
enum { STICK_MAX = 32767 };
|
||||
static uint64_t now_ns = 5000000000ull, presents = 1000;
|
||||
static uint64_t plat_now_ns(void) { return now_ns; }
|
||||
static uint64_t frame_step_presents(void) { return presents; }
|
||||
%s
|
||||
static int fails;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL: " __VA_ARGS__); putchar('\n'); fails++; } } while (0)
|
||||
|
||||
// One update at hz: the clock moves a frame, the game presents once, and the stick is read reads times.
|
||||
static int32_t update(int hz, int held, int reads) {
|
||||
now_ns += 1000000000ull / (uint64_t)hz;
|
||||
presents++;
|
||||
int32_t x = 0;
|
||||
for (int r = 0; r < reads; r++) {
|
||||
int32_t y = shake_stick(held);
|
||||
CHECK(r == 0 || y == x, "two reads in one update differ: %%d %%d", x, y);
|
||||
x = y;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
static void settle(void) {
|
||||
for (int i = 0; i < 4; i++) now_ns += 1000000000ull, presents++, shake_stick(0);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const int rates[] = {20, 30, 60, 120, 144, 240};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
for (int reads = 1; reads <= 2; reads++) {
|
||||
int hz = rates[i];
|
||||
settle();
|
||||
int32_t first = shake_stick(1); // the press, read before the update that shows it
|
||||
CHECK(first == -STICK_MAX, "%%d fps: the swing starts left (%%d)", hz, first);
|
||||
int n = 0, sides = 0;
|
||||
int32_t last = first, run = 1;
|
||||
for (int k = 0; k < hz * 2; k++) {
|
||||
int32_t x = update(hz, 0, reads);
|
||||
if (!x) break;
|
||||
n++;
|
||||
if (x != last) {
|
||||
CHECK(run == 2, "%%d fps, %%d reads: a side lasted %%d updates", hz, reads, run);
|
||||
sides++, run = 0, last = x;
|
||||
}
|
||||
run++;
|
||||
}
|
||||
// the press swings for 800 ms: 0.8 * hz updates after the press, the first counted at the press
|
||||
int want = (int)(0.8 * hz + 0.5) - 1;
|
||||
CHECK(n >= want - 1 && n <= want + 1, "%%d fps: %%d updates swung, want about %%d", hz, n, want);
|
||||
CHECK(sides >= n / 2 - 1, "%%d fps: %%d sides in %%d updates", hz, sides, n);
|
||||
}
|
||||
}
|
||||
// A hold keeps swinging past 800 ms; the release ends it at once.
|
||||
settle();
|
||||
shake_stick(1);
|
||||
int swung = 0;
|
||||
for (int k = 0; k < 240 * 2; k++) swung += update(240, 1, 1) != 0;
|
||||
CHECK(swung == 480, "a 2 s hold swung %%d of 480 updates", swung);
|
||||
CHECK(update(240, 0, 1) == 0, "the release after a long hold ends the swing");
|
||||
// A second press inside the swing keeps the side it is on.
|
||||
settle();
|
||||
shake_stick(1);
|
||||
update(60, 0, 1), update(60, 0, 1);
|
||||
int32_t on = update(60, 0, 1);
|
||||
int32_t again = shake_stick(1);
|
||||
CHECK(on == STICK_MAX && again == on, "a press inside the swing moved the side: %%d to %%d", on, again);
|
||||
// No presents (a load screen): the stick keeps its side while the swing lasts.
|
||||
settle();
|
||||
shake_stick(1);
|
||||
for (int k = 0; k < 10; k++) {
|
||||
now_ns += 50000000ull;
|
||||
CHECK(shake_stick(0) == -STICK_MAX, "without presents the side changed at %%d", k);
|
||||
}
|
||||
printf(fails ? "shake key: %%d failures\n" : "shake key: ok\n", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def cut(text, start, end_marker):
|
||||
a = text.index(start)
|
||||
b = text.index(end_marker, a)
|
||||
return text[a:b]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--cc', help='the C compiler (default: MSYS2 UCRT64 clang on Windows, clang elsewhere)')
|
||||
args = ap.parse_args()
|
||||
win = os.name == 'nt'
|
||||
cc = args.cc or (str(MSYS_BIN / 'clang.exe') if win and MSYS_BIN.exists() else 'clang')
|
||||
env = dict(os.environ)
|
||||
if win and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
src = (ROOT / 'runtime' / 'input.c').read_text(encoding='utf-8')
|
||||
body = cut(src, '#define SHAKE_NS', '\n// Under lock and inside sdl_enter')
|
||||
with tempfile.TemporaryDirectory(prefix='nx2navi-shake-') as tmp:
|
||||
c = Path(tmp) / 'check.c'
|
||||
c.write_text(HARNESS % body, encoding='utf-8')
|
||||
exe = Path(tmp) / ('check.exe' if win else 'check')
|
||||
subprocess.check_call([cc, '-O2', '-Wall', '-Wextra', '-Wno-unused-function', str(c), '-o', str(exe)], env=env)
|
||||
return subprocess.call([str(exe)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user