These files had never been tracked anywhere - they lived in a plain directory with no git at all, which is also where the whole reverse-engineering record sat. Code already committed refers to them by name (opponent_substitution.h cites "ANALYSIS.md section 6hh", DebugMenuOverlay.kt cites "DEBUG_MENU.md section 3"), so until now a fresh clone carried references to documents it did not contain. ANALYSIS.md the RE record, and the reason the rest works ARCHITECTURE.md how the mod's pieces fit together ARM64_TRANSLATION_LAYER.md the translation layer's running log PROGRESS.md chronological progress across both chats BETA_TELEMETRY_PLAN.md how crash/telemetry reporting is meant to work LOBBY_UI_DESIGN.md + .html lobby design and its clickable prototype DEBUG_MENU.md debug panel design STATIC_RECOMPILATION_FALLBACK.md the plan if translation had not panned out evidence/ font atlas capture from the glyph-corruption bug save_backups/ saves at known milestones, for reproducing state Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
LOBBY_UI_DESIGN.md — Multiplayer Lobby UI & Flow
Living document for the lobby overlay's UI/UX — screens, flow, data shape, and the product decisions behind them. Split out of ARCHITECTURE.md on 2026-08-18 (that file now only links here) so the native/engine-integration doc doesn't keep growing with UI-only decisions. Update this file whenever a screen or flow decision changes; don't let it drift from the prototype.
Clickable prototype: LOBBY_PROTOTYPE.html — self-contained, open directly in any browser, no server needed. Landscape orientation (matches the game). Every screen is tagged OVERLAY (our Compose UI) / NATIVE (real, unmodified game screen) / TRANSITION, with inline notes on what's already proven on-device vs. still open.
1. Lobby UI mechanism — overlay View, markers via BitmapGraphics
Recommendation: hybrid. Full interactive lobby screens (player list, ready-up, countdown) go through an Android overlay View/Compose layer on top of the game's GLSurfaceView — the native UI pipeline has no concept of touch-driven widgets, so it can't host these itself. Simple in-world/in-menu indicators (e.g. a future green/red map marker for an active lobby) should instead go through the BitmapGraphics native text/atlas bridge (launcher/app/src/main/java/com/ea/ironmonkey/BitmapGraphics.kt, ANALYSIS.md §3.3) — verified, already trusted by the game's own renderer, no new Flow/SB authoring needed.
| Concern | Map markers → BitmapGraphics extension |
Full lobby screens → Android overlay View |
|---|---|---|
| Mechanism | Add a method (e.g. drawMarker(x, y, color) via canvas.drawCircle) to BitmapGraphics.kt; call it from a hook alongside the existing drawString JNI call site (0x5625a0) |
Standard Android View/Compose overlay added to the activity's view hierarchy above/around the GLSurfaceView |
| Implementation cost | Low — one new Kotlin method, one new hook call from mpcore at the point we know a lobby is active |
Low — standard Android UI, fast iteration |
| Visual integration | Genuinely native — rendered through the exact same atlas-blit path as the game's own UI text, in the game's own coordinate space | Good but technically a separate layer composited over/under the GL surface |
| Touch/interaction support | None — this pipeline only produces pixels in a texture; no input handling | Full Android input handling, exactly what's needed for player list + ready buttons |
| Risk to singleplayer | Low — purely additive draw call, no-ops when no lobby is active | None — overlay is purely additive, toggled only when a multiplayer session exists |
| Car/upgrade display integration | N/A | We don't render the car ourselves either way — invoke the game's existing native car-select screen (same one singleplayer uses) as a sub-flow, our overlay only wraps around it |
Both are additive and don't touch Flow/SB authoring, so neither destabilizes the shared menu system. Preliminary — revisit once a first marker hook is actually implemented.
Still open: whether EAMText/GlyphMeshGLES/FreeType (also present in the binary, ANALYSIS.md §3.3) plays any role that would interfere with a BitmapGraphics-based marker — low priority until that hook is attempted.
2. Flow decision & data schema
Flow: Compose/View overlay for lobby creation and management, native car-select (filtered by the lobby's class setting) as a sub-flow, ready-checkmark to confirm. "Lobbies as cards on real street pins" is shelved — depends on hooking visible-street MapTrack event population, which visible-streets-investigation found no viable path for. The overlay approach needs no further RE — arbitrary track loading (ARCHITECTURE §3a) and the native car-select screen already work.
Lobby {
id: string
hostPlayerId: string
address: { ip: string, port: int } // see §4 — shown for both LAN and WAN join
trackId: string // arbitrary track, see ARCHITECTURE §3a BuildTrackScenePath hook
classRestriction: int | null // car class filter applied to native car-select; null = any
maxPlayers: int // real cap not decided yet (see §6a) — UI works for whatever it ends up being
rewards: { // reuses the CashReward shape found on RaceEvent, ANALYSIS.md §6aa
bronze: int
silver: int
gold: int
} // see §5 — explicit fields, validated at creation, not free-floating pills
players: [LobbyPlayer]
}
LobbyPlayer {
playerId: string
displayName: string // from the local PlayerProfile, see §3 — sent to peers on join
avatarId: string // preset id ("color+icon"), see §3 — not a photo/upload
isLeader: bool // explicit flag, not inferred from hostPlayerId match —
// keeps leader-ness a first-class, UI-visible fact instead of
// something every screen has to re-derive by comparison
ready: ReadyState
carId: string
colourIndex: int
mods: [int | null, int | null] // exactly 2 upgrade slots; each is a CarMod ModType id
// (or null = "НЕТ"/empty). Purely for display — other players
// resolve name/icon/description client-side from the car's own
// CarDescription.Mods catalog, already loaded for the garage
// screen. No numeric value/balance computation server-side.
}
enum ReadyState {
SetsUpLobby // leader only — track/class/rewards configuration
ChoosingCar
ChoosingRace
Loading // race-start fired, client is loading its own RaceLoaderTask
NotReady
Ready
}
Confirmed against the live in-game upgrade screen (2 slots, ВЫБОР УЛУЧШЕНИЯ list) and the CarMod struct in alfaromeo_4c_2012_desc.prefabs.sb — 7/7 checked entries matched by price (ModType 1/4/5/6/7/8 confirmed, TYRES_REINFLATING/CHASSIS_REINFORCED/BODY_IMPACT/POWERTRAIN_POWERPACK/NITROUS_BURN/NITROUS_EARN). ModType is a unique per-option id, not a shared category grouping. The separate im::app::car::CarPart/UpgradeParts catalog (skipCost/orderTimeMinutes timer-gating fields) has no live instance data anywhere checked — not part of this schema.
Resolved decisions:
- Host-leaves-lobby: auto-promote the longest-connected remaining
LobbyPlayerto leader; dissolve only when the lobby becomes empty. classRestriction: enforced explicitly by our own overlay (revised 2026-08-19, was "soft/advisory only") — car_select has no known native class-filter hook (RACEEVENT_CATEGORYTAG_OFFSETis a confirmed false lead, PROGRESS.md cont.58; 5 independent dead ends, cont.58-60), so enforcement can't happen inside the native carousel — but that's not a reason to skip enforcing it at all. The overlay checks the chosencarId's class againstclassRestrictionthe momentonCarSelectedfires; a mismatch blocks readying up (a visible warning replaces the ready button, "Сменить" is the only way forward) rather than silently letting a mismatched car through. Still a client-side check, not a true hard filter on the carousel itself — revisit if the real class-filter mechanism is ever found.- Straggler/timeout policy: hard timeout, auto-start, default 45s from when the first client finishes loading — see §6.
3. Player identity — profile chip, not a form
Every player row needs something to display before anyone's picked a car, and it can't come from an EA account — ARCHITECTURE §8 already rules out the Synergy backend for multiplayer, and this project has no login system.
Where the button lives: a small persistent chip (avatar + name) pinned in a fixed corner, present on every OVERLAY screen (map's entry point, lobby browser, create, waiting room) — not buried inside one specific screen's header. It's global identity, so it shouldn't only be reachable from one place. It deliberately does not appear on the two screens where our footprint is intentionally minimal (the native car_select hand-off, the start countdown) — same principle as the minimized lobby badge there.
Tapping it opens a popover, not a full-screen navigation — name field + an 8-preset avatar grid, right there over whatever screen you were on. Editing your name mid-lobby-browsing shouldn't cost you your place in the flow.
First run — not a forced form. Auto-generate a usable default (ROOKIE_####, random 4-digit suffix, random avatar preset) the moment the profile is first read, so multiplayer works immediately with zero typing. Edit anytime via the chip. This mirrors how most lobby-based games (Fortnite, Fall Guys, etc.) handle it — a name you can live with immediately, not a gate.
PlayerProfile {
displayName: string // local default "ROOKIE_####" (random), freely editable
avatarId: string // one of a small fixed set of {color, glyph} presets
}
Stored locally on-device only (Android SharedPreferences/DataStore — no server round-trip, no RE work, plain new Kotlin). Avatar is a preset, not a photo — a small fixed palette of colour+glyph combinations (🏎️/⚡/🔥/🏁/★/🌙/⚙/💀 over a themed accent colour), picked from a grid. Deliberately not a camera/gallery upload: no permissions prompt, no image transport concerns once RakNet carries lobby state, and it matches the HUD-badge visual language already established by CarSelectionBadge. Revisit only if the game ever needs a real player photo — nothing today requires it.
PlayerProfile is copied into LobbyPlayer.displayName/avatarId at join/create time and travels with the rest of the lobby state once RakNet exists.
4. Addressing — IP and port, LAN and WAN alike
Every lobby reference carries an explicit address: { ip, port } (schema above), surfaced consistently rather than hidden behind "LAN auto-discovers, WAN doesn't need it":
- LAN browser cards show the host's LAN address (
192.168.1.42:7777) alongside name/track/players — not just a bare "● LAN" ping indicator. - Manual "по IP" join tab gets real address + port input fields (not a stub), validated before the connect action enables: IPv4 dotted-quad or hostname pattern, port an integer in
1–65535. - Waiting room shows the host's own address in the side panel ("Адрес:
192.168.1.42:7777", with a copy affordance — a plain icon button, not a text label, to stay out of the way of the address itself) — for sharing with WAN friends who need to type it in manually, since there's no master-server/matchmaking list yet (ARCHITECTURE §5 — direct-connect only for the first working version).
Default placeholder port used throughout mocks/design: 7777 — not a final decision, just a consistent stand-in until RakNet setup actually picks one.
5. Rewards — explicit fields, validated
Bronze/Silver/Gold are real numeric inputs in Create Lobby, not fixed display pills. Validation, checked live as the leader types:
- Each value is a positive integer.
- Non-decreasing:
bronze ≤ silver ≤ gold. - "Создать" stays disabled with an inline error message until all three pass.
No other reward shape changes — still the same CashReward-derived structure (ANALYSIS.md §6aa) on the Lobby schema above.
The waiting room shows all three tiers too, not just gold — three small cells (🥇/🥈/🥉 + amount), not one pill. This mirrors the real game's own EventDetails screen (see §9 — its "1-Й/2-Й/3-Й НА ФИНИШЕ" reward list is exactly this shape), so the convention isn't invented, it's matched to what the real UI already does one screen over.
6. Waiting room — track hero, ready as a button
Revised 2026-08-18 after direct feedback that the first landscape pass was really just a portrait layout squeezed sideways, not a real redesign:
- Player rows show the car name and paint colour next to the player name — no car-shaped icon. An earlier pass added a small CSS-drawn car silhouette per row; cut it after direct feedback that it read as visual noise ("некрасиво") rather than useful information. The colour swatch (below) already carries the "this player picked a car" signal on its own.
- Track preview: the leader's chosen track shows as a placeholder hero block (a wide gradient/road-motif rectangle standing in for a real screenshot, which doesn't exist yet) with the track's friendly name and internal id caption — more prominent than a small text pill, since "what track is this" is the single most important piece of context in the room.
- Readiness is a button, not a toggle switch — deliberately different from the small iOS-style switch most lobby UIs default to. A full-width button reading "Я ГОТОВ" that becomes a solid, distinctly-coloured "✓ ГОТОВ" on press (press again to un-ready) is a clearer tap target and a clearer at-a-glance state, especially at the smaller touch scale of a landscape phone overlay.
- Car choice stays changeable after readying up. A persistent "Машина:
<car>· Сменить" row is always tappable — even whenready == Ready— and always reopens native car_select. Changing car always resetsready → NotReadyautomatically, so no peer ever sees a stale "ready" attached to a car that just changed; the player has to explicitly re-press ready afterward. - Paint colour is its own swatch, a small bordered round dot next to the car name (matching the visual weight of the game's own PAINT1–PAINT6 respray swatches) — never baked into an icon shape. An intermediate version tinted the (since-removed) car icon with the colour directly; that read poorly at small size, especially light/white paints against the dark panel. Same underlying
colorHexdata (fromonCarSelected, proven), just its own unambiguous element. - Upgrades are shown too, using the exact
mods: [int|null, int|null]shape already in theLobby/LobbyPlayerschema (§2) and already flowing live offonUpgradesAccepted(proven, cont.39-41): the compact player list shows a 2-dot filled/empty indicator per row (room space is tight — a name is more useful there than two upgrade names), while your own car card in the side panel shows the full resolved names for both slots (or "НЕТ" for an empty slot, matching the schema comment's own convention). Slot→name resolution (ModTypeid → display string) is a placeholder mapping in the prototype — the real display strings live in the game's own localized upgrade-screen text, not extracted here. - Fixed 2026-08-19: the demo car assigned when confirming through the normal flow (
Chevrolet Corvette ZR1) now matches the lobby's default class (СПОРТКАР), so the ready button is visible on the main click-through path instead of being silently replaced by the class-mismatch warning every time. The mismatch state (§2, still a real, useful thing to demo) is now reached deliberately, via a small "[демо] показать несовпадение класса" toggle link next to the ready button — not something the primary flow trips over by accident.
6a. Max players per lobby — real number TBD, UI works either way
Added 2026-08-19: the actual cap isn't decided yet, so the UI is built to not assume one.
- Create Lobby gets a
−/+stepper next to the track selector (range 2–12, arbitrary bounds — just enough to make the control meaningfully bounded, not a real design decision). - Waiting room shows a live
👤 N/maxpill next to the class tag, turning the same warning colour used elsewhere (--danger) once the room is full. - Server browser cards show
current/maxinstead of a static player-count string, and a full server (current == max) is visually locked — dimmed, tagged "ЗАПОЛНЕНО", not tappable — rather than silently allowing a join that wouldn't actually work.
None of this enforces anything server-side (there's no server yet) — it's UI that reflects whatever Lobby.maxPlayers ends up being once that number is actually decided.
7. Loading & waiting for stragglers — engine-level, not a custom screen
Revised 2026-08-18 per direct correction — the earlier draft had this as our own Compose "Загрузка заезда" screen with a list of progress bars. That's the wrong layer for it to live on. The actual requirement: one phone loads fast, another loads slow, and whoever's ready first needs to visibly still be waiting on people, not staring at our UI — the intro/starting-line scene should keep playing, on loop, until everyone's in.
- Once a client's own race load reaches the point where the game would normally show its native pre-race starting-grid scene (cars lined up, camera settled, about to cut to the 3-2-1-GO countdown), that's what stays on screen — not a custom loading UI panel. Consistent with this project's standing principle (ARCHITECTURE §2): hook the game's own flow, don't replace it with ours.
- Our overlay's only contribution here is a thin status strip, not a full panel — something like "Ожидаем игроков (2/4) · SLOWPOKE_88 загружается", pinned to one edge, with a subtle looping pulse so it visibly reads as "still working," not "frozen."
- The native 3-2-1-GO countdown itself is held until the host signals everyone's in, then plays exactly as it would in a normal single-player race start — no substitute countdown of our own.
- Engineering reality check: the hold/release mechanism doesn't exist yet. Candidate hook anchor is
InRaceStateconstruction (RTTI-confirmed, ANALYSIS.md §3.1) or the final event inRaceLoaderTask_DispatchInitialFSMEvents(ANALYSIS.md §6b stage 8) — either is a real, already-located point, just not yet wired for this purpose.RaceLoaderTask_ExecuteLoadSequence's ownSetLoadProgress(this, N)calls (ANALYSIS.md §6a slot 4, fractions 0.1–0.8) are the natural source for the real per-client progress number to report to peers, instead of inventing a synthetic one. - Straggler timeout still applies on top of this: hard 45s timeout (resolved 2026-08-18) from when the first client is ready — if it expires before everyone's in, the held countdown releases anyway and whoever isn't in yet is left behind. Left-behind representation (spectator vs. rubber-band-in-when-ready) is still an open, lower-priority call.
- Also still open, and upstream of all of this (see ARCHITECTURE §4b step 5.1): which race event actually carries the load. The synthetic "LAN" event currently has its loadout-confirm deliberately redirected back to the map because it has no real track/scene reference (crashes otherwise, PROGRESS.md cont.36) — the recommended fix is having the lobby's "start" re-target a real, existing race event as the vessel and let its loadout-confirm proceed normally (real events already sail through with zero crashes, cont.41), rather than trying to give the synthetic event a scene reference of its own.
8. Screen inventory (prototype)
Matches LOBBY_PROTOTYPE.html's screen rail. Profile is not in this numbered flow — it's the global chip/popover from §3, demonstrable from any screen.
| # | Screen | Tag | What it is |
|---|---|---|---|
| 0 | Карта — точка входа | OVERLAY | Entry button on the map, same FrameLayout pattern as CarSelectionBadge |
| 1 | Обзор лобби | OVERLAY | LAN list (with address) + "по IP" tab (validated) + create |
| 2 | Создание лобби | OVERLAY | Name, track (with placeholder preview), class, validated rewards |
| 3 | Комната ожидания | OVERLAY | Player rows (ready-button, reward trio, mod dots), track hero, address |
| 4 | event_details (перед car_select) | NATIVE | Real on-device screenshot — any ordinary event, not ours; see §9 |
| 5 | car_select (после event_details) | NATIVE | Real on-device screenshot — our overlay shrinks to a corner badge |
| 6 | Снова в лобби: авто выбрано | OVERLAY | Car/colour captured; "Сменить" stays available; ready button |
| 7 | Все готовы | OVERLAY | Host's "Начать гонку" enabled once everyone's Ready |
| 8 | Ожидание в гонке | NATIVE + thin overlay | Native starting-grid scene loops; thin status strip shows stragglers |
| 9 | Старт заезда | TRANSITION | Native 3-2-1-GO, released once everyone's in (or timeout) |
9. event_details before car_select — real screenshots, real reason
Added 2026-08-19, direct request: show the actual native hand-off as two steps, not one, using real captures instead of a drawn placeholder.
Why event_details comes first, not just car_select alone: this isn't a UX preference, it's the same technical constraint already on record in ARCHITECTURE §4b step 5.2 / PROGRESS.md cont.55/57 — TriggerTrueDirectCarSelectJump() needs at least one real event_detail → car_select transition to have happened this session (g_realEventDetailsVisitHappened) before a cold direct jump is reliable. Showing event_details first in the prototype makes that real engine requirement visible in the flow itself, instead of leaving it as an invisible precondition documented only in prose.
The screenshots are real, captured live via adb screencap on the project's own Galaxy A9 test device (com.ea.games.nfs13_mod), not drawn: an ordinary street (МАККЛЕЙН) → an ordinary event (ПОБУДКА, everyday street race) → its EventDetails screen → continue → car_select showing whatever car happened to be equipped (Subaru Cosworth Impreza STI CS400). Deliberately not the project's own synthetic "LAN" event — per the request, any real street/event/car works, and using a genuinely ordinary one keeps the mockup honest about what the native screens actually look like today, unmodified.
Both screenshots are embedded directly in LOBBY_PROTOTYPE.html (inline data: JPEGs, cropped to drop the on-screen nav bar) — the prototype has zero external image dependencies.
On-device viewing: LOBBY_PROTOTYPE.html?kiosk=1 hides the review-tool chrome (masthead, screen rail, notes panel) and lets the phone-frame content fill the real viewport — this is the mode to use when checking the mockup on an actual phone via adb, not the full review page.