Skip to content

Commit 5f7fb2f

Browse files
committed
fix sum shit
1 parent dd59d0e commit 5f7fb2f

9 files changed

Lines changed: 359 additions & 33 deletions

File tree

src/app/App.cpp

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,27 @@ bool App::init() {
7979
elog("Network: socket_lib_init done, network ready");
8080
#endif
8181

82-
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
82+
// Do NOT add SDL_INIT_GAMECONTROLLER. SDL2-wuhb crashes the RPX loader
83+
// before main() runs ("failed to load payload"). We only need
84+
// SDL_JOYBUTTONDOWN events anyway, not the SDL_GameController API.
85+
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) != 0) {
8386
LOG_ERROR("SDL_Init failed: %s", SDL_GetError());
8487
return false;
8588
}
8689
elog("SDL_Init OK");
90+
// Open every already-connected joystick so SDL starts forwarding events
91+
// for it. SDL only emits SDL_JOYBUTTONDOWN from opened joysticks. New
92+
// devices that plug in later are handled in App::run via
93+
// SDL_JOYDEVICEADDED.
94+
{
95+
int n = SDL_NumJoysticks();
96+
elogf("SDL: %d joystick(s) connected at startup", n);
97+
for (int i = 0; i < n; i++) {
98+
SDL_Joystick* j = SDL_JoystickOpen(i);
99+
if (j) elogf(" opened: %s", SDL_JoystickName(j));
100+
else elogf(" open failed for joystick %d: %s", i, SDL_GetError());
101+
}
102+
}
87103
if (TTF_Init() != 0) {
88104
LOG_ERROR("TTF_Init failed: %s", TTF_GetError());
89105
return false;
@@ -204,9 +220,33 @@ void App::run() {
204220
if (frameNum < 10) elogf("frame %d begin", frameNum);
205221
SDL_Event event;
206222
while (SDL_PollEvent(&event)) {
207-
if (event.type == SDL_QUIT) {
208-
elog("got SDL_QUIT");
209-
m_running = false;
223+
switch (event.type) {
224+
case SDL_QUIT:
225+
elog("got SDL_QUIT");
226+
m_running = false;
227+
break;
228+
case SDL_JOYDEVICEADDED: {
229+
// SDL won't deliver button events from a joystick until
230+
// someone opens it. Pattern from fortheusers/chesto.
231+
SDL_Joystick* j = SDL_JoystickOpen(event.jdevice.which);
232+
if (j) elogf("joystick connected: %s", SDL_JoystickName(j));
233+
break;
234+
}
235+
case SDL_JOYDEVICEREMOVED: {
236+
SDL_Joystick* j = SDL_JoystickFromInstanceID(event.jdevice.which);
237+
if (j) {
238+
elogf("joystick disconnected: %s", SDL_JoystickName(j));
239+
SDL_JoystickClose(j);
240+
}
241+
break;
242+
}
243+
case SDL_JOYBUTTONDOWN:
244+
// Single code path for all Wii U controllers: GamePad,
245+
// Pro Controller, Wii Remote (+ extensions), Classic.
246+
// SDL2-wuhb's joystick driver normalises every device to
247+
// the same button-index space (see Input.h enum).
248+
Input::onJoyButtonDown(event.jbutton.button);
249+
break;
210250
}
211251
}
212252
update();

src/app/Input.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#include "Input.h"
2+
3+
uint32_t Input::s_pressedBits = 0;
4+
5+
void Input::onJoyButtonDown(int buttonIdx) {
6+
if (buttonIdx < 0 || buttonIdx >= BTN_COUNT) return;
7+
s_pressedBits |= (1u << buttonIdx);
8+
}
9+
10+
Input Input::read() {
11+
Input in;
12+
const uint32_t bits = s_pressedBits;
13+
s_pressedBits = 0;
14+
15+
auto has = [bits](BtnIdx b) { return (bits & (1u << b)) != 0; };
16+
17+
in.a = has(BTN_A);
18+
in.b = has(BTN_B);
19+
in.x = has(BTN_X);
20+
in.y = has(BTN_Y);
21+
in.l = has(BTN_L);
22+
in.r = has(BTN_R);
23+
in.zl = has(BTN_ZL);
24+
in.zr = has(BTN_ZR);
25+
in.plus = has(BTN_PLUS);
26+
in.minus = has(BTN_MINUS);
27+
// D-pad OR'd with left-stick-emulation so sticks navigate the UI just
28+
// like the D-pad. Right stick is intentionally ignored: nothing in the
29+
// app uses it, and an over-eager grip would otherwise trigger nav events.
30+
in.up = has(BTN_UP) || has(BTN_LSTICK_UP);
31+
in.down = has(BTN_DOWN) || has(BTN_LSTICK_DOWN);
32+
in.left = has(BTN_LEFT) || has(BTN_LSTICK_LEFT);
33+
in.right = has(BTN_RIGHT) || has(BTN_LSTICK_RIGHT);
34+
// home stays false; Aroma intercepts HOME before SDL sees it.
35+
36+
return in;
37+
}

src/app/Input.h

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
#pragma once
2-
#include <vpad/input.h>
2+
#include <cstdint>
33

4+
// Game input, populated from SDL joystick events. The same code path works
5+
// for every Wii U input device (GamePad, Pro Controller, Wii Remote +/-
6+
// extensions, Classic Controller) because SDL2-wuhb's WIIU_JoystickInit
7+
// already initialises VPAD + KPAD + URCC and emits a single SDL_JOYBUTTONDOWN
8+
// per physical button press, with a canonical button index that matches
9+
// across all device types. See devkitPro/SDL src/joystick/wiiu for the
10+
// per-device index maps:
11+
//
12+
// 0=A 1=B 2=X 3=Y
13+
// 4=STICK_L_PUSH 5=STICK_R_PUSH
14+
// 6=L 7=R 8=ZL 9=ZR
15+
// 10=PLUS 11=MINUS
16+
// 12=LEFT 13=UP 14=RIGHT 15=DOWN
17+
// 16=LSTICK_LEFT 17=LSTICK_UP 18=LSTICK_RIGHT 19=LSTICK_DOWN
18+
// 20=RSTICK_LEFT 21=RSTICK_UP 22=RSTICK_RIGHT 23=RSTICK_DOWN
19+
//
20+
// SDL handles the stick-as-d-pad emulation for us: when the stick crosses
21+
// the threshold, SDL fires a SDL_JOYBUTTONDOWN with the corresponding
22+
// LSTICK_* index, then SDL_JOYBUTTONUP when it returns to centre. Same
23+
// edge-triggered semantics as the actual D-pad. We don't need to poll axes.
24+
//
25+
// App::run's event loop calls Input::onJoyButtonDown() for each
26+
// SDL_JOYBUTTONDOWN. Per frame, App::update() calls Input::read() which
27+
// returns a freshly drained snapshot.
428
struct Input {
529
bool up = false;
630
bool down = false;
@@ -16,32 +40,33 @@ struct Input {
1640
bool zr = false;
1741
bool plus = false;
1842
bool minus = false;
19-
bool home = false;
43+
bool home = false; // never fires: Aroma intercepts HOME before us
2044

21-
static Input read() {
22-
Input in;
23-
VPADStatus status;
24-
VPADReadError err;
25-
if (VPADRead(VPAD_CHAN_0, &status, 1, &err) > 0) {
26-
uint32_t btn = status.trigger;
27-
float lx = status.leftStick.x;
28-
float ly = status.leftStick.y;
29-
in.up = (btn & VPAD_BUTTON_UP) || ly > 0.5f;
30-
in.down = (btn & VPAD_BUTTON_DOWN) || ly < -0.5f;
31-
in.left = (btn & VPAD_BUTTON_LEFT) || lx < -0.5f;
32-
in.right = (btn & VPAD_BUTTON_RIGHT) || lx > 0.5f;
33-
in.a = btn & VPAD_BUTTON_A;
34-
in.b = btn & VPAD_BUTTON_B;
35-
in.l = btn & VPAD_BUTTON_L;
36-
in.r = btn & VPAD_BUTTON_R;
37-
in.y = btn & VPAD_BUTTON_Y;
38-
in.x = btn & VPAD_BUTTON_X;
39-
in.zl = btn & VPAD_BUTTON_ZL;
40-
in.zr = btn & VPAD_BUTTON_ZR;
41-
in.plus = btn & VPAD_BUTTON_PLUS;
42-
in.minus = btn & VPAD_BUTTON_MINUS;
43-
in.home = btn & VPAD_BUTTON_HOME;
44-
}
45-
return in;
46-
}
45+
// SDL2-wuhb button indices. Order MUST match the arrays in the SDL
46+
// joystick driver (devkitPro/SDL src/joystick/wiiu/SDL_wiiujoystick.h).
47+
enum BtnIdx {
48+
BTN_A = 0, BTN_B, BTN_X, BTN_Y,
49+
BTN_STICK_L_PUSH, BTN_STICK_R_PUSH,
50+
BTN_L, BTN_R, BTN_ZL, BTN_ZR,
51+
BTN_PLUS, BTN_MINUS,
52+
BTN_LEFT, BTN_UP, BTN_RIGHT, BTN_DOWN,
53+
BTN_LSTICK_LEFT, BTN_LSTICK_UP, BTN_LSTICK_RIGHT, BTN_LSTICK_DOWN,
54+
BTN_RSTICK_LEFT, BTN_RSTICK_UP, BTN_RSTICK_RIGHT, BTN_RSTICK_DOWN,
55+
BTN_COUNT
56+
};
57+
58+
// Called from App::run's event loop when a SDL_JOYBUTTONDOWN arrives.
59+
// Accumulates presses across the frame; read() drains.
60+
static void onJoyButtonDown(int buttonIdx);
61+
62+
// Drain the accumulator into a fresh Input. Called once per frame in
63+
// App::update(), BEFORE the active screen's handleInput. Resets the
64+
// accumulator so the same press isn't seen twice.
65+
static Input read();
66+
67+
private:
68+
// Bitfield of indices pressed since last read(). 24 bits used; uint32_t
69+
// is comfortable. Not atomic: SDL_PollEvent runs on the main thread, as
70+
// does read().
71+
static uint32_t s_pressedBits;
4772
};

src/ui/BrowseScroll.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
3+
// Pure scroll-window math for the Browse-tab card grid.
4+
//
5+
// Inputs:
6+
// currentScroll top row index currently visible
7+
// selectedRow row index of the cursor (0-based)
8+
// visibleRows how many rows fit on screen (must be >= 1)
9+
// Returns: the new scroll value that keeps `selectedRow` in view with
10+
// minimal movement (no scroll if already visible).
11+
//
12+
// Extracted for unit testing; the actual MainLayout::handleBrowseInput
13+
// just calls this and assigns the result.
14+
inline int computeBrowseScroll(int currentScroll, int selectedRow, int visibleRows) {
15+
if (visibleRows < 1) visibleRows = 1;
16+
if (currentScroll < 0) currentScroll = 0;
17+
if (selectedRow < currentScroll) {
18+
return selectedRow;
19+
}
20+
int lastVisible = currentScroll + visibleRows - 1;
21+
if (selectedRow > lastVisible) {
22+
return selectedRow - visibleRows + 1;
23+
}
24+
return currentScroll;
25+
}

src/ui/MainLayout.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "ui/DetailScreen.h"
44
#include "ui/DownloadQueueScreen.h"
55
#include "ui/TagFilterScreen.h"
6+
#include "ui/BrowseScroll.h"
67
#include "net/DownloadQueue.h"
78
#include <sysapp/launch.h>
89
#include "mods/InstalledScanner.h"
@@ -211,6 +212,7 @@ void MainLayout::handleBrowseInput(const Input& input) {
211212
auto& mods = games[m_selectedGame].mods;
212213
int modCount = (int)mods.size();
213214
int cols = CARDS_PER_ROW;
215+
int prevGame = m_selectedGame;
214216

215217
if (input.right) {
216218
if ((m_selectedMod % cols) < cols - 1 && m_selectedMod + 1 < modCount) {
@@ -229,6 +231,18 @@ void MainLayout::handleBrowseInput(const Input& input) {
229231
if (input.down) { int n = m_selectedMod + cols; if (n < modCount) { m_selectedMod = n; AudioManager::get().playSound(SoundId::Navigate); } }
230232
if (input.up) { int p = m_selectedMod - cols; if (p >= 0) { m_selectedMod = p; AudioManager::get().playSound(SoundId::Navigate); } }
231233

234+
// Reset scroll when the selected game changes, then ensure the new cursor
235+
// is visible. Otherwise just adjust scroll if cursor moved off the visible
236+
// rows. visibleRows is derived from the actual window height minus the
237+
// grid top (header/tab strip) and a small bottom strip for the hint line.
238+
if (m_selectedGame != prevGame) m_browseScrollRow = 0;
239+
const int H = m_app->screenHeight();
240+
const int bottomHint = 30;
241+
int visibleRows = (H - GRID_TOP - bottomHint) / (CARD_H + CARD_PAD);
242+
if (visibleRows < 1) visibleRows = 1;
243+
int selRow = m_selectedMod / cols;
244+
m_browseScrollRow = computeBrowseScroll(m_browseScrollRow, selRow, visibleRows);
245+
232246
// Pre-warm ImageCache for the currently-selected mod's images. When the
233247
// user presses A, DetailScreen finds them already cached -> no freeze.
234248
if (m_selectedMod < modCount) {
@@ -261,6 +275,7 @@ void MainLayout::handleBrowseInput(const Input& input) {
261275
[this](std::set<std::string> picked) {
262276
m_activeTags = std::move(picked);
263277
m_selectedMod = 0; // reset cursor when filter changes
278+
m_browseScrollRow = 0;
264279
persistUiState();
265280
}));
266281
}
@@ -682,11 +697,19 @@ void MainLayout::renderBrowse(SDL_Renderer* renderer) {
682697
default: break;
683698
}
684699
auto& mods = sortedMods;
700+
// Skip cards above the visible scroll window or below the bottom hint
701+
// line. Without this, render walks through hundreds of cards offscreen
702+
// for large repos, and the cursor visually disappears past the screen.
703+
const int rowH = CARD_H + CARD_PAD;
704+
const int bottomHint = 30;
685705
for (int i = 0; i < (int)mods.size(); i++) {
686706
auto& mod = mods[i];
687707
bool sel = (i == m_selectedMod);
708+
int row = i / CARDS_PER_ROW;
709+
if (row < m_browseScrollRow) continue; // above viewport
688710
int x = cx + CARD_PAD + (i % CARDS_PER_ROW) * (CARD_W + CARD_PAD);
689-
int y = GRID_TOP + (i / CARDS_PER_ROW) * (CARD_H + CARD_PAD);
711+
int y = GRID_TOP + (row - m_browseScrollRow) * rowH;
712+
if (y + CARD_H > H - bottomHint) break; // below viewport (rows are in order)
690713

691714
SDL_SetRenderDrawColor(renderer, 25, 25, 40, 255);
692715
SDL_Rect card = {x, y, CARD_W, CARD_H};

src/ui/MainLayout.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class MainLayout : public Screen {
7676
Repo m_repo;
7777
int m_selectedGame = 0;
7878
int m_selectedMod = 0;
79+
int m_browseScrollRow = 0; // top visible row in the Browse card grid
7980

8081
enum class SortMode { Default, NameAZ, Version };
8182
SortMode m_sortMode = SortMode::Default;

tests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ add_executable(cupstore_tests
3131
test_http_client.cpp
3232
test_text_cache.cpp
3333
test_sha256.cpp
34+
test_browse_scroll.cpp
35+
test_input.cpp
3436
curl_mock.cpp
3537
stubs.cpp
3638
${SRC}/mods/ConflictChecker.cpp
@@ -46,6 +48,7 @@ add_executable(cupstore_tests
4648
${SRC}/net/HttpClient.cpp
4749
${SRC}/util/TextCache.cpp
4850
${SRC}/util/sha256.cpp
51+
${SRC}/app/Input.cpp
4952
)
5053
# TextCache pulls SDL headers; we only test the no-renderer paths so functions
5154
# can be stubbed.

tests/test_browse_scroll.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include "ui/BrowseScroll.h"
3+
4+
// computeBrowseScroll(currentScroll, selectedRow, visibleRows) -> newScroll
5+
//
6+
// Property: after calling this, selectedRow is in [newScroll, newScroll + visibleRows - 1],
7+
// and we move the window by the minimum amount necessary.
8+
9+
TEST_CASE("BrowseScroll: cursor already visible -> no scroll change", "[browse-scroll]") {
10+
REQUIRE(computeBrowseScroll(0, 0, 4) == 0);
11+
REQUIRE(computeBrowseScroll(0, 3, 4) == 0);
12+
REQUIRE(computeBrowseScroll(2, 2, 4) == 2);
13+
REQUIRE(computeBrowseScroll(2, 5, 4) == 2);
14+
}
15+
16+
TEST_CASE("BrowseScroll: cursor below viewport -> scroll down minimally", "[browse-scroll]") {
17+
// visibleRows=4, scroll=0 means rows 0..3 visible. selectedRow=4 needs scroll=1.
18+
REQUIRE(computeBrowseScroll(0, 4, 4) == 1);
19+
// selectedRow=10 -> need rows 7..10 visible -> scroll=7
20+
REQUIRE(computeBrowseScroll(0, 10, 4) == 7);
21+
}
22+
23+
TEST_CASE("BrowseScroll: cursor above viewport -> scroll up to expose it", "[browse-scroll]") {
24+
// scroll=5 visibleRows=4 means rows 5..8 visible. selectedRow=3 means jump up.
25+
REQUIRE(computeBrowseScroll(5, 3, 4) == 3);
26+
REQUIRE(computeBrowseScroll(10, 0, 4) == 0);
27+
}
28+
29+
TEST_CASE("BrowseScroll: scrolling row-by-row matches expected window", "[browse-scroll]") {
30+
int scroll = 0;
31+
const int rows = 3;
32+
// Simulate cursor moving down 0..10
33+
for (int sel = 0; sel <= 10; sel++) {
34+
scroll = computeBrowseScroll(scroll, sel, rows);
35+
// sel must be visible
36+
REQUIRE(sel >= scroll);
37+
REQUIRE(sel <= scroll + rows - 1);
38+
}
39+
// After scrolling to row 10 with visibleRows=3, scroll should be 8
40+
REQUIRE(scroll == 8);
41+
}
42+
43+
TEST_CASE("BrowseScroll: degenerate visibleRows clamped to 1", "[browse-scroll]") {
44+
REQUIRE(computeBrowseScroll(0, 5, 0) == 5);
45+
REQUIRE(computeBrowseScroll(0, 5, -1) == 5);
46+
}
47+
48+
TEST_CASE("BrowseScroll: negative currentScroll is normalised", "[browse-scroll]") {
49+
// Shouldn't happen in practice, but ensure we don't underflow.
50+
REQUIRE(computeBrowseScroll(-3, 2, 4) == 0); // row 2 visible from scroll=0
51+
}
52+
53+
TEST_CASE("BrowseScroll: selectedRow == lastVisible edge", "[browse-scroll]") {
54+
// visibleRows=4 scroll=0 -> last visible = 3. row 3 should not move scroll.
55+
REQUIRE(computeBrowseScroll(0, 3, 4) == 0);
56+
// row 4 should bump scroll by 1.
57+
REQUIRE(computeBrowseScroll(0, 4, 4) == 1);
58+
}

0 commit comments

Comments
 (0)