# Legacy Recon 06 — Player-to-Player REQUEST system, WATCH system, DUO system

**Scope:** the unified `rq_*` request framework (Summon / Join / friend-Add / duo Spot-Dig),
the Watch (spectator) system, and the Duo system, mapped from the extracted GML.

**Sources of truth**
- Client: `extracted/client-5-8/` (BN Online Client 5-8)
- Server: `extracted/server-5-4/` (BN Online Master 5-4 — extracted dir is named `server-5-4`, not `master-5-4`)

All line citations are from the `.gml` (auto-transcompiled view; `.dnd.json` is the canonical
source but the GML matches it). Anything inferred rather than read is flagged **[INFERENCE]**.

---

## 0. Network message IDs touched by these systems

| msgid | Meaning | Direction | Server role |
|---|---|---|---|
| **15** | Request (sub-byte = request type) | C→S→C | **pure relay** — forwards `[15][senderPID][typeByte]` to target socket |
| **16** | Area-name send (for Summon/Join placement) | C→S→C | stores `p_area[pid]`, relays to one target |
| **22** | Duo/Watch capability flags (sub 0=canduo, 1=induo, 2=watchable) | C→S→all | stores flag, broadcasts to **all** online players |
| **23** | Duo Net (digger fatal-fall rescue ping) | C→S→C | relays `[23][senderPID]` to one target |

Server handlers: `extracted/server-5-4/scripts/0359-server_receive.gml`
case 15 @370, case 16 @388, case 22 @476, case 23 @492.
Client handler: `extracted/client-5-8/scripts/0097-client_receive.gml`
case 15 @256, case 16 @364, case 22 @385, case 23 @414.

**Critical architecture fact:** the server is a *dumb relay* for the entire request system.
It does **no** consent tracking, no request-type validation, no who-watches-whom state. All
pairing/accept/decline/auto-accept logic lives **client-side**. The server only forwards
msgid 15 verbatim and maintains the three broadcast flags (msgid 22).

---

## 1. Request-type enumeration

### 1a. The `rq_new` internal type index (0–10)

Header of `extracted/client-5-8/scripts/0272-rq_new.gml` lines 1–13. These indices are the
**local request-list slot type** (`global.rq_type[]`), NOT the wire byte. They encode both the
type and *who asked* (self vs other), so opposite-direction pairs can cancel/confirm each other.

| idx | Name (rq_new header) | Meaning | UI label (`draw_rqbar`) |
|---|---|---|---|
| 0 | Join from other | A remote navi asked to come to you (they Summon you) | "Go to {name}" |
| 1 | Summon from other | A remote navi asked you to come to them (they Join you) | "Bring {name}" |
| 2 | Join by self | You asked to join them | "Summon {name}..." |
| 3 | Summon by self | You asked to summon them | "Join {name}..." |
| 4 | (Unused locally) | reserved; = "Deny" in wire IDs | — |
| 5 | Add from other | Remote navi requested your friend data | "Befriend {name}" |
| 6 | Add by self | You requested their friend data | "Add {name}" |
| 7 | Spot from other | Remote navi asks you to **spot** for them (they will dig) | "Spot with {name}" |
| 8 | Spot by self | You offered to spot for them | "Spot for {name}..." |
| 9 | Dig from other | Remote navi asks you to **dig** for them (they will spot) | "Dig with {name}" |
| 10 | Dig by self | You offered to dig for them | "Dig for {name}..." |

UI labels: `extracted/client-5-8/scripts/0269-draw_rqbar.gml` lines 69–108. Note the label
wording is "swapped" relative to the index name because the label describes the *action you'd
take to confirm* (idx 0 "Join from other" shows "Go to {name}" — accepting means you go).
"by self" entries get a trailing "..." (lines 110–111) marking them as pending-sent.

### 1b. The wire sub-byte (msgid 15) — DIFFERENT numbering

The byte actually sent on msgid 15 is **not** the rq_new index. Mapping derived from the senders
and the receive handler:

