Skip to content

Feat/arcade - #5

Closed
martinellimarco wants to merge 126 commits into
XargonWan:feat/arcadefrom
martinellimarco:feat/arcade
Closed

martinellimarco wants to merge 126 commits into
XargonWan:feat/arcadefrom
martinellimarco:feat/arcade

Conversation

@martinellimarco

Copy link
Copy Markdown
Collaborator

No description provided.

mame, fbneo and hbmame are now one system, `arcade` (SystemIds.MERGED, their folders its aliases, nothing renamed on disk). neogeo, naomi, atomiswave and the rest stay systems of their own. Saves stay per core; canonical() is the comparison everywhere.
… fills

Hold on a machine that asks for it (HoldToGrab, a pure state machine with a short aim grace) until the ring fills, and it comes into the hand; the reticle draws the ring on desktop and HoldRing in VR. Machines that do not ask (grab_hold_seconds 0) grab as before. function_pickup.gd is a LOCAL PATCH of the godot-xr-tools fork.
A notice can stand apart from the machine's top and face the way its screen does (toast_anchor, a Transform3D), instead of billboarding at a fixed spot. Used by the arcade cabinet, whose monitor is tilted; other machines keep the old placement.
The options panel gets a core picker on the System tab: Default (...) plus every installed core that serves the machine. Picking one powers the machine off, clears its options and saves the choice with the room (core_name).
A ROM is a JAMMA board: a bare PCB with a 56-contact comb (28 + 28), a polarising slot, a few chips and a Label sticker, 200 x 300 x 35 mm, never a cartridge shell. The sticker takes the cover (the front of a laid-flat box) or the label, and never the cabinet render the scraper stored; ScreenScraper's support-2D is taken for arcade and support-texture ignored. ArcadeProfiles holds the bay kinds and the board art rules, mod-overlayable. The scene is baked by Tools/gen/gen_arcade_board.gd.
…boards

Issue XargonWan#1. The cabinet is the RetroSystem and the board is its media (row arcade_cabinet_generic, persisted and final). Its parts are ArcadeModules found by TYPE (screen, stick_buttons, coin, bay, sockets; contract version 1, additive only), so a 4P, cocktail or tate cabinet is a scene and a trackball or wheel a new module.

Screen: shape from Libretro.GetVideoGeometry(), never the texture size; tilted up behind a glass pane. Each player's panel is ONE controller to attach to: aimed at, the whole panel lights, a press attaches you, and the keyboard (captured on attach), the VR controllers or the pad play that player through netplay_route; a controller in a front socket owns its port. Coin door: a mesh tween that says coin_inserted when it lands, the coin bit being SELECT. Lower door on a VRHinge gating the JAMMA bay, power switch beside the socket. Floor spawn, movable, see-through while carried, options panel at its right at eye height.

Baked by Tools/gen/gen_arcade_cabinet.gd. arcade_tests (252 checks), the cabinet and grab probes, docs/dev/arcade.md. A headset, a real game, a Quest and two peers are OWED.
…its inside

A real upright has no pane in front of its picture: the glass is the front
edge of the cabinet, which leans back, and the tube stands well behind it.
The side profile now runs along that edge, the monitor leans about 27
degrees, the tube is set 7 cm back, and the marquee overhang is closed
underneath. The probe gains a side shot.
… lean

Three cabinet checks: the glass lies on the silhouette's slanted edge, the
picture sits more than 5 cm behind it, and the lean is between 20 and 35
degrees. The arcade_tests baseline goes 252 -> 255, and arcade.md describes
the new monitor.
Research on the arcade work kept asking before every search, and the docs
the project reads live on github.com.
The Famicom's pads laid their hardwired cord by hand in RetroController. The
arcade cabinet's guns need the same two things (the cord's length set to the
machine's, and a first lay that follows a route authored for where the device
rests), so both move into CaptiveCord. The pads behave as before.
…DICE get theirs

RomM files Neo Geo games under neogeomvs and neogeoaes; ES-DE keeps one neogeo
folder. The cartridges differ in plastic and connector, but the game data is
the same and the cores take one romset for either, so both map to neogeo.

Daphne (laserdisc) and DICE (discrete logic) are their own systems with their
own SystemInfo, so their cabinets have a system to belong to.
A gun cabinet has two guns that hang in holsters beside the cabinet, each on a
hardwired cord that goes into it. A gun is captive: built with the machine,
kept out of "spawned" so a save cannot bring back a second one, and put back
in its holster when let go (LightGun._return_to_well). Its cord is laid along
an authored route, not the straight line between two anchors, which packed the
cord into arches.

The cabinet's START panel is momentary: it is read while pressed, and the
gun's own START and coin reach the core as joypad START and SELECT too, which
is where MAME and FinalBurn read a credit in a light-gun game.

MAME ignores a gun on its port unless mame_lightgun_mode says lightgun, so that
option is pinned for the run while a gun is in a port (ForcedCoreOptions
.lightgun). FinalBurn needs nothing.

Aiming falls back to the cabinet's own tube when no set is connected.
A Neo Geo MVS game is a plastic cassette with the board's edge standing out of
it (gen_neogeo_cart), in the Neo Geo bay of an arcade cabinet
(arcade_cabinet_neogeo). The home console is an AES-style unit with its own
ports (gen_neogeo_console, NeoGeoConsoleModel), and an MVS built into a
console takes the same cassettes as the cabinet does.

The cassette's shell size is an estimate. The edge connector (240 contacts,
about 152 mm) is documented, so the shell is sized to the connector, not
measured.

neogeo_console_probe is windowed: the console's ports and the cassette seat.
Two more cabinets in the bay family: a Primitive Laserdisc Cabinet (Daphne's
stick and sword button), a laserdisc gun cabinet with the guns in holsters,
and a Primitive DICE Cabinet with a paddle dial per player. Daphne and DICE
boards are bare boards in their bays (their discs and shells are not
modelled), and the laserdisc gun cabinet shares the light gun's holster rig.
The vendored MAME entry declares no firmware, so the BIOS and device ROM sets
MAME looks for (neogeo.zip, qsound.zip, ...) were invisible to RomM's
download. This overlay is a whole copy of the vendored entry with one
addition. It lives in its own directory because libretro-core-info is
refreshed wholesale, and an edit there would be lost silently.
…cabinet

The generic arcade cabinet is "Primitive Arcade Cabinet" now, and the
variants sit beside it as their own model rows (light gun, Neo Geo AES,
laserdisc, laserdisc gun, DICE). The ids are persisted, so the generic one
stays the arcade tile's default and keeps its id.

ArcadeProfiles gains the bay kinds these need (mvs, laserdisc, discrete).
A bay row with romsets: false is a cabinet, not the MAME romset family, so the
generic cabinet does not list the Neo Geo cores. is_cabinet_system covers
every cabinet; is_arcade_system stays the romset family. Board art and the
missing-BIOS notice follow the wider one.

A cabinet with no keyboard map of its own plays from MAME's layout (the six
buttons on the bottom row, START on 1, the coin on 5) and not the numpad, so a
keyboard without a keypad can reach it. It never reaches a console.
…ial and pedal

A cabinet is picked up and carried like any machine, and it comes back to
stand: a heavy base (its centre of mass low), and while nobody holds it a
gentle controller turns it upright about the horizontal, keeping its heading.
The old yaw-only axis lock left it at whatever tilt it was let go at. A
cabinet spawns in the hand that asked for it, not set on the floor a stride
away.

Its audio comes from its own two speakers, low in the front either side of
the coin door, when no TV is connected, rather than from the middle of the
body. SystemAudio places the voices there.

The shape is set by the scene (cabinet_width, cabinet_height, the marquee and
the glass), so a wider shooter sets its own: collision, pointer boxes and the
options panel follow those numbers. gen_arcade_cabinet bakes it.

