A full-featured Attorney Online 2 client for Nintendo Switch, written in C++17 using SDL2 and devkitPro. Connects to any standard AO2 server over TCP or WebSocket.
Runs on real modded Switch hardware (Atmosphere CFW) and Ryujinx emulator — same .nro binary, no changes needed.
- What is Attorney Online
- Features
- Architecture
- Build Guide
- Running on Ryujinx
- Running on Real Switch
- Asset Setup
- Connecting to a Server
- Controller Reference
- Project Structure
- Module Reference
- AO2 Protocol Notes
- Contributing
Attorney Online is a courtroom roleplay game where players take on the roles of lawyers, witnesses, and judges to act out cases. Players communicate through in-character (IC) speech bubbles with character sprites and animations, out-of-character (OOC) chat, music playback, evidence presentation, and health bar management. The AO2 protocol is a lightweight text-based protocol over TCP or WebSocket.
ferris-ao-switch implements the full AO2 client protocol so Switch players can join existing AO2 servers alongside desktop and browser (WebAO) users in real time.
- Full AO2 protocol — IC messages with animations, OOC chat, music, evidence, health bars, rebuttal/realization, pairing, case alerts, mod calls
- TCP + WebSocket + WSS — connects to any AO2 server; auto-detects
ws://(plain WebSocket) andwss://(TLS WebSocket via mbedtls) prefixes - Dual-platform — same
.nroruns on Ryujinx emulator and real Switch hardware (Atmosphere CFW) - Any AO2 server — compatible with Ferris-AO, tsuserver3, Akasha, and any server implementing the standard AO2 protocol
- Server browser — fetches the public server list from the Attorney Online master server (
https://servers.aceattorneyonline.com/servers); master server URL is configurable in-app
- Character select — full grid of server character slots (grayed-out when taken), with name search/filter, mouse-wheel and touch drag-scrolling for big rosters
- Quick talk — a tap-to-talk IC bar (tap it or press Enter to type & send instantly) with inline
< >emote arrows; the emote isn't reset between lines, so back-and-forth is fast - Room switching — in-courtroom Rooms panel lists every area with live player counts, statuses and lock states (ARUP); join one to move rooms without reconnecting
- IC messages — typewriter effect, word wrap, per-message text colors (12 colors), shownames, objection/hold-it/take-that popups, realization flash, screenshake
- Emote picker — IC composer shows a grid of your character's emotes with sprite-button thumbnails and a live preview of the selected one
- IC log — always-on scrollback column showing recent IC lines (showname + colored, word-wrapped message), newest at the bottom; scroll back through history with the mouse wheel or a finger drag
- OOC chat — your own messages are highlighted so they're easy to find in the log
- Pairing — renders two characters side by side with individual offsets and flip states
- Evidence panel — view, present, add, edit, and delete evidence; grid view with thumbnails
- Music panel — full server music list; select and play any track; shows currently playing track
- OOC chat — scrollable log of OOC messages; send via system keyboard
- HP bars — defense and prosecution health bar display, 0–10 scale
- Narrator mode — send IC messages without a character sprite
- HTTP & HTTPS streaming — loads assets on-demand from a server CDN (
ASSpacket);https://(TLS) andhttp://URLs both supported; no base pack download needed - Off-thread image decode — the 8 worker threads now also decode sprites/backgrounds/icons (PNG/WebP/APNG → frames), so the render thread only does the GPU upload. The heavy decode no longer happens in the courtroom's load gate or the character grid's per-frame budget, so sprites and icons appear as fast as they download
- Smart WebP-first probing — AO2 assets carry no extension on the wire, so the client probes candidates WebP-first (the modern AO default), then
.webp.static/.png/.gif. It learns the format a server actually uses on the first sprite and then probes only that one (char icons/backgrounds via a worker-side sequential probe) — collapsing the cold-load 404 storm (was ~5 requests per asset, 4 of which 404) — with an automatic fall back for any odd/missing asset. The learned format is remembered per server, so a revisit probes right from the first asset - Decoded-animation cache — an LRU of already-decoded frame-sets (≈96 MB VRAM budget) keeps recently-seen characters/backgrounds in memory, so when someone who just spoke talks again their sprite re-shows instantly (no re-decode, no re-fetch)
- Persistent disk cache — streamed assets are saved to
sdmc:/switch/ferris-ao/cache(keyed by full URL) and served from SD on the next view/relaunch, so repeat fetches skip the network entirely — pairs with HTTPS keep-alive to make the most of Cloudflare/CDN edge caching - No-freeze loading — char.ini, sprites, audio and music all load off the main thread (your own sprite is pre-warmed on join, audio plays only from cache); the render loop never blocks on the network, so there's no join freeze or IC stutter even on 3000+ character servers
- Saved settings — a custom showname, theme, master-server URL and volumes persist across servers and launches (
sdmc:/switch/ferris-ao/config.ini) - Theme import — drop AO2 theme folders on the SD card and pick them in Settings (applies
courtroom_design.inilive) - Four-tier fallback — server CDN → community CDN (
attorneyoffline.de/base/) →sdmc:/switch/ferris-ao/base/local pack →romfs:/bundled fallback - Server background only — the courtroom streams the server's real background and never substitutes a bundled default courtroom (black until it loads)
- Sprite reuse — a character talking line after line never re-downloads or re-decodes its sprite (loads are path-cached)
- APNG + GIF animations — character idle, talk, and pre-animations via
IMG_LoadAnimation_RW() - LRU texture cache — 256-slot cache; all lookups use relative paths as keys, regardless of source
- 1280×720 layout — matches Switch native resolution in both docked and handheld modes; full-screen courtroom stage with an overlaid chat bar, corner HP bars, a now-playing strip, and an always-on IC log (authentic AO composition, themeable via
courtroom_design.ini)
- Touchscreen — tap buttons, panels, server/character lists, emote grid; tap the chat box to type a line instantly. Drag to scroll long lists (servers, the character grid, music/rooms panels, the IC log) — the handheld equivalent of a mouse wheel. Works in handheld mode (and via mouse on Ryujinx)
- Joy-Con + Pro Controller — full D-pad/stick/button mapping
- System keyboard — uses libnx
swkbdShow()for all text entry; works correctly on Ryujinx - Keyboard fallback — arrow keys + Enter + letter shortcuts for desktop/emulator development
- BGM — plays server music via SDL_mixer with crossfade between tracks
- SFX — per-message sound effects with an LRU chunk cache
- OGG/Opus/WAV — all formats supported by SDL_mixer portlib
- Background thread — all socket I/O on a dedicated thread; main thread only reads from a lock-free SPSC queue
- WebSocket — custom RFC 6455 implementation (~300 lines, no external dependency); SHA-1 and Base64 inline
- TLS WebSocket (
wss://) — mbedtls (switch-mbedtlsportlib) for encrypted WebSocket connections; SNI sent; certificate verification disabled (no CA bundle on Switch) - Reconnect — synthetic
__DISCONNECTnotification lets the UI handle drops gracefully
┌─────────────────────────────────────────────────────────┐
│ Main Thread (60 Hz) │
│ │
│ App::run() │
│ ├── SDL_PollEvent → InputManager::handle_event() │
│ ├── AOClient::process(InQueue) ← network thread │
│ │ └── mutates GameState │
│ ├── Screen::update(dt_ms) │
│ └── Screen::render() → Renderer → SDL_RenderPresent │
└─────────────────────────────────────────────────────────┘
↕ SPSCQueue (lock-free, no mutex)
┌─────────────────────────────────────────────────────────┐
│ Network Thread │
│ │
│ NetworkThread::run() │
│ ├── SDLNet_TCP_Open / ws_upgrade() │
│ ├── SDLNet_CheckSockets (1 ms poll) │
│ ├── recv bytes → extract AO packets → InQueue.push() │
│ └── OutQueue.pop() → send bytes (TCP or WS frame) │
└─────────────────────────────────────────────────────────┘
GameState is exclusively owned by the main thread. The network thread writes only to InQueue; the main thread writes only to OutQueue. No mutexes on the hot path.
Screen stack (max depth 4) — overlays (OOC panel, music panel, evidence panel, IC input) are pushed on top of CourtroomScreen and rendered bottom-up so lower screens show through.
Download and run the devkitPro installer for your platform. On Windows, use the MSYS2-based devkitPro pacman environment.
Ensure DEVKITPRO is set in your environment (the installer does this automatically):
echo $DEVKITPRO # should print /opt/devkitpro (Linux/Mac) or C:/devkitPro (Windows)Open the devkitPro pacman shell (MSYS2 on Windows, or your terminal on Linux/Mac) and install the required packages:
dkp-pacman -S switch-dev \
switch-sdl2 \
switch-sdl2_image \
switch-sdl2_ttf \
switch-sdl2_mixer \
switch-sdl2_net \
switch-libwebp \
switch-mbedtlsThese install the Switch-cross-compiled SDL2 libraries and their dependencies (libpng, libvorbis, libopus, freetype, libwebp, etc.) into $DEVKITPRO/portlibs/switch/.
switch-libwebp provides both libwebp (static/animated WebP decode) and libwebpdemux (animated WebP frame extraction). Both are required for full WebP support.
git clone https://github.com/SyntaxNyah/ferris-ao-switch.git
cd ferris-ao-switch
makeA successful build produces ferris-ao-switch.nro in the project root. The devkitPro Makefile handles compilation, linking, and the elf2nro step automatically.
Build outputs:
| File | Description |
|---|---|
ferris-ao-switch.nro |
The Switch homebrew executable |
ferris-ao-switch.nacp |
Metadata (title, author, version) |
build/ |
Intermediate object files |
Clean:
make cleanThe Makefile uses these flags for correctness on Switch:
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CXXFLAGS := -std=c++17 -O2 -fno-exceptions -fno-rtti $(ARCH)-fno-exceptions -fno-rtti— standard devkitPro practice; reduces binary size-march=armv8-a+crc+crypto— targets the Switch's Cortex-A57 with hardware CRC and crypto extensions-fPIE— required for Switch's ASLR
Ryujinx emulates Nintendo Switch homebrew correctly, including SDL2, libnx system calls, and the keyboard.
Set these three before launching, or you'll get no network, no typing, or no sound:
| Setting | Where | Why |
|---|---|---|
| Enable Internet Access — ON | Settings → System | Reach AO2 servers and stream assets. Without it the client can't connect to anything. |
| Enable Keyboard — ON | Settings → Input | Type into IC/OOC with your computer keyboard. |
Audio Backend = SDL3 (or SoundIO / OpenAL — not Dummy) |
Settings → Audio | Dummy produces no sound for any game. Also keep Ryujinx's volume up. |
- Open Ryujinx
- File → Open Ryujinx Folder → navigate to
portable/(or wherever your Ryujinx data is) - Drag
ferris-ao-switch.nroonto the Ryujinx game list, or use File → Load Application from File - Ryujinx will boot the
.nroand present the Connect screen
Network on Ryujinx: Ryujinx uses your PC's network stack (Enable Internet Access must be on). Connect to any AO2 server by IP or hostname exactly as you would from a desktop client. localhost works if you're running Ferris-AO on the same machine.
Keyboard on Ryujinx: with Enable Keyboard on, type directly with your computer keyboard. The in-app on-screen keyboard also works via mouse/touch and the controller.
Your Switch must be running Atmosphere custom firmware. Homebrew does not work on stock firmware.
Important: Running homebrew in applet mode (via the Album applet) gives limited RAM. Launch via hbmenu in title override mode (hold R while launching any game) for full memory access, which is recommended for a media-heavy app like this.
-
Copy
ferris-ao-switch.nroto your Switch's SD card at:sdmc:/switch/ferris-ao-switch/ferris-ao-switch.nro(Or any path under
sdmc:/switch/— hbmenu scans subdirectories.) -
Copy your AO base assets to:
sdmc:/switch/ferris-ao/base/See Asset Setup for the expected folder layout.
-
Boot into Atmosphere, open hbmenu, and launch ferris-ao-switch.
-
Use the Joy-Con or Pro Controller to navigate. Press A on any text field to open the system keyboard.
ferris-ao-switch supports on-demand asset streaming directly from a server's CDN. When a server advertises an asset URL (via the ASS packet during handshake), the client fetches every character sprite, background, music file, and sound effect from that URL as needed — no base pack download required.
For players: If the server you're connecting to has a CDN, you don't need to install anything. Just connect and play.
For server operators: Set the asset_url field in your server's config to point to your file server (HTTP or HTTPS). Clients that support it (including ferris-ao-switch) will stream from there automatically. Example:
[server]
asset_url = "https://cdn.myaoserver.com/base"The client constructs requests as <asset_url>/<relative_path>, e.g.:
https://cdn.myaoserver.com/base/characters/phoenix/(a)normal.png
HTTPS / TLS CDNs are fully supported. https:// asset URLs are fetched over
TLS via mbedtls (switch-mbedtls) — the same stack used for wss:// servers and
the master server list. Plain http:// works too. There's also a built-in
secondary community CDN (https://attorneyoffline.de/base/) that fills in the
classic base pack for servers that only host their own custom characters.
If the server has no CDN, or for offline use, assets can be installed locally. ferris-ao-switch looks for assets in three locations, in priority order:
| Priority | Source | When used |
|---|---|---|
| 1 | Server CDN — <server asset_url>/<relative> (HTTP or HTTPS) |
Server sent ASS packet with a URL |
| 2 | Community CDN — https://attorneyoffline.de/base/<relative> |
Classic base-pack fallback (built in) |
| 3 | sdmc:/switch/ferris-ao/base/<relative> |
Local base pack on SD card (optional) |
| 4 | romfs:/<relative> |
Bundled fallback (just the UI font today) |
If the server has no CDN and you have no local base, the client still runs and connects — you can read/send IC and OOC text, browse the music and area lists, and watch HP bars — but character sprites and backgrounds won't appear (nothing to load them from), so the stage stays black behind the chat bar. The courtroom draws its own chat bar, nameplate, HP bars and buttons as primitives, so the UI is fully usable without any art. Point the client at a server with a CDN (most public servers) or drop a base pack on the SD card to get the visuals.
The expected folder structure under base/ mirrors the standard AO2 base pack:
sdmc:/switch/ferris-ao/base/
├── characters/
│ ├── phoenix/
│ │ ├── char.ini
│ │ ├── char_icon.png ← char-select icon
│ │ ├── (a)normal.png ← idle sprite (prefix at char root)
│ │ ├── (b)normal.png ← talk sprite
│ │ ├── normal.png ← bare PNG (used for both if no (a)/(b))
│ │ └── emotions/
│ │ └── button1_off.png ← emote-picker button icons
│ ├── edgeworth/
│ │ └── ...
│ └── ...
├── background/
│ ├── gs4/
│ │ ├── witnessempty.png ← per-position background
│ │ ├── defensedesk.png ← per-position desk overlay
│ │ └── ...
│ └── ...
└── sounds/
├── music/Turnabout_Sisters.opus
├── general/sfx-deskslam.opus
└── blips/male.opus
The standard AO2 base pack is distributed with the Attorney Online 2 desktop client. After installing the desktop client, copy the base/ folder from its installation directory to sdmc:/switch/ferris-ao/base/.
Character sprites follow the AO2 naming convention (matching AO2-Client, AO-SDL,
and webAO). The (a)/(b) marker is a prefix and the sprite lives at the
character root — not in an emotions/ subfolder. Each is probed across the
extensions the server advertises (default order, WebP-first: .webp →
.webp.static → .png → .gif → .apng). The client also learns the
format a server actually ships from the first sprite that decodes and probes
only that one afterwards, so the usual ~5-candidate fan-out collapses to a single
request per asset (with an automatic fall back to the full list for an
odd/missing asset). A server's extensions.json overrides the default order.
| File | Purpose |
|---|---|
characters/<char>/(a)<emote>.<ext> |
Idle sprite (webp/apng/gif) |
characters/<char>/(b)<emote>.<ext> |
Talk sprite (webp/apng/gif) |
characters/<char>/<emote>.png |
Bare PNG — classic static emote, used for both idle and talk when no (a)/(b) exists |
characters/<char>/<preanim>.<ext> |
Pre-animation (no prefix) |
characters/<char>/char_icon.png |
Char-select grid icon |
characters/<char>/char.ini |
Character metadata (name, showname, blips, emotion list) |
The MS packet's emote field is the animation base name, so other players'
sprites render straight from the packet — no char.ini lookup needed. Folder
names are lowercased before the request (AO2 CDNs host lowercase-only trees).
Supported image formats: PNG, APNG, GIF, WebP (static), animated WebP. All are decoded via SDL2_image's IMG_LoadAnimation_RW / IMG_LoadTexture_RW — format detection is by file content, not extension. WebP requires switch-libwebp to be installed (included in the build prerequisites above).
ferris-ao-switch reads standard AO2 desktop-client themes directly from the base pack — no porting or conversion required. On startup, the client loads misc/default/courtroom_design.ini (and courtroom_sounds.ini) from your base folder or the server CDN and applies the theme's layout to the courtroom UI.
What the theme controls:
| Element | INI section |
|---|---|
| Viewport (background + character sprite area) | [Viewport] |
| Chatbox position and size | [Chatbox] |
| IC message text area | [IC text] |
| Nameplate / showname bar | [Showname] / [Nameplate] |
| Defense HP bar | [Defense HP bar] |
| Prosecution HP bar | [Prosecution HP bar] |
| OOC log / side panel | [Log] |
| Music name strip | [Music name] |
| UI sound effects | [Sounds] |
How it works:
- At startup,
ThemeManager::load("default")searches forcourtroom_design.iniin:misc/default/(classic base-pack path)themes/default/(newer AO2 theme path)
- Coordinates are read at their authored resolution (default 960×540) and scaled linearly to 1280×720.
- If no theme file is found, built-in defaults matching the standard AO2 layout are used.
Using a non-default theme:
Themes can be switched at runtime. Future versions will expose a settings screen to select the active theme by name.
Supported sound mappings (from courtroom_sounds.ini, [Sounds] section):
[Sounds]
realization = sfx-realization
testimony = sfx-testimony
cross = sfx-cross_examination
blink = sfx-blink
objection = sfx-objection
holdit = sfx-holdit
takethat = sfx-takethat
guilty = sfx-guilty
notguilty = sfx-notguiltyThe Connect screen has two tabs — switch between them with L / R:
Displays the public server list fetched from the Attorney Online master server in the background. Each row shows:
- Server name
- Player count
- Address and port
- Short description
Press A on any row to connect immediately. Press R to refresh the list. The master server URL defaults to https://servers.aceattorneyonline.com/servers and can be changed in-app by pressing ZL.
Manually enter connection details for servers not on the public list:
| Field | Description | Default |
|---|---|---|
| Host | Server IP address, hostname, or WebSocket URL | 127.0.0.1 |
| Port | TCP port (AO2 default: 27017) or WebSocket port | 27017 |
| Username | Your OOC display name | Switch |
Press A on a field to open the system keyboard and edit it. Press ZR to connect.
Everything here is saved to sdmc:/switch/ferris-ao/config.ini automatically and
persists across servers and launches. Up/Down to select a row, A / ←→ to change (or tap):
| Setting | What it does |
|---|---|
| Showname | Custom IC/OOC display name (blank = username). Persists on close. |
| Theme | Cycles through AO2 theme folders found on the SD card (see below) + the built-in default; applies instantly. |
| SFX / Music Volume | 0–128, applied live. |
Importing AO2 themes: drop a standard AO2 theme folder (one containing
courtroom_design.ini) into sdmc:/switch/ferris-ao/base/themes/ (or …/misc/)
and it appears in the Theme setting to select.
Project info and the repo link (also see Credits).
WebSocket servers: Prefix the host with ws:// to connect in WebSocket mode, or wss:// for TLS WebSocket (encrypted). Examples:
ws://game.example.comon port27018— plain WebSocketwss://game.example.comon port443— TLS WebSocket (default port 443)
After connecting, the handshake sequence runs automatically:
Connect → Character Select → Area Select → Courtroom
If the server places you in an area automatically (single-area servers), the Area Select screen may be skipped.
| Button | Action |
|---|---|
| + | Disconnect / return to Connect screen |
| D-pad | Navigate menus and lists |
| A | Confirm / select |
| B | Back / close overlay |
| Button | Action |
|---|---|
| L / R | Switch between Servers and Direct Connect tabs |
| D-pad Up/Down | Move selection (server list or field) |
| Mouse wheel / touch drag | Scroll the server list |
| A | Connect to selected server (Servers tab) / Edit field (Direct Connect tab) |
| R | Refresh server list (Servers tab) |
| ZL | Edit master server URL |
| ZR | Connect (Direct Connect tab) |
| Button | Action |
|---|---|
| D-pad | Move cursor |
| Mouse wheel / touch drag | Scroll the grid a row at a time |
| A | Select character (if not taken) |
| Y / F | Search characters by name (system keyboard) |
| B | Clear the search |
Searching filters the grid to matching names — the fast way to find one on a 600+ character server.
| Button | Action |
|---|---|
| D-pad Up/Down | Move cursor |
| Right stick | Scroll list |
| A | Enter area |
The stage fills the whole screen; a chat bar is overlaid across the bottom. The
chatbox has the showname merged in as a corner tab (not a floating plate),
with the incoming IC text inside and a tap-to-talk input bar below it that
carries inline < > emote arrows. A row of status buttons (IC / OOC /
Music / Evi / Rooms) sits to the right with key hints; HP bars are in the top
corners and the now-playing track runs along the top.
| Button | Action |
|---|---|
| X | Toggle the IC composer (full emote grid + preview) |
| L | Toggle the OOC chat panel |
| R | Toggle the music panel |
| Y | Toggle the evidence panel |
| − (Minus) | Toggle the Rooms panel (switch areas) |
| ← / → | Cycle your emote (no composer needed) |
| A / Enter | Quick-talk: type & send with the current emote — or skip the typewriter / confirm in a panel |
| D-pad Up/Down | Navigate / scroll the open panel |
| Mouse wheel / touch drag | Scroll the open panel or the IC log |
| B | Close the open panel |
| + | Leave the courtroom (disconnect) |
Keyboard equivalents: X IC, Z OOC, C Music, Y Evidence, R Rooms,
P leave, arrows/Enter/Esc for navigate/confirm/back.
Open with − (Minus) / R. Lists every area the server advertised with its
live player count, status (IDLE/CASING/…) and a [LOCKED] marker; your
current room is highlighted. Up/Down to move, A to join (the client
sends the AO2 area-join and the server swaps your background, HP and roster),
B to close.
Opened with X. Shows a grid of your character's emotes (with sprite-button thumbnails when the server provides them), a larger preview of the selected emote, the text colour with a live swatch, your position, and a preview of the typed message. It closes itself after a line is sent so you can watch it play. Thumbnails stream in the background, so opening it never stalls the courtroom.
| Button | Action |
|---|---|
| D-pad Left/Right | Move through the emote grid (from your char.ini) |
| D-pad Up/Down | Cycle the text colour |
| A | Open the system keyboard, then type and send the line |
| B / X | Close the composer |
Everything is tappable in handheld mode (and via mouse on Ryujinx):
| Screen | Tap |
|---|---|
| Connect | Tap a tab (Servers / Direct / Credits); tap a server to select, tap it again to connect; tap a Direct field to edit/connect |
| Character select | Tap the search bar to filter; tap a character to highlight, tap it again to pick |
| Courtroom | Tap the IC input bar to type a line instantly (current emote/colour/pos); tap the < > arrows on it to change emote; tap a HUD button to open its panel |
| Music / Rooms panel | Tap a row to play that track / join that room |
| IC composer | Tap an emote to select it; tap the message box to type & send |
| OOC panel | Tap to open the keyboard |
| Any panel | Tap outside it to close |
Drag to scroll. Since the handheld has no mouse wheel, a finger drag scrolls whatever has focus — the server list, the character grid, the music/rooms/evidence/OOC panels, the composer's emote grid, and the IC-log scrollback. A quick press-and-release is still a tap; only movement past a small threshold becomes a scroll, so taps and drags never collide. Mouse wheel (Ryujinx/desktop) drives the exact same scrolling.
ferris-ao-switch/
├── Makefile # devkitPro NX + SDL2 portlibs build system
├── icon.jpg # 256×256 NRO icon
├── romfs/ # Bundled assets (romfsInit → romfs:/)
│ └── fonts/noto_sans.ttf # UI font — the ONLY bundled asset; all art,
│ # characters, sounds and music stream over
│ # HTTP or come from the optional sdmc: base
│ # pack. The courtroom draws primitives when
│ # an image is missing, so none need bundling.
└── src/
├── main.cpp # Entry point — init App, push ConnectScreen, run
├── app.hpp / app.cpp # App class: game loop, screen stack, SDL init
├── net/
│ ├── packet_queue.hpp # Lock-free SPSC ring buffer (template, N must be power-of-2)
│ ├── http_fetch.hpp/cpp # Synchronous HTTP/1.1 GET (HTTPS via TlsConn)
│ ├── tls_conn.hpp/cpp # mbedtls TLS client (WSS + HTTPS), #ifdef AO_TLS
│ ├── connect_pool.hpp/cpp # Single-thread TCP connect pool (libnx thread-table safe)
│ ├── ws_handshake.hpp/cpp # HTTP/1.1 WS upgrade, inline SHA-1 + Base64
│ ├── ws_frame.hpp/cpp # RFC 6455 frame encode (masked text) / decode
│ └── network_thread.hpp/cpp # Background thread: recv loop, packet extraction, send
├── protocol/
│ ├── packet.hpp # Packet struct, parse(), escape/unescape
│ ├── ao_client.hpp/cpp # Handshake state machine + all in-lobby packet handlers
│ └── commands.hpp # Outgoing packet builder free functions (stack buffers)
├── state/
│ ├── game_state.hpp # All mutable game state, main-thread only
│ │ # (CharacterInfo, AreaInfo, EvidenceEntry,
│ │ # ChatLog ring buffer, ICAnimState — all here)
│ └── settings.hpp/cpp # Persisted prefs (showname/theme/URL/volumes) → config.ini
├── assets/
│ ├── asset_manager.hpp/cpp # 4-tier resolution: prefetch → CDN×2 → sdmc: → romfs:
│ ├── asset_stream.hpp/cpp # Background worker threads that pre-warm the cache
│ ├── extensions_config.hpp/cpp # extensions.json (per-category file-ext probe order)
│ ├── char_ini_parser.hpp/cpp # Windows INI parser for char.ini
│ ├── theme_manager.hpp/cpp # AO2 courtroom_design.ini → scaled ThemeLayout
│ ├── texture_cache.hpp/cpp # LRU SDL_Texture* cache (256 slots)
│ └── apng_player.hpp/cpp # APNG/GIF/animated-WebP via IMG_LoadAnimation_RW()
├── audio/
│ ├── audio_manager.hpp/cpp # SFX: Mix_Chunk LRU cache
│ └── music_player.hpp/cpp # BGM: Mix_Music with crossfade
├── render/
│ ├── renderer.hpp/cpp # SDL_Renderer wrapper + Layout:: constants, 1280×720
│ └── text_renderer.hpp/cpp # SDL_ttf wrapper, 32-slot LRU texture cache
├── ui/
│ ├── screen.hpp # Abstract Screen base class (opaque() compositing)
│ └── screens/
│ ├── connect_screen.hpp/cpp # Server browser + Direct Connect tabs
│ ├── char_select_screen.hpp/cpp # 8×4 character grid
│ ├── area_select_screen.hpp/cpp # Scrollable area list with ARUP data
│ └── courtroom_screen.hpp/cpp # Main courtroom: stage, chat bar, HUD, panels
└── input/
├── input_manager.hpp/cpp # SDL_GameController → Action enum, keyboard fallback
└── virtual_keyboard.hpp/cpp # libnx swkbdShow() wrapper (stdin fallback on desktop)
Lock-free single-producer / single-consumer ring buffer. Template parameter N must be a power of two. Uses std::atomic<int> head/tail with acquire/release ordering. No heap allocation after construction.
SPSCQueue<InPacket, 256> in_queue; // network thread → main thread
SPSCQueue<OutPacket, 64> out_queue; // main thread → network threadInline SHA-1 (~80 lines) and Base64 encoder. Sends a standard HTTP/1.1 GET upgrade request, validates the Sec-WebSocket-Accept response header. No external cryptography library needed — SHA-1 is only used for the handshake key validation, not for security.
ws_encode_frame()— client→server frames are always masked (RFC 6455 §5.3); usesSDL_GetTicks()XOR'd with the payload address as a mask key seedws_decode_frame()— server→client frames are unmasked; handles 7-bit, 16-bit, and 64-bit payload lengths; returnsFrameResultenum
Packet format: HEADER#field0#field1#...#%
parse_packet() splits on #, stores header and fields in fixed char arrays (no heap). Returns bytes consumed (0 = incomplete). Packet::unescape() and Packet::escape() handle the four AO2 escape sequences in-place.
States: Idle → WaitDecryptor → WaitId → WaitSi → WaitSc → WaitSm → WaitDone → InLobby
Each state transition sends the appropriate outgoing packet and waits for the server's response. Once DONE is received, all subsequent packets are dispatched to in-lobby handlers which mutate GameState directly.
All builders write into caller-supplied stack buffers and return the byte count. No heap allocation. Example:
char buf[256];
int n = ao::cmd::ct(buf, sizeof(buf), "MyName", "Hello world!");
out_queue.push({buf, n}); // push to network threadUses SDL2_image's IMG_LoadAnimation() (requires SDL2_image ≥ 2.6). Loads all frames into an array of SDL_Texture* (max 128 frames). update(dt_ms) advances the frame counter; current() returns the active texture. Falls back to IMG_Load() for static PNG if the file is not animated.
On Switch: calls swkbdCreate(), swkbdConfigSetGuideText(), swkbdShow(), swkbdClose(). The system keyboard applet blocks until the user confirms or cancels. Works identically on Ryujinx.
On desktop (non-__SWITCH__ builds): reads from stdin, allowing dev/testing without a Switch.
HEADER#field0#field1#...#%
All fields are UTF-8 text. Special characters are escaped:
| Wire | Meaning |
|---|---|
<num> |
# |
<percent> |
% |
<dollar> |
$ |
<and> |
& |
← decryptor#NOENCRYPT#%
→ HI#<hdid>#%
← ID#0#<servername>#<version>#%
← PN#<players>#<max>#<description>#%
← FL#<feature flags...>#%
[← ASS#<asset_url>#%]
→ ID#ferris-ao-switch#0.1#%
→ askchaa#%
← SI#<char_count>#<evi_count>#<music_count>#%
→ RC#%
← SC#<char0>#<char1>#...#%
→ RM#%
← SM#<area0>#<area1>#...#<song0>#<song1>#...#%
→ RD#%
← LE#...#%
← CharsCheck#<0|1>#...#%
← HP#1#<defense_val>#%
← HP#2#<prosecution_val>#%
← BN#<background>#%
← DONE#%
→ CC#<uid>#<char_id>#<hdid>#% ← join with chosen character
The server broadcasts 30 fields; the client sends 26. Fields 17, 18, 20, 21 are inserted server-side from the pairing partner's state.
[0] desk_mod [1] pre_anim [2] char_name [3] emote
[4] message [5] pos [6] sfx [7] emote_mod
[8] char_id [9] clip [10] objection_mod [11] evidence_id
[12] flip [13] realization [14] text_color [15] showname
[16] other_charid [17] other_name* [18] other_emote*
[19] self_offset [20] other_offset* [21] other_flip*
[22] immediate [23] looping_sfx [24] screenshake [25] frame_screenshake
[26] frame_real [27] frame_sfx [28] additive [29] effects
* = server-inserted pairing fields
Four types, sent as a broadcast whenever area data changes (delta-suppressed server-side):
| Type | Data |
|---|---|
ARUP#0#... |
Player counts per area |
ARUP#1#... |
Status strings per area (IDLE, CASING, RECESS, etc.) |
ARUP#2#... |
CM labels per area |
ARUP#3#... |
Lock states per area (FREE, SPECTATABLE, LOCKED) |
Areas come first (entries without .), then music files (entries containing .). The client splits on the first entry containing a . to determine where areas end and music begins.
Pull requests are welcome. Before contributing:
- Build successfully with
maketargeting Switch - Test on Ryujinx with a real AO2 server connection
- Follow the no-heap-in-hot-path rule: all per-frame data uses fixed arrays
- No
std::string,std::vector, or dynamic allocation inPacket,GameState, or the network path - New screens must inherit from
Screenand be pushed/popped viaApp::push_screen()/App::pop_screen() - New outgoing packet types belong in
protocol/commands.hppas free functions
- Evidence can be viewed but not yet attached to an outgoing IC message from the UI
- Switch rooms from inside the courtroom via the Rooms panel (
−); the separate Area Select screen pushed before the courtroom is currently bypassed (Character Select enters the courtroom directly)
Created by SyntaxNyah — https://github.com/SyntaxNyah/ferris-ao-switch
ferris-ao-switch is not affiliated with the official Attorney Online project.