| wire sub-byte | Sent by (client script) | Received as (`client_receive` case 15) | Meaning to recipient |
|---|---|---|---|
| 0 | `rq_summon` writebyte(0) @5 (`0280`) | @263 "has requested your presence" → `rq_new(pid,0)` | You were Summoned |
| 1 | `rq_join` writebyte(1) @6 (`0279`) | @274 "has requested to join you" → `rq_new(pid,1)` | Someone wants to Join you |
| 3 | `rq_decline` writebyte(3) @12 (`0282`) | @346 "has declined the last request" | Decline (consumes your sent rq) |
| 4 | `rq_cancel` writebyte(4) @12 (`0281`) | @354 "has cancelled their last request" | Cancel (consumes a received rq) |
| 5 | `rq_add` writebyte(5) @5 (`0331`) | @286 "has requested your friend data" → `rq_new(pid,5)` | Friend-add request |
| 7 | `rq_spot` writebyte(7) @3 (`0359`) | @305 "requested that you dig for them" → `rq_new(pid,7)` | They want to spot → you dig |
| 9 | `rq_duodig` writebyte(9) @3 (`0360`) | @325 "requested that you spot for them" → `rq_new(pid,9)` | They want to dig → you spot |

The receiver feeds the wire byte straight into `rq_new(senderPID, wireByte)` (@262). Because
`rq_new` is symmetric, the *recipient's* "from other" index equals the *sender's* wire byte for
0/1/5/7/9 — that is why the wire byte set is {0,1,5,7,9} (the even/odd partner indices 2,3,6,8,10
only ever exist locally as "by self").

### 1c. Accept / reject flow per type

The accept handshake is **implicit and symmetric** — there is no explicit "accept" packet.
You "accept" a received request by issuing the *complementary* request back. `rq_new` detects the
matching opposite already in your list and **confirms** instead of queuing a duplicate
(`0272-rq_new.gml`):

- **Join confirm** @48–71: receiving idx-0 while you hold idx-3 (or sender is your duo partner)
  → set `global.jid`, play `JoinOut` sprite → you teleport to them. Returns 1 (confirmed).
- **Summon confirm** @73–100: receiving idx-1 while you hold idx-2 (or duo partner) →
  `rq_areasend(pid)` (msgid 16) so they can place you, optional msgid-15 sub-0 nudge if duo.
- **Add confirm** @102–118: idx-5↔6 → trade friend data (no teleport). `0331-rq_add.gml`
  @14–24 writes the UID|name into inventory category 2 on confirm.
- **Spot/Dig confirm** @120–154: idx 7↔10 and 8↔9 → forms a duo (see §3).

Each `rq_new` confirm calls `rq_collapse` (`0271`) to remove the now-satisfied pending entry.

**Consent model:**
- Summon / Join / friend-Add / Spot / Dig between **non-duo** players: **require explicit
  consent** — the second party must run the complementary `rq_*` action. Otherwise the request
  just sits in the requestee's `draw_rqbar` list for 900 steps (30 s @30fps) then times out
  (`0270-rq_timercount.gml`, `global.rq_timer = 900` set in `rq_new` @159).
- **Decline** (`rq_decline`, msgid-15 sub-3): explicitly drops the most-recent *received* request
  and notifies the sender. **Cancel** (`rq_cancel`, sub-4): drops your most-recent *sent* request
  and notifies the target. **Ignore** (`rq_ignore`): silently drops a received request **locally
  only** — no packet sent (`0283-rq_ignore.gml` has no `sendmessage`).
- **Duo-partner auto-accept** @51 & @76: if the sender `argument0 == global.duopid`, Join/Summon
  **auto-confirm with no second consent** — your duo partner can pull/push you freely. This is the
  duo "tether" expressed through the request system.

**Gating / preconditions:**
- Summon/Join refused if target is on their OCS (`Online_Command_Screen`) — `rq_join` @19,
  `rq_summon` @24.
- Join needs the zone's main link data (`iv_zonesearch`) unless target is your duo partner —
  `rq_join` @1. Summon similarly needs link data + `smobilecheck()` (not mid-transport) —
  `rq_summon` @1,14.
- Friend-Add refused if target on OCS — `rq_add` @1.

---

## 2. WATCH (spectator) system — deep dive

Watch lets you slave your **camera** to another navi and follow them around a room. It is
explicitly *not* gated by any Navi Expansion / DuoPlugn (no `canduo` check anywhere in the
watch path).

### 2a. State variables (init: `objects/0034-title/events/Other-2.gml` @32–35)
- `global.potenpid` — nearest eligible target under the proximity cursor (watch *or* duo candidate).
- `global.watchpid` — the navi you've *locked onto* as a watch target (persists even when paused).
- `global.watching` — the navi the camera is *actively* following right now (-1 = not following).
- `global.watx`, `global.waty` — last camera position when you paused watching (for resume range check).
- `global.watchable` — **your own** "may others watch me?" flag (default off; toggled by chat cmd).
- `global.p_watchable[pid]` — per-peer copy of everyone's watchable flag (from msgid-22 broadcast).

### 2b. Becoming watchable (the watched side)
Chat command **`` `watchable ``** toggles `global.watchable` and broadcasts via msgid 22 sub-2
(`0008-ChtCmdRec.gml` @67–85). Command **`` `shake eyes ``** force-clears watchable AND sets the
force-flag byte=1 (`@88–98`), which tells every current spectator to drop you.

Server (case 22 @484–488): stores `p_watchable[pid]`, reads the optional force byte but **does
not act on it** — it just rebroadcasts (`allupdate[7]` @489). The *spectator's* client enforces
the force-drop on receive (case 22 @401–410: if force byte set and they watch you,
`watchpid=-1; watching=-1`, "has cast off all spectators").

### 2c. Starting / locking a watch (the watcher side)
1. **Proximity acquire** — server-obj Step (`objects/0000-server/events/Step.gml` @248–262):
   each step, if not already in a duo/watch, scan all players in the same room within **42 px**;
   the nearest one that is `p_watchable` (or a duo candidate) becomes `global.potenpid`.
2. **Lock on** — **Ctrl+W** (`objects/0000-server/events/KeyPress-87.gml`): if no current
   watch/duo and `potenpid` is watchable → `watchpid = watching = potenpid`. Pressing Ctrl+W
   again while watching clears `watchpid=watching=-1` (toggle off).
3. **Pause/resume** — chat **`` `watch ``** (`0008-ChtCmdRec.gml` @101–134): if `watching!=-1`,
   stash `watx/waty` and set `watching=-1` (pause, keep lock). If paused, resume only if the
   target is within ±320×±240 of your last watch pos *or* of your own avatar (range gate @111–112).

### 2d. What the watcher experiences — the `watcher` object (`objects/0567-watcher/`)
- **Create** (`Create.gml`): `view_object[0] = self.id;` — the **camera is bound to the watcher
  instance**, not to your avatar. So you literally see through a roving camera that chases the
  target. It is a *follow-cam*, NOT see-through-their-eyes and NOT a fog-of-war reveal.
- Spawned by server-obj Step @267–272 when `watching != -1` and no watcher exists yet.
- **Step** (`Step.gml`):
  - Target = `global.p_x/p_y[global.watching]` (@5–6). Camera eases toward it
    (`move_towards_point`, speed scaled by distance @18–26).
  - **Escape rule** @20–24: if target drifts >320 px horiz or >240 px vert from the camera,
    print "{name} has escaped from view." and `instance_destroy()` → watch ends.
  - **Duo pan** @7–17: only if the watcher's `duo` flag is set (spotter watching their digger),
    arrow keys nudge the camera ±160/±120 px to look ahead independently of the target.
- **Destroy** (`Destroy.gml`): saves camera pos into `watx/waty`, sets `watching=-1`, and
  **restores `view_object[0] = server`** (camera snaps back to your own avatar).

### 2e. Watcher's own avatar is frozen while watching
`smobilecheck()` (`0284`) and `mobilecheck()` (`0276`) both return 0 (movement disallowed) when
`global.watching != -1`. So you cannot walk your own navi while actively following someone — you
are rooted in place spectating. (Pausing watch via `` `watch `` frees you to move again.)

