docs: bring living project documentation under version control

ANALYSIS.md/ARCHITECTURE.md/PROGRESS.md and the rest of this project's
living docs have always lived one directory above this repo's root
(NFSMW_Online_Claude_workdir/*.md), so they were never actually part of
this git history despite being the authoritative record of every hook,
offset, and RE finding this branch's code is built on.

Mirrors the docs/ layout already used on native-arm32-trace-harness so
both branches reference the same file set by name, pending the actual
code merge (see PROGRESS.md's own "Repo merge pending" note).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-22 23:48:32 +03:00
co-authored by Claude
parent 239a9a6346
commit 944aa68b96
9 changed files with 8070 additions and 0 deletions
+200
View File
@@ -0,0 +1,200 @@
# 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`](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 `LobbyPlayer` to 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_OFFSET` is 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 chosen `carId`'s class against `classRestriction` the moment `onCarSelected` fires; 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 `165535`.
- **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 when `ready == Ready` — and always reopens native car_select. Changing car always resets `ready → NotReady` automatically, 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 PAINT1PAINT6 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 `colorHex` data (from `onCarSelected`, proven), just its own unambiguous element.
- **Upgrades are shown too**, using the exact `mods: [int|null, int|null]` shape already in the `Lobby`/`LobbyPlayer` schema (§2) and already flowing live off `onUpgradesAccepted` (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 (`ModType` id → 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 212, arbitrary bounds — just enough to make the control meaningfully bounded, not a real design decision).
- **Waiting room** shows a live `👤 N/max` pill next to the class tag, turning the same warning colour used elsewhere (`--danger`) once the room is full.
- **Server browser** cards show `current/max` instead 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 `InRaceState` construction (RTTI-confirmed, ANALYSIS.md §3.1) or the final event in `RaceLoaderTask_DispatchInitialFSMEvents` (ANALYSIS.md §6b stage 8) — either is a real, already-located point, just not yet wired for this purpose. `RaceLoaderTask_ExecuteLoadSequence`'s own `SetLoadProgress(this, N)` calls (ANALYSIS.md §6a slot 4, fractions 0.10.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.