Skip to content

fix(gamepad): derive Android type_hint from device name (#270)#31

Merged
apotema merged 1 commit into
mainfrom
fix/270-android-gamepad-type-hint
Jun 11, 2026
Merged

fix(gamepad): derive Android type_hint from device name (#270)#31
apotema merged 1 commit into
mainfrom
fix/270-android-gamepad-type-hint

Conversation

@apotema

@apotema apotema commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

On sokol-Android the gamepad_connected event carried type_hint = .unknown, so the gamepad-android HUD showed [unknown] while the desktop raylib path showed [xbox] for the same controller (labelle-assembler#270).

Tracing the path: the JNI glue (backends/sokol/src/android_gamepad_jni.c) resolves InputDevice.getName() and passes it to core's labelle_android_on_device_added(device_id, sources, name_ptr, name_len, descriptor_ptr, descriptor_len). Core already called ev.setName(name) and ev.source_class = classifySources(sources) — but never set ev.type_hint. So the name reached core fine; only the type classification was missing.

Fix

  • Add a shared, name-only typeHintFromName classifier to the gamepad contract (gamepad.zig) — case-insensitive substring match for xbox / playstation / nintendo, .generic for any other non-empty name, .unknown for empty. This is the canonical version of the helper currently duplicated in the raylib backend (typeHintFromName) and the wasm source (typeHintFromId).
  • Re-export it (and TypeHint) from gamepad_source/root.zig.
  • In android.zig's labelle_android_on_device_added, set ev.type_hint = source.typeHintFromName(ev.nameSlice()). Uses the buffered name so truncation is honored. Stays behind the existing is_android gating.

Tests

  • typeHintFromName host tests in gamepad.zig (xbox/ps/nintendo/generic/unknown).
  • A host test in android.zig locking the connect-event wiring (setName then typeHintFromName(nameSlice()).xbox).
  • zig build test: 84/84 pass, and the build's check-platforms step compile-checks gamepad_source for aarch64-linux-android (success), aarch64-ios, x86_64-linux, wasm32. Also verified a direct zig build-obj -target aarch64-linux-android src/root.zig.

On-device verification on the test tablet is pending (handled by the main session).

Closes the core half of labelle-assembler#270. The assembler PR (gamepad-android demo nameFor by-id fix) depends on this change for the [xbox] type half; cross-referenced there.

The sokol-Android gamepad source already received the real
`InputDevice.getName()` via the JNI glue and set `ev.setName()` /
`ev.source_class`, but it never set `ev.type_hint`, so the
`gamepad_connected` payload carried `unknown` and the HUD showed
`[unknown]` where the desktop raylib path showed `[xbox]`.

Add a shared, name-only `typeHintFromName` classifier to the gamepad
contract (`gamepad.zig`), re-export it (plus `TypeHint`) from
`gamepad_source/root.zig`, and use it in `android.zig`'s
`labelle_android_on_device_added` to derive the vendor family from the
buffered name. This matches the desktop (raylib) and web (wasm) name-only
classification and is the canonical shared helper future backends should
reuse instead of duplicating.

Android-only emission stays gated behind the existing `is_android` checks;
the classifier itself is platform-agnostic and host-tested.

Refs: labelle-assembler#270
@cursor

cursor Bot commented Jun 11, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Small metadata change on hotplug connect events only; no input state, auth, or persistence impact.

Overview
Android connect events no longer leave type_hint at .unknown when only a device name is available. labelle_android_on_device_added now sets ev.type_hint from the buffered name via a shared classifier, so HUD labels can show e.g. [xbox] instead of [unknown] (labelle-assembler#270).

The PR adds typeHintFromName to the gamepad contract in gamepad.zig: case-insensitive substring rules for Xbox, PlayStation, and Nintendo; .generic for other non-empty names; .unknown for empty. TypeHint and typeHintFromName are re-exported from gamepad_source/root.zig so platform sources share one name-only path with raylib/web. Host tests cover the classifier and the Android setNametypeHintFromName(nameSlice()) wiring.

Reviewed by Cursor Bugbot for commit 3a7300f. Bugbot is set up for automated code reviews on this repo. Configure here.

@apotema

apotema commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Assembler-side companion: labelle-toolkit/labelle-assembler#272 (gamepad-android demo nameFor by-id lookup). That PR depends on this one for the [xbox] type_hint half — merge this first / repoint the core pin on release.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a shared name-based gamepad classifier (typeHintFromName) to derive the vendor family (TypeHint) from human-readable device names when stable USB vendor IDs are unavailable, specifically integrating it into the Android gamepad source. The review feedback highlights a potential misclassification bug where generic terms like "wireless controller" (checked in the PlayStation block) could override more specific brand keywords like "Nintendo" or "Switch" if the PlayStation checks are performed first. It is recommended to reorder the classification checks and add a corresponding test case to ensure "Nintendo Switch Wireless Controller" is correctly classified.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/gamepad.zig
Comment on lines +38 to +52
pub fn typeHintFromName(name: []const u8) TypeHint {
if (containsIgnoreCase(name, "xbox") or
containsIgnoreCase(name, "microsoft")) return .xbox;
if (containsIgnoreCase(name, "playstation") or
containsIgnoreCase(name, "dualsense") or
containsIgnoreCase(name, "dualshock") or
containsIgnoreCase(name, "sony") or
containsIgnoreCase(name, "wireless controller")) return .playstation;
if (containsIgnoreCase(name, "nintendo") or
containsIgnoreCase(name, "switch") or
containsIgnoreCase(name, "joy-con") or
containsIgnoreCase(name, "pro controller")) return .nintendo;
if (name.len > 0) return .generic;
return .unknown;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Because "wireless controller" is a very generic term that matches many controllers (such as a "Nintendo Switch Wireless Controller"), checking for it in the PlayStation block before checking for Nintendo terms will cause Nintendo wireless controllers to be incorrectly classified as .playstation.

Swapping the order of the Nintendo and PlayStation checks resolves this, ensuring that specific Nintendo keywords like "nintendo" or "switch" are matched first, while still allowing the generic "wireless controller" fallback (common for Sony DualShock controllers on macOS/iOS) to correctly map to .playstation.

pub fn typeHintFromName(name: []const u8) TypeHint {
    if (containsIgnoreCase(name, "xbox") or
        containsIgnoreCase(name, "microsoft")) return .xbox;
    if (containsIgnoreCase(name, "nintendo") or
        containsIgnoreCase(name, "switch") or
        containsIgnoreCase(name, "joy-con") or
        containsIgnoreCase(name, "pro controller")) return .nintendo;
    if (containsIgnoreCase(name, "playstation") or
        containsIgnoreCase(name, "dualsense") or
        containsIgnoreCase(name, "dualshock") or
        containsIgnoreCase(name, "sony") or
        containsIgnoreCase(name, "wireless controller")) return .playstation;
    if (name.len > 0) return .generic;
    return .unknown;
}

Comment thread src/gamepad.zig
Comment on lines +193 to +204
test "typeHintFromName classifies known vendor families (name-only path)" {
try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("Xbox Wireless Controller"));
try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("XBOX 360 For Windows"));
try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("Microsoft X-Box pad"));
try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("Sony DualSense Wireless Controller"));
try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("PLAYSTATION(R)3 Controller"));
try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("Wireless Controller"));
try std.testing.expectEqual(TypeHint.nintendo, typeHintFromName("Nintendo Switch Pro Controller"));
try std.testing.expectEqual(TypeHint.nintendo, typeHintFromName("Joy-Con (L)"));
try std.testing.expectEqual(TypeHint.generic, typeHintFromName("Generic USB Joystick"));
try std.testing.expectEqual(TypeHint.unknown, typeHintFromName(""));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case for "Nintendo Switch Wireless Controller" to ensure that controllers containing both a specific brand keyword (like "Switch") and the generic "Wireless Controller" phrase are correctly classified under their specific brand rather than falling back to PlayStation.