### 2f. Directionality & visibility
- **One-way.** Watching is unilateral; the watched player is never told who watches them (no
  packet carries spectator identity — msgid 22 only carries the boolean watchable flag). The
  watched party only has the blunt `` `shake eyes `` to evict *all* unknown spectators.
- Watcher sees exactly what any co-located player sees: it relies on `global.p_x/p_y[watching]`,
  which the server only keeps current for **same-room** peers (position msgid 3 is room-scoped,
  server_receive @570). Once the target leaves the room their position stops updating and the
  escape rule fires. So Watch grants **no extra visibility** (no hidden tiles, no through-walls) —
  purely a camera that follows a same-room navi who has opted in.

---

## 3. DUO system — deep dive

Duo is a tighter two-navi bond for the Spot/Dig activity, gated behind a key item. The CLAUDE
context calls Watch "a more limited form of Duo" — accurate: Duo = mutual + tethered + frozen
free-teleport + rescue net + (for spotter) a duo-flavored watcher with pan controls.

### 3a. Gating — the DuoPlugn key item (#5)
`global.canduo` is set to 1 only when key item #5 ("DuoPlugn") is in inventory:
- On login: `0094-begin_client_receive.gml` @96–97 (`iv_itemsearch(1,"5")`).
- On pickup: `objects/0442-d_key/events/KeyPress-35.gml` @11–14 ("DuoPlugn" → `canduo=1`).
Both then broadcast msgid 22 sub-0 (canduo) so peers learn you can duo (`p_canduo[pid]`).
Watch has **no** such gate.

### 3b. State variables (init Other-2 @32–35; per-peer cleared in `0092-init_user.gml` @10–12)
- `global.duopid` — partner PID (-1 = no duo).
- `global.duorole` — **0 = spotter, 1 = digger** (confirmed by `draw_rqbar` @19–27 and the
  fatal-fall logic). Spotter watches; digger dives.
- `global.p_canduo[pid]` / `global.p_induo[pid]` — peer flags from msgid 22 sub 0 / 1.

### 3c. Formation (via the request system)
Initiated by **Ctrl+S** (offer to spot, `KeyPress-83.gml`) or **Ctrl+D** (offer to dig,
`KeyPress-68.gml`). Preconditions @5: `duopid==-1`, `potenpid` has `p_canduo` and not `p_induo`.
- Ctrl+S → `rq_spot(potenpid)` (`0359`): sends msgid-15 sub-7, queues local idx-8.
- Ctrl+D → `rq_duodig(potenpid)` (`0360`): sends msgid-15 sub-9, queues local idx-10.

When the partner issues the complementary action, `rq_new` confirms (idx 7↔10, 8↔9). On confirm
**both** sides set `global.duopid = partner`, `potenpid = -1`, and **broadcast msgid 22 sub-1
(`induo=1`)** so the rest of the server knows they're occupied (`rq_spot` @14–22,
`rq_duodig` @14–22, and the receive-side confirm at `client_receive` @314–322 / @334–342).

⚠ **Likely legacy bug (flag, not a feature to port):** both `rq_spot.gml` @15 and
`rq_duodig.gml` @15 set `global.duorole = 0` on the *initiator*. The receive-side confirm sets
the role correctly (`client_receive` @315 sets `duorole=1` for the dig-for case, @335 sets
`duorole=0` for spot-for). So the side that *sent* the offer and gets confirmed-on-send always
ends up `duorole=0` (spotter) regardless of whether they pressed Ctrl+S or Ctrl+D. **[INFERENCE]**
on intent — the consistent value strongly suggests a copy-paste oversight; verify against
gameplay before replicating. Do **not** faithfully port the bug.

### 3d. Mutual benefits & rules
1. **Shared follow-cam (spotter only).** When the spotter is watching the digger
   (`global.watching == global.duopid && !global.duorole`), the watcher spawns with `duo=1`
   (server Step @270–271), unlocking arrow-key camera pan (watcher Step @7–17). The duo can also
   start watching via the same `` `watch `` path even without a watchable flag, because the
   `` `watch `` branch @117–130 keys off `duopid` directly. **[INFERENCE]** that this is the
   intended "spotter sees ahead for the digger" mechanic.
2. **Auto-accept Summon/Join.** A duo partner's Summon/Join auto-confirms with no consent prompt
   (`rq_new` @51, @76) — partners freely pull each other across the room.
3. **Tether — no free teleport.** Link-data Teleport from inventory is blocked while
   `duopid != -1`: `0308-iv_setactions.gml` @110 requires `global.duopid == -1`; @132–136 prints
   "Unable to use link data while in a duo." The digger instead **auto-rejoins** the spotter:
   after a teleport/join animation completes, if `duorole` (digger), `duojoin=1` triggers
   `rq_join(global.duopid)` (server Step @175–176, fired in Other-7 @7–8).
4. **Duo Net (digger rescue).** On a fatal fall, a lone navi is dumped to the Digital Abyss
   ("Abyssal Ruin"). But a **digger** in a duo is saved: server Step @191–200 keeps them in-room
   ("Duo Net utilized!") and sends **msgid 23** to the spotter. The spotter receives it
   (`client_receive` case 23 @414–427): "Digger {name} has taken a fatal fall. Duo Net utilized!",
   sets `jid=99`, stops watching, plays `JoinOut` to teleport down to the rescue point. This is the
   headline duo payoff — the spotter literally catches the falling digger.

### 3e. Dissolution
- **Ctrl+S / Ctrl+D while in a duo and within 42 px of partner** → `end_duo()`
  ("Ended the duo with {name}.") — `KeyPress-83/68.gml` @7–11.
- **Changing rooms** → `online_room` (`0095`) @1–5 calls `end_duo()` automatically
  ("The duo with {name} has ended.").
- **Partner drops induo** → `client_receive` case 22 @394–399: if peer's `induo` goes 0 and they
  were your `duopid`, call `end_duo()` ("The duo with {name} has ended.").
- `end_duo()` (`0361`): `duopid=-1`, `watching=-1`, broadcasts msgid 22 sub-1 (`induo=0`).

---

## 4. Client-vs-server responsibility matrix

| Concern | Client | Server (`server-5-4`) |
|---|---|---|
| Request queue / timers / dedupe / confirm logic | **All** (`rq_new`, `rq_collapse`, `rq_timercount`, `draw_rqbar`) | none — relay only |
| Request type validation / consent | **All** (complementary-request handshake) | none (forwards msgid 15 verbatim @370–377) |
| Decline/Cancel routing | sends sub-3/4 | relays to one target |
| Ignore | **local only** (no packet) | n/a |
| Summon/Join placement (area string) | `rq_areasend` → msgid 16 | stores `p_area[pid]`, relays (@388–396) |
| `canduo` / `induo` / `watchable` flags | sets + broadcasts via msgid 22 | stores `p_canduo/p_induo/p_watchable`, **broadcasts to all** (@476–490, @555–564) |
| Who is watching whom | **client only** (`watchpid`/`watching`) | **server never knows** |
| Watcher camera, escape rule, pan, avatar freeze | **All** (`watcher` obj, `mobilecheck`) | none |
| Duo role, tether, teleport block, auto-rejoin | **All** (client logic) | none |
| Duo Net rescue | digger detects fall, sends msgid 23; spotter reacts on receive | relays msgid 23 to one target (@492–498) |
| Anti-cheat on any of the above | none | **none** — fully trust-the-client |

**Porting note for REBNO:** because the legacy server is a pure relay with zero authority over
requests/watch/duo, a server-authoritative rebuild must *invent* this consent + duo-state
machine on the server (it does not exist to copy). The wire shapes (msgid 15/16/22/23 byte
layouts above) are the only server contract that exists today.

---

## Evidence index (file → what it proves)
- `client-5-8/scripts/0272-rq_new.gml` — type enum (header), confirm/cancel handshake, timers.
- `client-5-8/scripts/0269-draw_rqbar.gml` — UI labels + duo/watch HUD, type→string map.
- `client-5-8/scripts/0270-rq_timercount.gml` — 900-step request timeout.
- `client-5-8/scripts/0271-rq_collapse.gml`, `0274-rq_dig.gml`, `0275-rq_find.gml` — list mgmt.
- `client-5-8/scripts/0279-rq_join.gml` / `0280-rq_summon.gml` / `0281-rq_cancel.gml` /
  `0282-rq_decline.gml` / `0283-rq_ignore.gml` / `0331-rq_add.gml` / `0278-rq_areasend.gml` /
  `0359-rq_spot.gml` / `0360-rq_duodig.gml` / `0361-end_duo.gml` — actions + wire bytes.
- `client-5-8/objects/0567-watcher/events/{Create,Step,Destroy}.gml` — follow-cam, escape, pan, restore.
- `client-5-8/objects/0000-server/events/KeyPress-87.gml` (Ctrl+W watch),
  `KeyPress-83.gml` (Ctrl+S spot), `KeyPress-68.gml` (Ctrl+D dig).
- `client-5-8/objects/0000-server/events/Step.gml` @175–205 (fatal fall / Duo Net send),
  @248–272 (proximity potenpid + watcher spawn + duo-pan flag).
- `client-5-8/objects/0000-server/events/Other-7.gml` @7–8 (digger auto-rejoin).
- `client-5-8/scripts/0008-ChtCmdRec.gml` @67–134 (`` `watchable ``/`` `shake eyes ``/`` `watch ``).
- `client-5-8/scripts/0284-smobilecheck.gml`, `0276-mobilecheck.gml` — avatar freeze while watching.
- `client-5-8/scripts/0308-iv_setactions.gml` @109–137 — teleport blocked in duo (tether).
- `client-5-8/scripts/0094-begin_client_receive.gml` @96–103, `objects/0442-d_key/.../KeyPress-35.gml`
  @11–20 — DuoPlugn (#5) gates `canduo`.
- `client-5-8/scripts/0097-client_receive.gml` cases 15/16/22/23 — receive handlers.
- `server-5-4/scripts/0359-server_receive.gml` cases 15/16/22/23 + @555–564 broadcast — relay model.

<!-- legacy reverse-engineering recon; no REQ tag — this is research, not a deliverable artifact -->