A panel can carry a dial (LEFT and RIGHT turn it, for DICE's paddles), and a
gun cabinet's foot pedal is drawn, not active (ArcadePedalModule).
… counts it

arcade_variants_tests covers the cabinet variants: their rows, the bays they
take, the holster and cord numbers, and the captive gun wiring. The generic
cabinet's suite and the two probes are updated to the same code.

test_baseline.json records arcade_tests 255 -> 266 and the new suite at 301.
Those counts are the working copy's, not a run: this checkout has no built
GDExtensions, so the suites cannot run here (they fail to parse Libretro).
arcade.md covers the Primitive cabinet and its variants, the holsters and the
captive cord, and the bays. CLAUDE.md's index row for arcade (2z) names the
variants and the cabinet parts. modding.md shows how a mod registers a cabinet
model. testing.md is updated with the suite change.
…a cabinet

Four cabinets spawned lagged, and one game lagged after minutes at its first
boss. A cabinet is about a hundred and ninety parts, and every shadow-casting
light in the room draws each of them again, an omni light up to six times. The
buttons, the stick, the terminals and a board's chips are most of the casters,
and their shadows are a few pixels wide.

ArcadeShadowBudget takes the shadow off parts under 10 cm when a cabinet, a bay
or a board is built (a board's own mesh goes through cartridge.gd).

The measuring tools: arcade_scale_probe (the cost of N cabinets, scenes only or
with a core running), arcade_soak_probe (minutes of play, one CSV row every 5
seconds, and which series grow), arcade_mesh_census (every mesh with its
triangles and shadow mode), and run_arcade_matrix.py, which runs the scale
probe once per configuration in its own Godot process, since a core is never
looped inside one. performance.md records what was measured and kept.
…hanged

The cabinet's input was pushed to the core every frame whether or not a button
had moved, and a stick's basis was rewritten every frame, including after it
had settled (its lerp never reaches its target exactly). A `position =` dirties
the transform even when it writes the value it holds.

ArcadeInput keeps what the core last heard per port and skips an unchanged
state, and the stick and the panel's lights stop once they settle. Cleared when
the machine goes off, so a restart is told afresh.
The cabinet's compartment has a block with three phono sockets (video yellow,
left white, right red) on top, facing up, so a hand leaning into the open
compartment can plug a console straight in. The block belongs to the cabinet,
not to a bay: whichever bay is in the mount, the sockets stay where they are.

A console cabled to it plays on the cabinet while the bay holds no unit. The
module is found by type, as the cabinet's other parts are (KIND_AV_IN).
FinalBurn Neo starts the most games the moment they are dropped in, with the
least romset fuss: its sets are self-contained and the QSound DSP it needs is
built in. MAME runs more sets but is picky about its romset version and its
device ROMs (a CPS-2 game wants qsound.zip), so it stays one pick away on the
System tab.

FinalBurn Neo is also the only core vetted for netplay rollback, which
NetplayCores already chooses by itself. Android keeps mame2003_plus, as before.
…to it

The bay (the cartridge slot, the board's seat, the JAMMA socket) used to be
baked into the cabinet, so the cabinet and what it played were one thing. The
bay is an ArcadeBayUnit now: one scene per kind (JAMMA, Neo Geo MVS,
laserdisc, DICE), a pickable that stands in the room, and it carries the board
standing in it. Saved as its own entry, with the board as a reference.

The cabinet stays the machine: the core, the input, the picture, the saves and
the netplay identity. Only two things follow the bay (RetroSystem.link_bay).
WHERE the board stands: the cabinet's cartridge slot becomes the bay's, and its
own is shut and kept as home. WHICH system it plays: the bay's kind. So a
generic cabinet with an MVS bay plays Neo Geo and refuses a JAMMA board.
Taking the bay away stops the game and the board stays in the bay.

A bay joins a cabinet two ways, never both. Docked: the cabinet's BayMount
snap zone holds it rigid, so moving the cabinet moves it, and the door gates
the docked bay's slot. Cabled: a JAMMA lead (jamma_cable, jamma_port,
connector jamma_plug) joins the cabinet's socket to a bay's. A fresh cabinet
starts with its own kind of bay in the mount; a restore never does, since the
saved room is the whole truth about what is in there.

A cabled machine's picture is the cabinet's screen, so its shape comes from the
machine's geometry and not the board's (ArcadeScreenModule).

Spawn cards offer every bay and the JAMMA lead on every cabinet, since a bay
clipped into another kind's cabinet is what makes it play that system. The
cabinet card no longer offers a "default" item beside its bodies.
Loads a saved slot through the same async restore the loading overlay waits
on, and reports how far it gets, which is where "RESTORING OBJECTS n / m"
would stop.
…ow it

arcade_bay_tests and arcade_bay_probe cover the bay as its own object. The
cabinet suites and the variants suite are updated to the new bay, and
arcade.md, testing.md and CLAUDE.md's arcade row describe it.

Not run here: the GDExtensions are not built in this checkout, so the suites
cannot run. Tools/test_baseline.json is not updated in this commit.
Found with the Quest 3 (logcat and play), fixed on desktop with the arcade suites.

Light guns and MAME
- Current MAME on Android is `mamearcade`: it gets the gun option too, as do
  `mamemess` and the 2003/2010 builds. `mame2003` has no gun value; it is pinned
  to `pointer` and the gun is also reported as a pointer (GUN_AS_POINTER).
- START and SELECT from a gun in the hand are sent as the joypad's too, because
  MAME 0.289 reads a start and a credit off the joypad only.
- Aim is measured on the picture, not the glass, under a pillarboxed fit.
- A captive gun is held while the button is held: let go, it falls. It goes
  into its holster only when let go at the holster.
- `arcade_load_probe --options` prints the gun options a core really has and
  what the run holds.

Cabinet transparency
- A cabinet's whole model, docked bay and board, and the guns in their
  holsters, go see-through while carried, on Forward+ and mobile alike.
- The Quest's mobile renderer ignores per-instance transparency, and
  ALPHA_HASH copies did not show, so the held look is one shared shader that
  discards pixels in a screen-space pattern, with shadows off while held.
- A cabinet carried on the laser ray (no pick_up, no signal) counts as held.
  It stays see-through while it moves and for 0.6 s after it stops.

Notices
- A notice grows with its message (up to 900 px), keeping its bottom edge, and
  stays up longer when it has more to read. A refused game's full core message
  is no longer cut off.

Audio
- SystemAudio logs a health line 4 s and 14 s after a bind (sink fill, brake,
  mixer underruns, active voices, gains).

Also: the vendored libretro-core-info overlay gains fbalpha_libretro.info (the
last full FB Alpha, published for Android, missing from libretro's core-info
set), and the duplicate cable_instance() in light_gun.gd (a rebase residue
that broke LightGun's parse) is gone.

docs/dev/arcade.md has the measured option table and the open items.
When a board (or MVS cassette) goes into the bay the cabinet dresses itself in
the game's scraped art; pulling it undresses it.

- Marquee: the wheel, else the cover, else the title as before. Lit with the
  machine like the marquee was.
- Sides: the cover fills the whole flank (the side's own silhouette, UV'd over
  its bounds) and is cropped to it, never stretched.
- Cover = the front of the scraped box, else the scraped label, else a RomM
  cover the spawn menu already cached. Nothing new is fetched or scraped.
- `MediaDimensions.load_media_image` now looks in every ROM folder of the
  system: arcade art scraped from the `mame` tile lives in `mame/media` while
  the primary `arcade/` is empty.
- Hooked into `RetroSystem._on_cartridge_inserted/_removed`, so drops, saved
  room restores and re-docks all reach it. Materials are StandardMaterial3D, so
  a carried cabinet dithers the art with the rest.
- System tab "Cabinet Art" (cabinets only, on by default): off leaves it naked.
  Saved in the room only when off.
- The composite input's legend is smaller, so it stays inside its yellow plate.

arcade_variants_tests art/ (138 checks over the six variants); the probe's
artfront/artright/artleft/avin shots (windowed) show it.
The cabinet's menu gets a DIP Switch tab (cabinets only), by core:

- MAME has a DIP Switches screen on the game's own screen: the tab's button
  taps Tab, Down, Return into the core (through the netplay key schedule, like
  a keyboard) and, once open, Tab to shut it. Measured with the new
  Tools/cores/arcade_dip_probe: Tab opens MAME's menu, the sequence lands on
  the DIP screen.
- FinalBurn Neo has no such screen (Tab, F2, F3 do nothing visible); its DIPs
  are core options, so the tab lists exactly those, stepped like the Options
  tab.
- A core with no DIPs, or a cabinet that is off, gets a one-line note.

arcade_tests dip/ covers both forms and the key sequence; it also pins the
composite input's legend fitting its plate and the reset button staying hidden.
Tools/models/dip_tab_probe photographs the tab (windowed). docs/dev/arcade.md
records the art and DIP decisions.
martinellimarco and others added 25 commits October 26, 2026 22:15
… deck, pop'n's domes, a guitar, a drum kit, a Taiko drum, Mambo's congas

The `music` form was an upright with a stick and six buttons whatever the game. It now builds a controller of its own for six instruments (the table's `i`:
ideck ipopn iguitar idrums itaiko iconga; IIDX, beatmania III and Keyboardmania stay the generic panel). ArcadeInstruments holds the layouts (where each part
stands and which of MAME's buttons it is, read from djmain.cpp, ksys573.cpp and namcos10.cpp), the builder draws them, and a part keeps the name the control
module finds it by (Btn1..Btn10, DialPivot, Start), so the input, the panel's grip and a headset's fingertips need nothing of their own:

- beatmania: a deck a player, a turntable on the outside and five keys (three white in front, two black behind); the second deck is the first in a mirror.
- pop'n music: nine domes in two rows, white yellow green blue red blue green yellow white.
- GuitarFreaks: a guitar a player with three frets, a pick bar, a wail button and the effect knob; DrumMania: five pads in an arc and the bass drum's pedal on
  the floor, which goes down; Taiko: a drum a player with four zones (sectors of a ring, their own finger zones); Mambo a Go-Go: three drums of three pads.
- Ten buttons on a panel (BUTTON_COUNT): MAME's buttons 7 to 10 are the pad's L2, R2, L3, R3 (an estimate) and the keys V, B, N, M.
- A cap may press its own way (`press_offset`), a fingertip zone may be a sector (`sector_reading`), a turntable is taken anywhere on its platter.
- Every panel's reach includes its START, so the body is wide enough and the panel's grip covers all of it (the suite found a drum pad and Taiko's START outside it).

arcade_form_tests music/ (every set's instrument, panel counts and deck depth, mirrored decks, part names, MAME buttons, nothing hidden, the grip, START clear of
every part, sector meshes facing out, a finger in a Taiko zone through the hands' own pokes, the platter's reach, the bass pedal on the floor going down, the
colours of the nine domes) is mutation-tested line by line.
…s and a collar on its shaft turns

147 romsets (56 families) have a joystick AND a dial in MAME (Ikari Warriors, Victory Road, Heavy Barrel, Bermuda Triangle, Gondomania, Tron, Top Gunner's
bootleg, "Rotary Joystick" in its own description), and panel_kind took the dial and stopped: a spinner and no stick. The panel is now `rotary`: a stick as any
other with a collar of light metal on its shaft (`DialPivot` on the stick's pivot, a red mark on its top), turned by LEFT and RIGHT as every dial is and taken by
a hand below the ball. A dial with a paddle is still a driving game's wheel, with a trackball a trackball, with a pedal a wheel.

Arkanoid Returns is the one Taito F3 game that reads a paddle and the F3 board declares a joystick for its menus: it would have gained a stick beside its
spinner, so the generator drops the controls a cabinet never had (UNUSED, by family) and keeps the four buttons the joystick carried.

The deck probe gets a `formdeck_` close-up of the control deck from above and in front of the player.

arcade_form_tests rotary/ (11 checks) and the audit case are mutation-tested, the generator's too.
…s that turn

G-LOC R360, its Japanese set and Wing War R360 were the canopy cockpit of every other flight game. Sega's R-360 was a sphere cockpit hung in a two-axis
gimbal on a ring that turns a full circle (two research summaries agree). The three sets (a per-machine RING list in cockpits.py, since each is a clone with its
own cabinet) now carry the table's `r` where the cabin's `c` was, and the cockpit builds two hoops outside each side of the platform on an axle through the
seat's middle and a hoop behind the seat, each with four hazard-yellow brackets so the turn shows.

They are open frames (no collision, no pointer box, nothing in the picture's line of sight) and they turn with the base's motion, eased: a pull turns the outer
ring about the axle, a push to the side the rear one about the view's axis; the seat and the eyes do not turn with them, so a headset is never spun round, and
with Cabinet Motion off they stay level. G-LOC's own cockpit and the plain Wing War keep their cabin.

arcade_form_tests ring/ (14 checks) is mutation-tested line by line, the generator's inputs too (22 breakages, all red); the hood group's sight line takes the
pod as well. The old seating and audit cases that expected the cabin on the two clones now expect the rings.
… their three screens

Research (BEEP! 4/87 through a Gigazine summary, one chain, medium-low): Taito's three-screen cabinet had a bench-style seat with Body Sonic built in. The
table's `q` now reaches a plain upright (Darius, Darius II and their clones, the two-screen build included), the spec asks for a bench with a seat a player,
and the bench builder takes the seat's distance from the spec: an upright's bench stands 0.98 m from the front, not the gun ride's 1.16, so the seated player
reaches the sticks and the three pictures fill the view from 0.6 m. It is furniture: a player may stand. Ninja Warriors (same cabinet design, listings say
standing), Darius Gaiden and every other upright stay as they were.

arcade_form_tests seating/ gains the table, the spec, the built bench (reach, picture distance, platform under the seats, nothing of the deck in the line of
sight) and a case that no other upright carries a seat; mutation-tested, the generator's inputs too.
…flight yoke, Bass Fishing a rod with its reel

Four games were drawn with a ball on a stick (or sent to the `odd` pile, which builds an upright anyway). Their real controllers are not that, so the table
now has an attribute for it: `y<style>` (`STYLE` in Tools/arcade/gen_game_index.py, by parent romset; `k` stays retired, it was a boot time and arcade_tests
asserts no line carries it), read by `ArcadeGameProfile.control_style()`. `yyoke` is Atari's yoke (Star Wars, The Empire Strikes Back and Red Baron, clones
included), `yrod` Sega's Bass Fishing (and its clones). Nothing else changes: the games stay uprights with one panel. The fishing game's paddle and pedal
would have made it a stand-up driving cabinet, which it is not, so a style wins over the kinds the controls would pick.

- Specs: `panel_kind` returns "yoke"/"rod" for a styled game, a yoke's panel is wider than a stick's (YOKE_IN, YOKE_FIRST_COLUMN, YOKE_REACH) so its handgrips
  stand inside it and the caps clear of them, and the extent counts the grips' reach.
- Builder: `_yoke_head` is a column, a box hub, a bar and a handgrip each end (a quarter of a metre apart) with a trigger on the front of each; `_rod_head` is a
  cork grip, half a metre of blank rising toward the angler, a reel and a crank on a mount of its own (the control module sets `DialPivot` directly, so the
  crank sits under a mount that turns its axle to the reel's). The part a hand takes is still `StickBall` and the dial still `DialPivot`, so the input, the
  panel's grip and a headset's fingertips needed nothing new: RIGHT leans the yoke, swings the rod and winds the reel.
- Docs: arcade-cabinets.md 2e (what is built, what is an estimate), the two games out of the not-built rows; testing.md.

arcade_form_tests styles/ (14 checks: which romsets carry a style and which do not, uprights with one panel, the panel's width and where the grips and caps
stand, the yoke's parts and their spacing, caps clear of the grips, leaning and the hand's grab at the hub with no collar, the rod's grip/blank/reel/crank,
its slope and the reel's axle, the swing and the winding, the two grabs a few centimetres apart, a street fighter's stick keeping its ball) and the layout
group for the new kinds are mutation-tested line by line, the generator's too (the table regenerated with the mutant each time). arcade_form_tests 743.
No core, ROM or headset was involved: every size and the way the real controllers are held are estimates from search summaries, not measurements.
…uper G are a rider standing on a rocking board

Five games where the player STANDS on the machine's own moving platform were drawn as an upright with a stick (Top Skater, Alpine Surfer and Alpine Racer were `odd`,
Ski Super G a plain upright). The table now has a form for the vehicle: `tskate` (Top Skater's skateboard), `tsnow` (Alpine Surfer's snowboard) and `tski` (Alpine Racer,
Alpine Racer 2 and Ski Super G's skis), from `BOARD` in Tools/arcade/gen_game_index.py by parent romset; `ArcadeGameProfile.is_board()` says whether a game has one. MAME
counts two riders for the Alpine games (two linked machines): `PLAYERS` makes it one rider a machine, as it makes Darius Gaiden two.

One rig for all three (`ArcadeCabinetBuilder._build_board`), built on the WIDE body's big screen as the bicycle is:

- a stand 0.86 x 1.30 m and 8 cm high, 0.84 m out (clear of the bay door's swing), and on it a plate that rocks on a lengthwise axle 0.20 m up (`LeanPivot`, which the control module
  leans with the steering as a bike's; the axle is high enough that the plate's low edge clears the stand at full lock);
- on the plate the vehicle: a skateboard (deck, a kicktail each end, two trucks, four wheels), a snowboard (its ends turned up, a binding and a strap a foot) or two skis (tips turned up, a binding each);
- a rail the rider holds, 1 m above the floor: four posts, a side rail each side with a grip sleeve on it (`BarGripL/R`) and a crossbar across the front with START on top of its middle,
  all part of what leans, so the control module and the hands find it by name as they find a bike's. No seat (a person stands), no collision, and the pointer box stops under the rail.

The hands take a grip with the trigger; sideways 15 cm is a full lean. **The bars' middle (what the let-go distance is measured from) is the middle of the two grips**: put at the
crossbar, a hand at the end of its throw is 0.575 m from it, past the 0.55 m limit, and lets go of the rail at full lock; both tests catch that. Every size is an estimate from search summaries (nothing
was opened, no photograph seen); what is not drawn is said in arcade-cabinets.md 2f: the real mechanisms (Alpine's footplates swivel and slide, Top Skater tilts about three axes: only the
roll is drawn), the second axis MAME gives these games ("Steps Edge" is player 2's port, "Slide") which is not sent, the buttons, Alpine's poles, Alpine Surfer's separate monitor. Ski Champ, Sega
Water Ski and Surf Planet stay uprights: nothing found says what their riders stood on.

arcade_form_tests rider/ (46 checks) and arcade_hand_tests rider/ (21), arcade_tests one more, and a stand/rail/lean shot in Tools/models/arcade_cabinet_probe. The form's suite found the snowboard's tips
hanging past the stand while it was written (the board is shorter now). About fifty mutations of the builder, the spec and the profile were run; four survived the first round (the bars' middle at
the crossbar with the grips behind it, and the upturned ends tipped the other way, which a box's extent cannot tell: it is the same round its middle, so the test now compares the two ends of each piece) and none
survives the second. arcade_form_tests 789, arcade_hand_tests 222, arcade_tests 452; the seven arcade suites pass.
…t of a stick each and a 50 inch class screen

`swa` (and `swaj`) were the one game of the 143 the audit had left unchanged: two analog sticks and a throttle read as a pedal in MAME, drawn as a plain upright
with a stick a player. What the research says of Sega's Model 1 game (two listings, medium) is that it seats TWO, a pilot and a gunner, each with an analog joystick
that has a trigger and a thumb button, and that only the pilot's seat has a throttle lever; its monitor was a 50 inch rear projection housed separately (one listing,
low). What the seats were (stools, a cockpit, a bench) and where the stick stood were not found.

It is built the way the other upright you sit at is (Darius): `qs` in the table (cockpits.py SEATING) gives it the bench of two seats a hand's reach from the deck, and
`m50` (MONITOR_CLASS) the biggest picture an upright's body holds, the tall body 1.08 m wide. The two sticks stay on the deck with their three buttons each. Not drawn, and said
so in arcade-cabinets.md (2b): the pilot's throttle (MAME gives the game a PEDAL, which no upright panel draws) and a stick between the knees.

arcade_form_tests seating/ takes it (the uprights you sat at are Darius's and this one, nothing else; Atari's Star Wars stays standing; the spec is a bench of two seats,
two stick panels, the tall body and 50 inch screen): 790 checks. The seven arcade suites pass.
…'s verdict on every one of the 143 names

World Combat / Warzaid (Konami Viper, 2002) is one optical light gun a player and one trigger each (MAME's viper.cpp; a player takes cover by aiming off the screen, there is no
pedal). MAME lists its 4P sets as two buttons only because their gun axes are an undefined TODO there, and files just the 2P set (`wcombatu`) as a gun game, so the table had
a clone as a gun cabinet and its parent and two clones as a plain upright. `UNTYPED_GUNS` (Tools/arcade/gen_game_index.py) names the parent: all four are two guns in holsters, no
pedal. arcade_form_tests audit/ takes it (and arcade_tests' roster of shooters two more); the entry was broken three ways (gone, one gun, mounted guns) and each turned the case red.

docs/dev/arcade-cabinets.md section 9 is finished:

- 9.3 says what the audit made worth building, game by game, with the section of each;
- 9.4 is the verdict on each of the 143 names in the order they were listed: MAME's line (description, year, maker, driver status), what spawns now (read from the real spec by a probe run
  on the committed table, not typed) and a verdict with the note behind it. 87 are built as the research and MAME describe them, 28 are plain uprights because the real cabinet was one, 26 are
  approximate (the note says what is missing: a second control beside the first, a twin seat, a throttle, a keyboard, a yoke, a sit-down version, or nothing found on the cabinet), 2 are not
  MAME romsets (`lamborcc`, `jubeat`). `interstellar` is `istellar` (Funai/Gakken's laserdisc game, not "no MAME" as 9 said), found by a research pass, which also gave Kick Goal (an
  8-way stick and three buttons a player: an upright) and the Lamborghini names (home games). Nothing was run against a core or a ROM and no photograph of a real cabinet could be opened:
  the table is what is built, not what was played.
- CLAUDE.md's index row for the arcade cabinet says what exists now (it still called every `music` game an upright).

The full runner (Tools/run_tests.py, 83 suites) passes 77 and times out or fails six that fail the same way at 48d8483, before this audit began: netplay_tests, object_sync_tests, romm_tests
and mod_browser_tests need peers or a server a sandbox does not give them, and scene_tests and combo_tv_tests do not exit here (combo_tv_tests prints its PASS and then hangs, at 48d8483 too). arcade_form_tests 791.
…abinet

"Primitive" is what the spawn menu calls a stand-in model, and this cabinet stopped being one when it began taking the shape of the game in its bay (a panel
a player, a tate body, guns in holsters, a cockpit, a stage). Only the label changes: the registry id `arcade_cabinet_generic` is persisted in saved rooms and on the netplay wire and
stays. It is still the first, and only, cabinet row of the `arcade` platform, which is what makes it that tile's default; the Neo Geo, Laserdisc and DICE tiles keep their own cabinets.

The name is changed in the registry, the model's and the tests' headers, the probe, docs/dev/arcade.md, docs/modding.md and CLAUDE.md's arcade row.
…56/573, NAOMI and every other arcade board

The Arcade page of the BIOS section showed fifteen sets typed by hand: System 246, 256 and 573 (Taiko no Tatsujin 7 on, Tekken 4, DDR, GuitarFreaks), the whole of NAOMI and Atomiswave,
Chihiro, Triforce, Lindbergh, Konami's Viper, Python, GX, GV and Twinkle, every Capcom, Sony and Taito ZN-family board, Hikaru, HNG64, PlayChoice-10, the Nintendo Super System,
Mega-Tech and Mega Play, Atari System 1... were not there, so a player with those games had nothing to find on RomM by name.

Tools/arcade/gen_mame_bios.py reads MAME's -listxml (0.289) and rewrites the firmware block of libretro-core-info-retroxr/mame_libretro.info:

- a BIOS is every `isbios` machine a coin-taking game names as its romof, through its parent too, plus the arcade boards whose drivers take no coin yet (System 246/256, Lindbergh, Triforce,
  Python, System Y2, ALG 3DO: ALLOW); a console, a toy or a PC sharing a driver file with one is EXCLUDEd with its reason;
- a device set is a device with ROMs of its own that at least 20 coin-taking games use (QSound, the Namco I/O MCUs, Sega's JVS/MIE boards, the YM2413 patch ROM...), under the name of
  the device it is merged with (`qsound_hle` is `qsound.zip`); the long tail is a computer's keyboard or a disk drive.

101 sets: the original fifteen first with their words, 62 BIOS by the games that want them, 24 device sets. No game title is typed from memory: a well-known board's family is hand-written
but its games are MAME shortnames the script resolves to MAME's own description (a shortname that is not one of that BIOS's games stops the run); every other entry gets MAME's description,
its maker and three well-known games.

FinalBurn Neo's list was checked against its drivers (BDF_BOARDROM and the board ROM field of every BurnDriver in src/burn/drv): complete but for atarisy1.zip, which 27 games name; it is in the
overlay now. Its console BIOS (gba, astrocde, the SNES DSPs) stay out: no tile plays them with this core.

arcade_tests profiles/ takes it (497 cases, 45 new): no file twice, every entry optional and described by its own path, the three the player named and twenty more by name, at least 90 sets,
FinalBurn Neo's atarisy1.zip and the fourteen it had. Broken five ways (the old list, a duplicate, sys246 gone, FinalBurn's entry gone, one required) and each turned a case red.
docs/dev/arcade.md says how the list is made and what is left out.
… stick no longer hides the coin slot

After Burner II's stick had a pointer box 0.32 m across at its head, and a green plate that big. A laser aimed from the seat, or from behind it, at a coin slot 0.8 m in front met that box
first (a ray hits the nearest pointable), so nobody could put a coin in. Reproduced first: arcade_form_tests grip/ aims from behind the controls at chest height at every coin slot and RETURN of
After Burner II, G-LOC, Daytona, Scud Race and Lucky & Wild, and none reached its slot (0 of 2 on the cockpits; the wheel's disc and the stick's box were the blockers).

- the flight stick's grip is its own head and grip (STICK_GRIP_BOX 0.10 x 0.24 x 0.10), plate included;
- a racing cockpit's wheel is a RING: twelve boxes along the rim, a hub and three spokes for the pointer (the hole stays open to a laser as it is to the eye), an annulus for the glow
  (ArcadePanelGrip.ring_radius / ring_width);
- the bars of a bike, a bicycle and a board's rail are a bar deep (BARS_GRIP_DEPTH 9 cm), not 20 cm of air over the grips.

A deck's panel and the caps are on the panel's plane and are unchanged. arcade_form_tests grip/ (11 checks): the coin slots reachable from behind the cockpit, the stick's box and plate its own size,
the wheel's probes (hole free, rim, top of the rim, hub, spoke hit; outside the rim free), the bars a bar deep. Broken four ways (the 0.32 box back, the wheel back to a disc, the bars 20 cm
deep, no ring glow) and each turned its cases red. The seven arcade suites pass (arcade_form_tests 802). The probe takes a formgrip_<romset> shot with every grip lit.
The platform of a dance cabinet was 2 cm high and sank into any floor
that was not dead flat, leaving the arrows half buried. It is now 10 cm
(STAGE_H) with the nine panels flush on its top: the base may clip at
its foot, the arrows never do.

There are two ways to dance, in a headset or on a desktop:

- Walking on it. Nothing tracks feet, so the body is the head's place
  on the floor (ArcadeInput._body_point). A panel is pressed while that
  point is over its square grown by 2 cm, with the floor under it within
  18 cm below the panel's top to 6 cm above it (ArcadePanelHands.foot_on,
  update_body). The press joins the hands' own (buttons(), active()), so
  it needs no attaching and takes the same netplay route as a fingertip.
  A machine switched off, or no camera, lets every arrow up.
- Standing up on it. Each square carries a StageSeat (a CockpitSeat with
  a trigger over the whole top, an eye point 1.62 m over the platform and
  a step-down point to the side). Aiming at the platform and pressing
  stands the player on it as on a seat, but the eye is not lowered: the
  height changes by the platform's 10 cm and nothing else. While they
  stand there their own controller plays that dancer (the panel is
  attached, so WASD or the arrows, a pad's d-pad or stick and a Quest's
  sticks press the arrows). Stepping down lets the panel go; a machine
  switched on with a dancer already up takes the panel then.

Tests: arcade_form_tests stage/ (29 checks: heights, every arrow above
the platform's top, the seats and their points, attach on sit and let go
on step down, the body over each arrow of both squares, off the square,
on the room's floor, in the air, the machine off, a morph rebuilding the
seats) and arcade_hand_tests zone/ (foot_on's limits). 15 mutants of the
new code, each turned a case red; one survivor (an arrow held by the body
staying down when nobody is there) got its own case. Baseline: form 831,
hand 227. The probe gains formplat_/formstage_ shots.

Docs: arcade-cabinets.md 9.2b, arcade.md (stage, Feet), testing.md.
OWED: a headset (whether the head's place is a believable foot), the
Quest, two peers.
…pe that looks like one

A gun game's extra switch now has a kind (the table's `f<kind>`,
ArcadeGameProfile.pedal_kind):

- cover (Time Crisis, Crisis Zone): unchanged, down while the player is
  out of cover.
- action (Virtua Cop 3): the foot pedal switches the ES mode on, so it is
  UP unless the player steps on it; the same gestures that take cover in
  Time Crisis (the gun hand's A or X, the pedal key, the free hand's
  trigger, ducking) step on it, and its hint says so. It was missing: the
  game was drawn with no pedal at all. Both guns get one.
- sensor (Golgo 13, g13knd, g13jnr): a rifle's proximity switch. MAME names
  it "Sensor" (button 2; "Scope Sensor", button 3, in Juusei no Requiem)
  and comments "if the player is 10cm or closer then this will be on".
  The game's shooting waits for it, which is why the trigger only fired in
  the menus. A box on the deck beside the rifle's post with a lamp
  (ScopeSensor, the pedal module without a plate) is on while the rifle is
  in a hand, lifted by the pedal's gestures, and on whatever the hands do
  while the head is within 10 cm of the eyepiece's lens (13 to let go).
  The pad bit is the MAME button's: Aux A and the pad's A for button 2, Aux B
  and the pad's Y for button 3.

The digital scope of a one-screen rifle game (Golgo 13) read as a flat patch
cut out of the monitor. Its lens now draws what a scope shows: a round
field of view with a vignette and a reticle with mil dots (the shader's
scope_reticle, never on Silent Scope's own second screen), at 3 times
instead of 2.5.

Tests: arcade_tests game/ (kinds and buttons per romset), arcade_form_tests
specs/ and scope/ (the spec carries the sensor, the lens draws the reticle,
Silent Scope's does not), arcade_variants_tests pedal/ (both kinds, the head
at the eyepiece with its hysteresis, a pistol with no eyepiece, button 3).
Mutation-tested: 17 mutants, each turned a case red. Baseline: arcade 500,
form 834, variants 494.

Not built: a desktop view into the scope, the mission select's Up / Down
Select and Enter, a scope that follows the eye. Nothing was run against a
real Golgo 13 ROM.
… really had

Three games were drawn with pistols in holsters and are not that. Every
shape is an estimate (no photograph or manual could be read; the sources
are search summaries), the structure follows MAME and what the research
agrees on.

- Rail Chase and Rail Chase 2: two guns fixed on posts, joystick-like (the
  research calls the controller "an analog stick with a trigger"): a tall
  rounded handle with a ball and a red cap on its top, the trigger and a short
  nose that points where it aims. Nobody draws one from a holster any more.
- Laser Ghost: three guns, one a player, fixed to the cabinet, each a big
  blaster: 40 cm from grip to muzzle (a pistol's 24), a finned barrel, the
  "top-mounted laser imaging unit" the research names, a power cell, a thick
  grip with a fire and a special-weapon cap. Its swivel is 22 cm ahead of the
  grip so the muzzle rests 3 cm short of the glass.
- Gunbuster: MAME has TWO stations, each an 8-way joystick, two buttons and
  a light gun, and a link device (four players are two cabinets linked). Two
  stations are built, each a pistol fixed on a post and a stick with two
  buttons beside it (the table's `j`). A port a gun owns took START and the
  coin only, a gun writing no pad of its own, so the stick would have moved
  nothing: for this game the four directions and the first two buttons reach
  the core on the gun's port too. The linked pair is not built and the
  "four guns" the request names are not in MAME's inputs.

How: the table's `y` on a gun game names the shape (`ystick`, `yblaster`,
GUN_STYLE), the spec's `gun_scene` picks the scene (GUN_SCENES), and
gen_arcade_gun.gd bakes two new variants (--variant=stick|blaster) on the
pistol's own cord boss, grab point and cord so the mount, the wells and the
cord route need nothing of their own. The post's height and the swivel's reach
depend on the shape.

Tests: arcade_form_tests specs/ guns/ seating/ (the shapes, the scene each gun
is built from, every gun fixed, the muzzle clear of the glass, the stick bits),
arcade_variants_tests guns/ (every shape keeps the pistol's cord boss and grab
point), arcade_tests game/ (the table). Mutation-tested: 8 mutants, each turned
a case red. Baseline: arcade 502, form 843, variants 496.
…two buttons

Road Blasters was drawn as a stand-up with a steering wheel and two pedals.
MAME's own port list says otherwise (atarisy1.cpp: one steering axis, two
buttons "Lasers" = BUTTON2 and "Special Weapon" = BUTTON3, no BUTTON1, one
analog pedal) and the research agrees: a yoke in the style of Atari's Star
Wars yoke that only turns, about 270 degrees from lock to lock, with a pair
of triggers and thumb buttons on its grips, one gas pedal, no brake.

- The yoke (the table's `yhelm`): the wheel's column and WheelPivot, so the
  input, the hands and the lock find it as they find a wheel, but a hub with
  a bar across it and a handgrip standing out of each end in place of a rim,
  a trigger on each and a cap on its end. It turns 135 degrees each way
  (`lock_deg` on the pivot, ArcadeControlModule.wheel_lock_deg).
- The hands: a hand takes it by a grip, never by the middle of the bar
  (ArcadePanelHands.near_helm_grip), and the holding hand's A / X is Lasers
  (button 2) and its B / Y Special Weapon (button 3): the buttons that are on
  the grips. A cockpit's pointer shapes are the yoke's hub, bar and a box
  round each grip, no rim.
- The panel has caps for the buttons the game uses only (`u23`,
  ArcadeGameProfile.used_buttons): Btn2 and Btn3, no Btn1.
- One gas pedal, in the middle, and no brake or gear lever.
- MAME's descriptions call three of its sets "cockpit" (roadblstc,
  roadblstc1, roadblstcg): they are the seated cockpit with the same yoke,
  the other eight sets the stand-up.

Estimated: the yoke's size and shape, its lock (sources say about 270 degrees,
one 2003 poster argued 180), that its triggers and thumb switches are the two
buttons, and where the caps stand. The cabinet's own dimensions, panel and
marquee were not found.

Tests: arcade_tests game/, arcade_form_tests helm/ (the spec, the yoke, the one
pedal, the buttons, the pointer shapes; Pole Position and Daytona keep their
wheels), arcade_hand_tests helm/ zone/. Mutation-tested: 12 mutants, each
turned a case red. Baseline: arcade 504, form 855, hand 238.
…s have three angled monitors

The cabinets of three screens were drawn as one flat widescreen glass. TX-1's
side screens are "tilted 30 degrees" (a Wikipedia summary), Buggy Boy's cockpit
has three that form a panorama, and a driving cockpit that wraps its screens
round the driver is the point of having three. All four are a bank now.

- ArcadeCabinetSpecs.bank_layout stands three monitors, each as tall as the tube
  and as wide as its part of MAME's frame is in the frame's shape: the centre one
  square to the glass, a wing each side hinged on the centre one's edge and turned
  in 30 degrees (BANK_YAW_DEG), the centre set back so the outer edges stand on
  the plane a flat picture would. A dark case stands behind each along its own
  normal (the first build pushed a wing's case straight back in the cabinet's
  frame, which put its face in front of the picture: two grey boxes where the
  wings should be, found by looking at the photo).
- ArcadeScreenModule lights the wings (CabinetScreenL / CabinetScreenR) with their
  own third of the frame, only while the core's frame is the shape the table
  expects (the scope's rule, 4%): any other view shows the whole frame on the
  centre monitor and leaves the wings dark, as do attract, a picture from another
  machine and no picture at all. The wings' fit is set when the frame's shape
  changes, not every frame.
- The table marks them (`dfan`, BANK in gen_game_index.py; the profile's
  bank_style() and bank_parts(), sorted left to right because Race Drivin'
  Panorama's first screen is the middle one). The generator also makes Buggy
  Boy's three-screen sets the sit-down cockpit (MAME's "(cockpit)" descriptions);
  the Junior stays the stand-up.
- docs: every multi-screen cabinet surveyed (arcade-cabinets.md 9.2e): what the
  real one had, how sure the sources are, what is built and what is not (Darius
  and Ninja Warriors are one wide glass behind a half-silvered mirror; Cyberball
  2072's tilt is one unattributed summary and is not built). The scope's zoom in
  two docs said 2.5 times and has been 3 since Golgo 13.

Estimated: the 30 degrees and that they turn in (the source does not say which
way), that Race Drivin' Panorama and Driver's Eyes stand as TX-1's do (no source
gives their angles), the monitors' size, the case's depth. No core or ROM was at
hand: the pictures are test cards, one colour and one to three bars each.

Tests: arcade_form_tests bank/ (42 checks: the layout's invariants for any yaw
and unequal parts, the spec of each set and none for Darius or the upright Buggy
Boy, the build with the wings hinged and every case behind its picture, the
screen module's every state), arcade_tests game/. Mutation-tested: 16 mutants,
each turned a case red. Buggy Boy's shifter check moved to the Junior and the
cockpit. Baseline: arcade 507, form 897.
…es and played by swing, button or mouse

Taiko no Tatsujin had a drum a player and no sticks. Each player now has a pair of bachi in a
cradle either side of their drum, each on a cord into the cabinet's apron, taken out and put
back the way a gun is (arcade.md "Captive gear").

What is built
- ArcadeInstruments._taiko names two `gear` rows a player and a narrower pointer box for the
  panel (grip_x), so a laser at a stick finds the stick and not the panel; the builder makes the
  cradles, a `GearRest` marker and the cord's `Entry` on the apron; the model lists them
  (captive_gear) and RetroSystem builds them after the form and frees them before the next one.
- ArcadeGearRack builds each stick as a pickable (ArcadeHeldGear / ArcadeDrumstick, baked by
  Tools/gen/gen_arcade_drumstick.gd), its cradle (a snap zone that takes ITS stick only), the
  mates, and the cord (VerletRope laid hanging down the front by CaptiveCord.hanging_route).
  Sticks are captive: never "spawned", held while the grip is, let go past the cord's reach and
  reeled in when left beyond it.
- A hit is a tap of the pad bit of the drum zone (MAME BUTTON1..4) on the stick's player's port:
  a swing (the head crossing a face downward at 0.45 m/s or more, swept, with a rearm height),
  the trigger / B / Y (don) and A / X (ka) on the hand's own half of the drum, or the mouse on
  the desktop: left a don, right (or middle, a pad's left trigger) a ka, the two sticks taking
  turns. A stick in a hand is somebody at the cabinet, so it starts an attract game.
- On the desktop a stick rides in the lower right and its mate, taken out of its own cradle, in
  the lower left (DesktopPickup's companion); the held hint names the mouse buttons
  (mouse_right_outline.svg added: the mirrored left-button icon, noted in ATTRIBUTIONS).

Two engine-side findings the sticks exposed
- The grab driver's socket preview reaches the grab distance plus the target's bounding radius,
  which for a long stick covered the whole drum and held the stick in its cradle wherever it was
  played. A target may now say its own radius (preview_radius(), in grab_driver.gd and
  function_pickup.gd); the stick says 3 cm.
- A stick let go in the air can hang asleep (the open "controller that will not drop"): the
  second of a pair released with the first did so 4 times in 5 under the windowed probe, and
  `sleeping = false` did not wake it. ArcadeHeldGear._wake_if_hung pushes a released stick that
  is asleep with nothing under it. Root cause not found; documented in quest-device.md.

Tests: arcade_form_tests sticks/ (47 checks, headless) and arcade_hand_tests sticks/ (22 checks,
a fake hand taking a stick through the real pickup function: buttons, hand side, ports, a slow
and a fast swing, the stick held over the drum not drawn into its cradle, seated on release).
37 mutants of the new code, each turned a case red (two survived at first and got stronger
cases). Probe: Tools/models/taiko_desktop_probe (windowed, the real DesktopPickup). Baseline:
form 944, hand 260. Docs: arcade.md, arcade-cabinets.md, testing.md, quest-device.md, CLAUDE.md.

Not run on a core, a ROM, a headset or a Quest: every size, speed and place is an estimate.
… a bay at the screen's height and are held like a guitar

GuitarFreaks had a guitar drawn lying on the deck and a wail button. Each player now has a real
guitar on a cord into the cabinet, hung in a bay at the side of the screen, taken down by the body,
played held the way a guitar is held (body in one hand, neck in the other) and put back, the way
the Taiko sticks and the light guns are (arcade.md "Captive gear").

The cabinet
- A game whose instrument is `guitar` gets the `wide` shape (a vertical glass) at 1.44 m: the deck
  between the bays, a bay either side, the side panels. The picture is 38 inches (4:3, 0.65 m), which
  fills the deck between the guitars and never reaches a guitar's body. `bays` is in `_same_form`.
- Each bay (ArcadeCabinetBuilder._build_bays) is a plate behind the guitar, a boom to a clip round the
  neck, a shelf jutting past the guitar's line over the deck's lip with a cup on it, a hole where the
  cord drops through, a grommet on the apron's face and the `GearRest` marker with the cord's `Entry`.
  Guitars are at the screen's height (waist 1.16 m) and under the marquee, so they never cover the
  picture. A bay of a one-player game keeps its hardware and has no guitar.
- The panels are START and a pedal each (MAME has no pedal for GuitarFreaks: the pedal is the wail,
  BUTTON7, the one button a foot can work while the hands are busy).
- The cord hangs in front of the apron: a rope has no collision with the body and leaves a node along
  its local -Z, so the guitar stands forward of the deck's lip, the Entry is turned about Y and the
  jack's attach point about X. Measured in the windowed probe; the suite's oracle is "both cords hang
  IN FRONT of the apron once settled".

The guitar (ArcadeGuitar extends ArcadeHeldGear, baked by Tools/gen/gen_arcade_guitar.gd)
- Five places a hand takes it by (ArcadeGuitarGrab): BodyR and NeckL, BodyL and NeckR, Desktop. The first
  hand takes the nearest place of its side, the second takes the OTHER end with the other hand
  (second_hand_grab = SECOND averages both poses); the cradle seats it by BodyR. Held, the neck lies out
  to the player's left (right for the mirror), forward and 17 degrees up.
- Neck hand: trigger, X/A and Y/B are the red, green and blue frets (BUTTON1..3, held, latched with a
  gap). Body hand: trigger strums (a tap of BUTTON6 with a haptic), stick up or the neck raised past 25
  degrees is the wail (BUTTON7), stick sideways the effect knob on the dial's axis. One hand works the
  frets and a free hand's trigger strums.
- Desktop: Z X C and V (arcade_fret_r/g/b, arcade_wail, rebindable and on a pad), left click strums,
  right or middle wails, the wheel turns the knob; the guitar rides in the lower right.
- The held hint names the hands it is held in and the player's own keys.

What the rack learned on the way (shared with the sticks)
- A snap zone seats a thing at its own transform times the grab point's inverse, so the cradle now
  stands at `rest * point.transform`; before, the guitar hung at the hand's pose. A gear says its own
  cradle reach (well_reach), its kind, its hint height and its hint rows when a hand arrives, and the
  buttons and sticks it holds (ArcadeInput._with_hands ORs them into its player's state).
- captive_gear() finds `GearRest*` markers anywhere in the model and each names its player and the
  face its cord goes into.
- Four actions in the InputMap and in DesktopBindings / DesktopPad / the binding editor.

Tests: arcade_form_tests guitar/ (49 checks) and arcade_hand_tests guitar/ (42 checks, a fake hand
taking the guitar through the real pickup function: frets, strum, wail with its gaps, the knob, the
tilt, what reaches the port, the left-handed way about, the hint on the guitar, an attract game
started by taking one). 54 mutants of the new code (35 against the form suite, 19 against the hand
suite): every one turned a case red except a defensive guard no caller reaches. Three survived the
first pass and got stronger cases (the cord entry was compared with its own constant, the hint test
read the rows the guitar would compute rather than the ones on it, the other hand's places were not
exercised). Probe: arcade_cabinet_probe --forms=gtrfrk3m (windowed). Baseline: form 990, hand 302,
binding 246, desktop_pad 94. Docs: arcade.md, arcade-cabinets.md, testing.md, CLAUDE.md.

Not run on a core, a ROM, a headset or a Quest: every size, pose, speed and key is an estimate. The
"two pedals" asked for was read as one pedal a player.
…ger, in one hand or two, and turn about the stem with them

The bars of Prop Cycle's bicycle (and a motorbike's) were taken by the trigger only and moved by
pushing the hand sideways: a bar 15 cm over was a full turn. Nobody turns handlebars that way, and
a squeeze, which is how a hand takes hold of handlebars, did nothing. They now follow the hands
(arcade.md "A bicycle's bars follow the hands").

Taking and letting go
- The bars are taken on the squeeze as well as the trigger (ControlGrab.takes_grip, only BarsGrab:
  a squeeze near a stick, a wheel or a dial still takes nothing, because a squeeze is a carry or an
  attach there). A hand lets go when what took it lets go (_taken_with) or when it is 55 cm from the
  middle of the bars. A hand reaching for them has no laser, so the squeeze cannot also carry the
  cabinet.
- A second hand takes the OTHER grip (takes_second_hand, BarsGrab._occupied); no other control has a
  second hand. ControlGrab gained attach_hand / drag_hand / detach_hand / release, which every control
  other than the bars leaves at its old one-hand behaviour. A hand that joins or leaves moves nothing:
  the deflection so far is folded into the base (_fold) and the hand that is left goes on from there.
- Both grips are outlined, amber with a hand on them and green while they can be taken (it was the
  first grip only, which read as the way in).

How they move
- A bicycle's bars turn about the stem: one hand by its angle about the stem since it took hold, so
  the grip stays under the hand up to the lock (28 degrees = a full turn); the right grip toward the
  rider is a turn to the right, the left grip away the same. Two hands by the line between them, one
  grip forward and one back. A hand on the stem (8 cm) or moving along the bar says nothing.
- They tip with the lift of the hands (10 cm is all the way back) and, with both on them, with their
  push or pull; one hand's push or pull is the turn, so it cannot be a tip as well.
- A motorbike's bars keep the sideways push, as the mean of the hands.

Tests: arcade_hand_tests cycle/ (21 checks, the arc, along the bar, at the stem, the lift, the draw
and the stick sent) and handlebars/ (37: squeeze and trigger let-go rules, the squeeze's edge, a
second hand on the other grip and not the same one, two hands by the line and by their push, joining
and leaving without a jump, release, a squeeze on a stick takes nothing, a second hand cannot take a
stick, both grips outlined, a motorbike's squeeze and two hands). 31 mutants of the new code; five
survived the first pass (joining and leaving moved nothing because the test had the bars centred, the
release and the squeeze's edge were unexercised) and got cases of their own. Probe:
arcade_cabinet_probe --shots=handsbars,handscycle (windowed). Baseline: hand 344. Docs: arcade.md,
arcade-cabinets.md, testing.md.

Not run in a headset: the 28 degrees, the 10 cm of lift, the 8 and 15 cm limits and the squeeze
thresholds are estimates, and how it feels to take bars by a squeeze is owed.
…buttons the game has

A panel's controls were silent. Taking the bicycle's bars said nothing about a squeeze, how to turn
them or the crank being a pedal; a stick, a wheel or a dial likewise; and which buttons a game has
(the panel hides the caps the game does not use) was found by trying. The panel now carries a hint
and says it (arcade.md "Control prompts").

What it says
- A control taken by a hand (ArcadePanelHands._take -> ArcadeControlModule.prompt_control): what it
  is for, in the glyph of the button of that hand (trigger; the squeeze for bars). Stick, wheel,
  dial, trackball, gear lever, a bicycle's bars (three lines: turn, a hand on each grip, lift or
  push to tip it), a motorbike's or a board's lean, the crank ("take a pedal, then turn it
  forward"), a slide, a throttle that stays where it is left. The hint floats 22 cm over the control
  that was taken, where the hand is. A second hand joining a pair of bars is the same take.
- What THIS game has, once: the buttons the panel shows, in the core's words when it gave them ("1
  Punch, 2 Kick") and by number when it did not or when there are more than four ("buttons 1 to
  6"), and a wheel's pedals. They are not said at every take of a stick, and are said afresh when
  the game in the panel changes. A cabinet with no button shown (Prop Cycle, a cockpit) says nothing
  of buttons.
- A panel attached to (ArcadeInput._on_attach_changed -> prompt_attached): with a headset's
  controller, which stick moves and which of its buttons are start, the coin and each of the game's,
  by the player's own map for this machine (three to a row); with the keyboard, the keys of the
  cabinet's own map: Ctrl Alt Space, Z X C, 1 start, 5 coin for a cabinet with no map of its own.
- A verb is learned by doing it: each verb row counts one use per take and is gone after three
  (HeldHint.LEARNED_AFTER); the rows about the game are never counted; AppPrefs.show_hints switches
  it all off.

How
- ArcadePrompts (new, pure): the words per kind of control, the buttons' words, the attach rows.
- HeldHint: on_taken / on_taken_desktop show rows with no pickup behind them, clear_rows and unpin
  let one hint serve a host that says something different each time, and `persist_uses` (the suites
  switch the writing of the counts to the player's file off).
- DesktopBindings.display_name_for_system says what a scope's key is without applying the scope: the
  InputMap belongs to whoever holds the keyboard, and a cabinet must not rewrite it to tell the
  player its keys. It shares _layers_for with apply_for_system, and binding_tests checks the two say
  the same for every scope and action.
- CLAUDE.md said a squeeze takes no control; since the bars (N12) it takes those, and the line says so.

Tests: arcade_hand_tests prompts/ (39 checks: the words and every glyph they name for both hands, the
buttons by name and number, the controller's rows by the player's own map and the keyboard's by the
cabinet's keys, the bicycle's bars and crank, a stick with the game's buttons said once, three takes
teaching a verb, another game's buttons said afresh, a wheel's pedals, a motorbike's lean, the hints
off, the hint over the control, attaching with the keyboard and a controller) and binding_tests
desktop/ (8). 41 mutants of the new code: one survived the first pass (the project's own key was
never the answer in any case) and got one. Probe: arcade_cabinet_probe --shots=handsprompts
(windowed). Baseline: hand 383, binding 254. Docs: arcade.md, testing.md, CLAUDE.md.

Not run in a headset: the words, how long the hint stays (HeldHint's 5 s), the 22 cm, and whether it
is where the eyes are with a hand on a control are estimates. A fingertip on a button says nothing.
- A cabinet held on the laser shows its base rectangle and stands
  there on release (GroundPlacement asks the ray-grabbing pickups).
- A seat's cushion can be pressed by the VR laser: SeatTrigger gets a
  sibling StaticBody3D, since the laser ignores Area3D.
- A hand holding a stick, wheel or lever gives its controller's sticks
  and buttons to the game and holds the player's walking back.
- Selectable arcade controls are outlined yellow, and not outlined while
  a hand holds them; the panel grip is a yellow frame, not a green wash.
…nd enhance SeatTrigger for dynamic laser interaction
…sets too

On a Quest the BIOS page of the Arcade tile showed FinalBurn Neo's sets and none of MAME's. The
list (101 sets, generated from MAME's -listxml) had been written into the overlay of `mame`
alone, and the Quest's MAME is `mamearcade` ("MAME (Git)", the arcade half: its .info says
Android cannot take the whole set). A core that declares nothing is silent on the page, so the
MAME section simply was not there, which read as the list having vanished.

mame and mamearcade are the same code base and take the same list:

- libretro-core-info-retroxr/mamearcade_libretro.info is the vendored entry whole (an overlay
  replaces it) plus the same firmware block, and its BIOS note [3] says what mame's entry already
  says (the sets may also sit in SYSTEMDIR/mame/bios; the vendored mamearcade text was never
  updated).
- Tools/arcade/gen_mame_bios.py writes every target (TARGETS), makes the mamearcade overlay from
  the vendored file when it is missing, and has --sync to copy mame's block into the others
  without the 320 MB XML. Run on the current files it changed nothing in mame's.
- FirmwareRequirements.installed_cores_by_system takes the folder and the library naming to look
  at, so a probe or a test can read what a Quest has (mamearcade_libretro_android.so) on a desktop.

With mame, mamearcade and fbneo installed the page now has three sections (FinalBurn Neo, MAME,
MAME (Git)), measured through the real tab (SpawnMenuCoresView, arcade tile: 226 optional sets,
235 rows).

Tests: arcade_tests profiles/ +14 (the Quest's core lists the 101 sets, the same list as mame's
entry for entry, mame/mamearcade/fbneo all on the arcade page, a folder with the Quest's names
gives a section for mamearcade and fbneo and finds all three). Six mutants (the silent core, a
short list, another page, other words, the naming and the folder ignored) each turned a case red.
Baseline: arcade 521. Docs: arcade.md (the paragraph that said mamearcade declares none was the
bug), testing.md.

Not run on a Quest: that mamearcade finds its sets in system/mamearcade/mame/bios is the code
base's (its nvram is already in save/mamearcade/mame/nvram), not a measurement.
…orked by the hands

In a headset the buttons, sticks and levers of a cabinet that was off could not be touched:
ArcadeInput stepped the hands only while the machine was on or attracting, and let every hand go
(and drew nothing of them) the moment it was not. They are worked in every state now. What they
hold reaches no core, because there is none, and a motor does not run.

What it does (arcade.md "A cabinet that is switched off still has its controls worked")
- The hands step in every state (ArcadeInput._step_hands has no `live`; ArcadePanelHands.update
  lets go only of a panel that is hidden or outside the tree). Attract is unchanged: a finger or a
  hand on the panel starts the game there. With the cabinet off nothing starts: the power switch does.
- A cabinet that is off shows what the hands alone hold (ArcadeInput._show_hands_only: _with_hands
  on an empty state, the same mapping a running cabinet uses, into show_state): the caps sink under
  the fingers, the stick, wheel, dial, trackball, pedals, lever and throttle follow the hand and come
  back as they do running. Nothing is sent (last_sent stays empty); the coin, START and the panel's
  own attach are not part of it.
- ArcadeControlModule.show_state(mask, axes, running := true): with running false the game's gear
  is not followed (a lever thrown by hand is in no gear and goes back to first when let go) and the
  moving base, a motor, is not driven (a flight stick leaned on a cabinet that is off leaves the
  cockpit level). `running` is part of the "nothing changed" early out, or a hand that stays on a
  stick across the power switch would leave the base level once the machine is on.
- ArcadeInput._end_run reset the gear and the throttle (reset_shift, rest) EVERY frame the cabinet
  stayed off, so nothing a hand moved could stay. It does that once, when a run ends: a throttle
  pushed forward with the power off stays there, as a throttle does.
- A hold holds the player back, powered or not: it used to be released with the power, which let a
  thumbstick push walk the player off a stick still in the hand. A dance stage's arrows are pressed
  by the body off as on.
- ArcadePanelHands.update(delta, ctrls) and ArcadeControlModule.update_hands lose their `live`
  argument; a laser press on the panel still attaches and the next frame lets go (a machine that is
  off has nobody to play).

Tests: arcade_hand_tests off/ (24: a cap sinks and comes up, the stick's outline and no laser at it,
it leans and comes back, the wheel and a pedal, the gear lever and its spring, a flight cockpit's
base stays level and plays when switched on, a throttle stays put; each "nothing is sent"),
arcade_form_tests stage/ (an arrow of a stage that is off). The cases that pinned the old rule are
rewritten: caps/ and laser/ now hide the panel to see the hands let go, input/ and transfer/ say the
finger and the hold survive the switch, seating/'s end of a run starts one first. Ten mutants (the
hands not stepped, not shown, shown as nothing pressed, sent as well, the base driven, the gear
followed, `running` out of the early out, the reset every frame, a hidden panel not let go, the
stage's body not asked) each turned a case red. Photographed windowed: arcade_cabinet_probe
--shots=hands,handsoff (the stick leaned and button 2 sunk with the machine off, last_sent empty).
Baselines: hand 427, form 991, seat 182. Docs: arcade.md, arcade-cabinets.md, testing.md, CLAUDE.md.

Not run in a headset. Estimates to judge there: that a lever springs back when let go on a machine
that is off (a real one stays in its gear), and that walking stays held while a hand holds a control
of a cabinet that is off.
…ch folder

A DownloadManifest saves into the folder it is set up with, and the "BIOS / Extras row offers
updates" case never set one up, so every run of the suite wrote cores_manifest.json into the cwd of
whoever ran it: the project's own folder, an untracked file in the checkout after every
Tools/run_tests.py. The case now sets the manifest up against a scratch folder under user:// and
removes it. 178 checks as before; the file is no longer made (run alone, and the folder is gone
after).
@XargonWan

Copy link
Copy Markdown
Owner

Already in, thanks!

@XargonWan XargonWan closed this Oct 29, 2026
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.

2 participants