test "typeHintFromName classifies known vendor families (name-only path)" {
    try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("Xbox Wireless Controller"));
    try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("XBOX 360 For Windows"));
    try std.testing.expectEqual(TypeHint.xbox, typeHintFromName("Microsoft X-Box pad"));
    try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("Sony DualSense Wireless Controller"));
    try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("PLAYSTATION(R)3 Controller"));
    try std.testing.expectEqual(TypeHint.playstation, typeHintFromName("Wireless Controller"));
    try std.testing.expectEqual(TypeHint.nintendo, typeHintFromName("Nintendo Switch Pro Controller"));
    try std.testing.expectEqual(TypeHint.nintendo, typeHintFromName("Nintendo Switch Wireless Controller"));
    try std.testing.expectEqual(TypeHint.nintendo, typeHintFromName("Joy-Con (L)"));
    try std.testing.expectEqual(TypeHint.generic, typeHintFromName("Generic USB Joystick"));
    try std.testing.expectEqual(TypeHint.unknown, typeHintFromName(""));
}

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3a7300f. Configure here.

Comment thread src/gamepad.zig
containsIgnoreCase(name, "dualsense") or
containsIgnoreCase(name, "dualshock") or
containsIgnoreCase(name, "sony") or
containsIgnoreCase(name, "wireless controller")) return .playstation;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X-Box wireless misclassified PlayStation

Medium Severity

In typeHintFromName, the Xbox branch only matches the contiguous substring xbox, so names like X-Box Wireless Controller skip Xbox and Microsoft checks. The PlayStation branch then matches the wireless controller substring and returns .playstation, so some Xbox pads get PlayStation glyphs instead of Xbox on Android.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3a7300f. Configure here.

@apotema apotema merged commit 33272be into main Jun 11, 2026
2 checks passed
@apotema apotema deleted the fix/270-android-gamepad-type-hint branch June 11, 2026 17:15
apotema added a commit that referenced this pull request Jun 11, 2026
…32)

typeHintFromName checked the PlayStation block (which treats the generic
'wireless controller' as PS) before the Nintendo block, so a 'Nintendo
Switch Wireless Controller' misclassified as playstation. Move the
Nintendo brand-keyword check ahead of the generic PS fallback + add a
regression test. (Re-do of a #31-review fix that was lost when its
worktree was detached and the push silently no-op'd.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant