Switching Android to the N64 control style silently disabled several
overlay buttons: bondmove.c gates crouch cycling, the alt-fire toggle, the
reload hack, the radial menu and weapon next/prev behind
`controlmode == CONTROLMODE_PC`. In an N64 style they were all no-ops, so
the pad had buttons on it that did nothing.
Crouch is recoverable. It rides on synthetic pad bits -- CONT_8000, 4000 and
2000 -- that no real N64 controller has, so it cannot collide with any N64
button meaning. Its block moves out of the PC-only branch and now runs in
every control style. Verified on device: the view drops and the stance
cycles.
The rest are not recoverable. Their N64 pad equivalents already mean
something else in this style -- L is a second aim button, D-Left sidesteps,
D-Down looks down -- so enabling them would collide. They are removed from
the overlay instead, leaving a pad where every button does something, and
weapon cycling goes back to the N64 way: hold USE, tap FIRE or AIM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Launcher: Material 3 dark scheme keyed to Perfect Dark's cyan HUD rather
than the stock palette, an icon + headline hero, a large pill Play button,
filled cards per section, a tonal/outlined MaterialButtonGroup for the mod
actions, a Slider in place of the SeekBar, and Material dialogs and radio
buttons.
On-screen pad: the same accent, shape variety (FIRE and AIM become pills so
the triggers read differently to the face buttons), an eased press animation
that swells the control and flips its fill to the accent with a dark label,
a deadzone ring on the stick, an accent knob while it is held, and a pill
backdrop behind the editor hint so it stays legible over the game.
Material 1.14 is the actual Expressive release, but it requires minSdk 23
and minSdk 21 was a deliberate choice, so this builds on 1.13 -- the newest
that still supports 21 -- and assembles the expressive look from its
Material 3 styles: large corner radii, heavy type, tonal surfaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The overlay was driving aim while the C buttons did the walking. The cause
was not the overlay: the port defaults extcontrols to true, which forces
CONTROLMODE_PC, and in that mode bondmove.c takes walking and strafing from
the *second* stick (c2/rstick) and maps the C buttons to digitalstepforward
and digitalstepback. A single-stick touch pad therefore wrote to the aim
stick, and the C buttons moved the player.
Control style 1.1 is the N64 scheme and the one that matches this overlay:
bondmove.c's "1.1 or 1.3" branch sets digitalstepforward/back to false and
routes the C buttons to speedvertadown/up and sidestep, leaving the analog
stick to walk and turn.
extcontrols now defaults to false on Android only. Other platforms, which
have two real sticks, keep PC controls.
Note that pd.ini takes precedence over this default, so an existing install
keeps whatever ExtendedControls it already saved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The right stick was never a second analog stick as far as the game is
concerned: bondmove.c reads it as contpad1's rstick, which is PD's own
two-controller look mode. That is a different scheme from the one the N64
shipped with, where the analog stick does all movement and the C buttons
handle looking and sidestepping.
The overlay now matches the hardware: one stick, plus the C buttons in their
diamond where the second stick used to be. How the stick divides between
walking and turning is the game's Control Style setting, as it should be.
The right-stick path is removed rather than left dormant -- touchSetStick and
touchGetStick lose their stick index, inputReadController stops writing
rstick, and the look-sensitivity slider goes with it, since it only ever
scaled the stick that no longer exists.
Verified on device: C-up pitches the view, and the stick still drives
movement and turning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buttons were oversized on a real screen: FIRE alone covered about a quarter
of the display height. Face buttons come down roughly 25%, the sticks about
12%, and the offsets come in with them so the clusters stay tight against
their corners instead of leaving the smaller buttons stranded.
Radii stay at or above 24dp for anything used in play, which is the smallest
comfortable touch target.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tested on a Moto G 2025 (1604x720 landscape, Android 16), which is where all
of these showed up.
Look control was invisible. It was a transparent drag region rather than a
stick, and the scattered buttons sat on top of it, so there was nothing to
grab. It is now a real, drawn right stick mirroring the left one, and both
sticks are fixed rather than floating: deflection is measured from the
control's own centre, so what is under the thumb is what the game is told.
Layout was sized as a fraction of viewport height and positioned as a
fraction of width. On a 2.2:1 screen that made every button small and flung
them across the middle of the play area. Controls are now placed in dp from
the nearest corner, so they hold their physical size and grouping across
aspect ratios, and nothing sits over the centre of the screen.
Labels said Q, L, <, CR. They now say what the control does, with the N64
button underneath, auto-sized to fit the circle.
Quick taps could be dropped entirely: inputReadController() samples the pad
once a frame, so a press and release inside 16ms was never observed. Newly
pressed bits are latched in touch.c and reported for at least one frame.
Launcher showed a purple ActionBar over the title and ran under the status
and navigation bars. AppCompat reads the unprefixed windowActionBar and
windowNoTitle attributes, not the android: ones, so the theme now uses the
NoActionBar parent, and the content pads itself by the system bar and
display cutout insets -- which land on the short edges in landscape, exactly
where the buttons are.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NDK r30's meta/platforms.json puts its floor at API 21, and meta/abis.json
lists min_os_version 21 for all four ABIs, so nothing here needs API 30.
* ndkVersion 27.2.12479018 -> 30.0.15729638 (r30-beta2)
* minSdk 30 -> 21, ANDROID_PLATFORM android-30 -> android-21
* restore the x86 ABI, which was only dropped because minSdk 30 made it dead
* re-guard layoutInDisplayCutoutMode behind SDK_INT >= P; that field does not
exist before Android 9 and the guard was only safe to drop at minSdk 30
* drop the unused recyclerview dependency (the mod list is a RadioGroup)
Note that minSdk < 23 makes AGP fall back to legacy packaging, so native
libs are compressed in the APK and extracted on install: smaller download,
larger installed footprint. r30 still emits 16 KB-aligned LOAD segments,
which targetSdk 36 needs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On-screen controls
* port/src/touch.c holds a virtual pad written by the Android UI thread and
read by inputReadController() on the SDL thread, so state is kept in
atomics and sticks in fixed point.
* inputReadController() folds it into player 1 before the gamepad section,
so a physical pad still fills in whatever the overlay is not deflecting.
* TouchControls is a transparent full-screen view above SDLSurface. It only
consumes a pointer that lands on a control and returns everything else to
the view below, so SDL still sees taps as mouse input in menus.
* Buttons are hit-tested ahead of the look area, so a press near a button
never spins the camera.
* Layout is normalised to the viewport and editable in place: tap the toggle
to hide/show, hold it to drag and pinch controls around. Saved per device.
* The overlay hides itself, and stops consuming touches, while a physical
controller is attached.
Mods
* Mods live in <externalFilesDir>/mods/<name>/ and the selected one is passed
as --moddir, which the port overlays on the ROM's files.
* The launcher imports a mod from a .zip, guarding against Zip Slip and
absurd unpacked sizes, and hoists a single wrapper folder if there is one.
Netplay
* INTERNET/ACCESS_NETWORK_STATE for enet.
* Keep the in-game console on Android: it is how netplay reports and chats,
and SDL raises the soft keyboard when the game asks for text.
* Predictive back is disabled for the game activity, since SDL routes Back
through the legacy onBackPressed() path.
Also removes GameView.java, which drove a GLSurfaceView path the port never
used, along with the no-op JNI stubs it called into.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android:
* compileSdk/targetSdk 34 -> 36, minSdk 21 -> 30, ANDROID_PLATFORM android-30
* pin ndkVersion 27.2.12479018, AGP 8.1.2 -> 9.2.1, Gradle 8.0 -> 9.4.1
(Gradle 9.x is what runs on the JDK 25 that Android Studio now ships)
* Java 8 -> 17, javac encoding pinned to UTF-8
* drop the x86 ABI: no 32-bit x86 devices exist at minSdk 30, and it was
a third of the native build time
* refresh AndroidX dependencies
Fixes found while getting the above to build:
* AssetUtils invoked a bare `python` on Windows and Android, which does not
exist on distros that only ship `python3`. Use the interpreter CMake finds,
on every platform.
* os_libc.h re-declared bcopy/bcmp/bzero, which clashes with bionic's
fortified overloadable versions; include <strings.h> on Android instead,
the same way OSX is already special-cased.
* libultra.c defined those same wrappers, which bionic already provides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings upstream netplay (enet, port/src/net/*, netmenu) onto the branch
carrying the Android port.
Conflict resolutions:
* system.c: sysLogPrintf levels now carry LOGFLAG_ bits, so mask them off
before indexing prefix[]. The in-game console is kept on Android too,
since netplay reports through it.
* main.c: keep conInit()/netInit() alongside the Android boot logging.
* menutick.c / constants.h: combine both sides.
Also fixes two build breaks found on the way:
* main.c called SDL_Init/SDL_GL_SetAttribute outside #ifdef ANDROID while
SDL.h was only included for Android, breaking every desktop build.
* net/netmsg.c used strcasecmp without <strings.h>.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>