# 03 — Behavioral Linkage Recon (BN Online)

Reconstructed from extracted GML control flow. Trees:
`extracted/client-5-8/` (client) and `extracted/server-5-4/` (the 39dll Master/server).
Citations are `file:line`. Behaviors flagged **CLIENT** / **SERVER** per the
networking split. Guesses are explicitly labeled.

## Architecture invariant (read this first)

The Master is a **thin relay + state store**, not a simulation authority. It
receives typed messages, mutates `global.p_*` / `global.u_*` arrays, and
re-broadcasts. Almost every *gameplay rule* below (teleport gating, fall
probability, hexsling redirection, duo formation, summon zone-check) is computed
**on the client**; the server only persists the resulting state and forwards it.

Server message dispatch: `server-5-4/scripts/0359-server_receive.gml` — a
`switch(messageid)` over IDs 0..26. Relevant cases:
- `case 1` room change (sets `global.p_room`, and `global.dabypass` — see Fall)
- `case 3` movement (x,y,fps,xdif,ydif) — **client-authored**, server just stores+rebroadcasts (`:142-148`)
- `case 5` login/auth (argon2 N/A in legacy; plaintext pwd compare `:195`)
- `case 8`/`case 21` persist inventory / hexbridges
- `case 15` summon/join/duo request relay
- `case 16` area-name relay
- `case 22` duo & watchable flags
- `case 23` "digger fell" duo-net ping

Zone numbering (one room per zone): `client-5-8/scripts/0336-room_zone.gml`,
`0326-zone_room.gml`, `0328-zone_name.gml`. Set per-room in room creation code,
e.g. `rooms/0058-BNCentral/creation-code.gml:30 → global.zone = 10`.
**Zone 3 = Emptiness' Hull (free-teleport zone); Zone 4 = Digital Abyss
(no-teleport zone).** These two magic numbers drive most gates below.

---

## 1. Portal fast-travel + home portal + login spawn

**Acquiring Link Data (the "portal" you stand on).** Object `0058-hometele`
(despite the name, this is a *link point / portal that grants Zone Link Data*).
- `Create.gml:2` builds the data string:
  `datastring = "1|1]" + name + "|2]" + tname + "|3]" + string(global.zone) + "|4]0|5]0"`.
  Fields decode via `iv_ldatainfo`: [0]=isMainZoneLink, [2]=area name, [3]=zone#,
  [4]=x, [5]=y.
- `KeyPress-35.gml` (the "use" key = GM keycode 35) while `scol` (standing on it):
  `iv_additem(3,datastring)` — inventory **category 3 = Zone Link Data**
  (`:13`), prints "Zone link data … acquired".

**Opening the menu + selecting Link Data → teleport.** The inventory action
table is built in `client-5-8/scripts/0308-iv_setactions.gml` (`argument0==3`
branch, `:88`). The "Teleport" action body (`:107-156`) is the **gate**:

```
if(iv_zonesearch(destZone) > 0                       // hold DEST zone link data
   && (iv_zonesearch(global.zone) > 0                // AND hold CURRENT zone link data
       || global.zone == 3                           //     OR in Emptiness' Hull (free)
       || server.gportal)                            //     OR standing on a portal/linkpoint
   && (iv_itemsearch(1,'2') || server.gportal)       // AND have RmtPlugn item '2' OR on a portal
   && global.zone != 4                               // AND not in the Digital Abyss
   && global.duopid == -1)                           // AND not in a duo
{ global.jid = 100; ... server.sprite_index = TeleOut; }
```

`iv_zonesearch` (`scripts/0330-iv_zonesearch.gml`) returns >0 iff the inventory
holds a **main** zone-link (`iv_ldatainfo(...,0)` true) for that zone number.
`server.gportal` is a boolean meaning "currently on a portal": set by the local
avatar colliding with a linkpoint (`server/events/Collision-427.gml:2`) or a
hometele (`Collision-58.gml:2`), cleared 2 steps later by `Alarm-5.gml:2`.

**The "fast-travel from anywhere only to home" rule lives in the item gate, by
composition — there is no single flag:**
- *From anywhere* requires the **RmtPlugn expansion (`iv_itemsearch(1,'2')`)**
  AND main link data for both your current zone and the destination
  (`iv_setactions.gml:109-110`). Failure messages at `:138-154` spell this out
  ("Without a navi expansion … You must be at a central area portal or a link
  point to use link data").
- *Home* is special because it is the **login spawn**, applied
  unconditionally without any of the above checks (see below). The only
  always-available "go home" path is `\`goto ocs` → the OCS, then the OCS auto-
  teleports you home.

**Setting home.** Same script, `:157-182`: action "Set As Home" →
`global.homedata = iv_itemsearch(3, curdata); save_settings();`. `homedata` is an
index into inventory category 3. "Reset Home" sets it to `-1` (= OCS).
Persisted client-side in `save_settings.gml:25` (`homedata+1`, so default -1 →
0).

**Login spawn = home portal.** The Online Command Screen object
`0045-commandob/Create.gml:25-39`:
```
if(global.homedata != -1 && !global.gonehome && !global.dabypass){
  global.jid = 100;
  global.p_room[100] = zone_room(iv_ldatainfo(itemdb[3,homedata],3));
  global.p_area[100] = iv_ldatainfo(...,2);
  p_x[100]/p_y[100] = saved coords (or -1 → spawn at the hometele instance);
  alarm[0] = 15;          // Alarm.gml:2-3 → online_room(p_room[100])
}
```
`jid == 100` is the "home/self-teleport" sentinel; the landing handler is the
local avatar's `0000-server/Alarm-1.gml`. On login `begin_client_receive.gml:104-110`
re-validates `homedata` (resets to -1 if you no longer hold that zone's link data).

> **CLIENT.** The entire teleport gate, home selection, and login spawn run on
> the client. The server only learns about it via a `case 1` room-change message
> afterward and re-broadcasts presence.

---

## 2. Fall handling + Digital Abyss

**Where falling is detected.** Local avatar `0000-server/events/Step.gml:165-205`.
After movement resolution, if `tcollide == 0` (no floor under the navi) and the
sprite isn't already mid-transport and the avatar is visible and not a jokershell:

```
rand = random(99);                       // :168
global.dafalls += 1;                      // running fall counter
global.jid = 99;                          // 99 = self-fall sentinel
p_x/p_y[99] = xstart/ystart;
if(ceil(rand) > 9 || global.dafalls <= 5) // ~90% safe, AND first 5 falls always safe
{   // → respawn in same room/area (effectively "return toward home/checkpoint")
    duojoin = (duopid != -1 && duorole) ? 1 : 0;
    p_area[99] = global.area; p_room[99] = room;
}
else                                      // the slim Abyss branch, ~10%
{
    if(duopid == -1 || !duorole){         // solo (or you're the spotter)
        global.dafalls = 0;
        p_area[99] = "Abyssal Ruin";       // ← Abyss destination AREA
        p_room[99] = Digital_Abyss;        // ← Abyss destination ROOM (zone 4)
    } else {                               // you're the digger in a duo → Duo Net saves you
        duojoin = 2; p_area[99]=area; p_room[99]=room;
        caddline("…fatal fall. Duo Net utilized!");
        writebyte(23); writebyte(duopid);  // ping spotter (server case 23)
    }
}
sprite_index = JoinOut;                   // play the warp-out animation
```

**The probability/threshold (exact):** on each fall, `rand = random(99)` (0..99
float). The Abyss is taken only when `ceil(rand) <= 9` **AND** `global.dafalls > 5`.
So it is roughly a **1-in-10 (~10%) chance**, but **suppressed for your first 5
falls** (the counter resets to 0 whenever the Abyss actually triggers). Net: you
cannot hit the Abyss until at least your 6th cumulative fall, then ~10% per fall.
**Abyss destination = room `Digital_Abyss`, area `"Abyssal Ruin"`.**

**The "saved / not so lucky" messaging + actual relocation.** Local avatar
`0000-server/Alarm-1.gml` (the landing handler for jid 99/100):
- `:2-17`: if you ended up *not* in the Abyss → "Saved from the digital abyss.
  You might not be so lucky next time!"; if you *did* land in the Abyss → it
  collapses all pending requests and prints "You have fallen into The Digital
  Abyss. You were not so lucky this time."
- `:19-22`: if target room ≠ current room, calls `online_room(...)` (room swap),
  else repositions in-place. `:29-33`: if `p_x == -1`, spawn at the
  `hometele` instance (the home-portal anchor).

**Server side of fall:** purely reactive. When the client changes into the
Abyss room it sends `case 1`; the server sets `global.dabypass[uid]=1` when
`room==49` (`server_receive.gml:72`) so that on the *next* login the client is
sent straight to the Abyss instead of home (`commandob/Create.gml:42-46`:
`if(global.dabypass){ gonehome=1; online_room(Digital_Abyss); }`). This is the
"you stay stuck in the Abyss until you climb out" hook.

> **CLIENT** decides the fall and rolls the dice; **SERVER** only persists the
> "you are currently in the Abyss" sticky bit (`dabypass`) and relays the
> duo-net ping (`case 23`).

---

## 3. Hexport (6-orb hexagon, straight-line, under-floor travel)

**Entering.** Hexporters are directional terminals (`0347-hexporter_d`,
`_u/_l/_r`, and the omni `0414-hexporter_any`). All require the navi-expansion
item `iv_itemsearch(1,"1")` (the HxpPlugn) — else
"A navi expansion is necessary to interface with this object"
(`hexporter_d/KeyRelease-35.gml:18`). On use within 14px:
```
server.fspeed = 0; server.sprite_index = HexportIn; server.x/y = terminal; hxpdir = <dir>;
```
For `hexporter_any/KeyRelease-35.gml:8-23` the launch direction is chosen from the
navi's current `direction` matched against the terminal's open exits
(`up/down/left/right` booleans); if your facing isn't an open exit it sets
`hxpdir = 5` (invalid → no launch).

**The orb / hexagon + under-floor depth.** The local avatar's `Step.gml:1-24`
manages depth while `sprite_index == Hexport`: it normally sits at
`depth_set(6,23)` (a *low/under* layer) so the hexagon-of-light glides **beneath
floor tiles**, but pops up to layer 4 when overlapping a hexporter-tile-side
(`hxtsideb`) so it surfaces at terminals (`:16-23`). Hexporting speed ramps:
`Step.gml:28-31` `fspeed += 0.2` up to a cap of 8. The HexportMask is used as the
collision mask (`:23`).

**Straight-line motion + redirection at hexslings.** Motion is pure
`direction`/`fspeed`. Redirection happens when the orb collides with a hexsling
stop-tile `0417-hxstsideb/Collision-0.gml`:
```
if(Hexport && !hxdeny && fspeed!=0 && !keywait){
  fspeed=0; snap to tile;
  if(up+down+left+right <= 1){          // 0 or 1 open exits → auto-resolve
     set hxpdir from whichever single exit is open; hxdeny=1; alarm[2]=10;
  } else keywait = 1;                    // multiple exits → wait for arrow key
}
```
With `keywait` set, the player's arrow keys pick the exit
(`hxstsideb/KeyPress-37..40.gml` → set `hxpdir` 2/0/1/3). After `alarm[2]`
fires, `0000-server/Alarm-2.gml:2-27` converts `hxpdir` (0/1/2/3) into
`direction` (90/270/180/0) and resumes at `fspeed = 0.2`.

**"Prefers the direction the player wasn't traveling."** The clearest
implementation of this is the **hexbridge bounce** (next section,
`hxbtsideb/Collision-0.gml:8-14`): when there are **no** open exits it flips the
travel axis (1↔0, 3↔2) — i.e. sends the orb back the way it came. For plain
hexslings I did **not** find an explicit "opposite-of-travel default"; multi-exit
slings *wait for input* (`keywait`) and single-exit slings take the only exit.
**(Flagged: the "prefers the direction you weren't traveling" phrasing maps to
the hexbridge reversal, not a hexsling default — see Open Questions.)**

**Exit.** When `hxdeny` is false at `Alarm-2`, the orb plays `HexportOut`
(`Alarm-2.gml:31`) and `0042-player`/`0000-server` `Other-7` reassembles the navi
(`HexportOut → NaviStandD`, nudging x+=… y-=23).

> **CLIENT** drives all hexport motion, redirection, and depth. The orb position
> is shipped to peers as ordinary movement (`case 3`); peers render the
> `Hexport` sprite under their own floor tiles via the same `player` depth code.

---

## 4. Hexbridge bounce-back + persistent floor spawn

Object `0444-hexbridge` + its bottom-side `0445-hxbtsideb`. A hexbridge is an
initially-empty gap that, once a hexorb bounces off it, becomes a **permanent
solid floor tile for that player**.

**Per-player activation state** is `global.hxbridge[]` — a flat list where index
0 is the count and 1..N are bridge-ID strings. The **bridge ID is positional**:
`bridgeid = string(x) + room_get_name(room) + string(y)`
(`hexbridge/Create.gml:6`). On room entry, `Create.gml:7-13`:
```
if(hxb_search(bridgeid) > 0){ sprite_index=HexbridgePlat; mask_index=Tile1; got=1; }
```
i.e. if you've previously activated this bridge it spawns already-solid.
`hxb_search` (`scripts/0348-hxb_search.gml`) is a linear scan of `global.hxbridge[]`.

**Bounce + activation** in `hxbtsideb/Collision-0.gml`:
```
if(Hexport && !hxdeny && fspeed!=0){
  fspeed=0; snap;
  if(up+down+left+right == 0){           // dead end → BOUNCE BACK
    flip hxpdir: 1↔0, 0↔1, 3↔2, 2↔3;
  } else { take the open exit; }
  hxdeny=1; alarm[2]=15;
  if(!got){                              // first time: PERSIST the bridge
    global.hxbridge[0]+=1;
    global.hxbridge[count]=bridgeid; got=1;
    writebyte(21); writeint(count); for each: writestring(bridgeid);
    sendmessage(s_tcpsocket);            // → server case 21 stores it
  }
}
```
So the same collision that reverses the orb also records the bridge and
sends the whole hexbridge list to the server.

**Server persistence:** `server_receive.gml case 21 (:471-474)` overwrites
`global.hxbridge[uid][]` from the message; it is written back to the client on
login (`case 5 :218-222`, "Write their activated hexbridges"). Backed up via
`uhxb_backup/uhxb_restore` scripts (`server-5-4/scripts/0383-0385`).

> **CLIENT** computes the bounce and the bridge ID; **SERVER** stores the
> activated-bridge list per user and replays it at next login. Bridges are thus
> permanent and per-account.

---

## 5. Duo system (DuoPlugn)

**Eligibility & advertising.** Holding key item `'5'` (DuoPlugn) sets
`global.canduo=1` and tells the server (`begin_client_receive.gml:96-103`,
server `case 22 bytetemp==0`). The server tracks three per-player flags it just
relays: `p_canduo`, `p_induo`, `p_watchable` (`server_receive.gml case 22`,
broadcast in `allupdate[7]`).

**Proximity targeting.** `0000-server/Step.gml:248-262`: each step, if you can
duo and aren't already in one/watching, scan all peers in-room within 42px for a
candidate (`p_watchable` or `p_canduo && !p_induo && canduo`) and store the
closest as `global.potenpid`. That's the name that appears bottom-right.

**Forming a duo.** Two roles: spotter / digger. Request scripts:
- `0359-rq_spot.gml` (you spot) → sends `case15` subtype 7, locally `rq_new(pid,8)`.
- `0360-rq_duodig.gml` (you dig) → subtype 9, `rq_new(pid,10)`.
When the matching opposite request exists, `rq_new` (`scripts/0272-rq_new.gml`,
the duo-confirm branches `:120-154`) returns 1 ("joined"); the client then sets
`global.duopid`, `global.duorole`, and notifies the server `case22 bytetemp==1`
(`p_induo`). `end_duo.gml` clears `duopid` and unsets `p_induo`. The
`client_receive.gml case 15` subtypes 7/9 (`:305-343`) handle the inbound half.

**Mutual benefits** (authoritatively listed in the in-game help text,
`scripts/0347-iv_keyactions.gml:88-102`, DuoPlugn "Benefits"):
- Summon/join **auto-accept** between the pair.
- Summon/join **succeeds regardless of zone-link-data acquisition** (the bypass —
  see Flow 5).
- Pair can chat **unhindered by distance** (normal whisper/proximity limits
  waived).
- Spotter may `\`watch` the digger and pan the view with arrow keys
  (`0567-watcher` object).
- When the digger takes a **fatal fall, they instantly Join the spotter**
  instead of dying (Duo Net — see Flow 2, `Step.gml:191-201` and
  `client_receive.gml case 23 :414-427`).
- Where the Abyss would break up the duo, the pair is sent to the normal
  post-fall area together instead.

**"Duo partners always see each other (override hidden-tile invisibility)."**
I could **not** find a hidden-tile visibility override keyed on `duopid` in
client-5-8 (see Flow 7 + Open Questions). The duo *does* override distance for
chat and proximity gating, and the spotter-watch camera follows the digger
regardless of tiles, which is the closest behavioral match I can evidence.

> Duo formation is **CLIENT-negotiated** via relayed requests; the **SERVER**
> only stores `p_induo`/`p_canduo`/`p_watchable` and forwards the duo-net ping.

---

## 6. Summon / Join (player-to-player teleport) + zone-link gate + duo bypass

**Summon** (`scripts/0280-rq_summon.gml`) = pull a peer to you.
**Join** (`scripts/0279-rq_join.gml`) = teleport yourself to a peer.

The **zone-link-data gate** is the opening `if` of each:

```
// Summon (checks YOUR zone):
if(p_room[target] != OCS &&
   (iv_zonesearch(global.zone) > 0 || global.zone == 3 || target == global.duopid))

// Join (checks the TARGET's zone):
if(p_room[target] != OCS &&
   (iv_zonesearch(room_zone(p_room[target])) > 0 || global.zone == 3 || target == global.duopid))
```

So a summon/join is **rejected unless** you hold the main Zone Link Data for the
relevant zone (`iv_zonesearch(...) > 0`), OR the zone is the free zone 3
(Emptiness' Hull), OR **the target is your duo partner** (`target == global.duopid`)
— that last clause is the **duo bypass**. On rejection it prints e.g.
"Unable to summon … without the main link data for this zone." (`rq_summon.gml:30`)
or the join equivalent (`rq_join.gml:25`). Targets in their OCS are always
refused (`:24-27` / `:19-22`).

On success it sends `case 15` (subtype 0 summon / 1 join) to the server which
just relays it to the other player (`server_receive.gml case15 :370-386`), and
locally calls `rq_new` to register the pending request. The actual relocation is
the same `jid`/`JoinOut`/`JoinIn` machinery as teleport, plus `rq_areasend`
(`scripts/0278-rq_areasend.gml`) to sync the destination area name.

Duo auto-accept: in `rq_new.gml`, the summon/join confirm branches treat
`argument0 == global.duopid` as an immediate accept (`:51`, `:76-90`) — no manual
`\`accept` needed.

> **CLIENT** enforces the zone-link gate and the duo bypass entirely; the
> **SERVER** blindly relays the request (it never re-checks link data — a client
> could in principle bypass this, a parity/anti-cheat note for the rebuild).

---

## 7. Area transition markers (subdividing one zone-room into Areas)

Each zone is a **single GameMaker room** (Flow Architecture note). Named "Areas"
within it are delimited by invisible trigger objects with the `ac_` prefix
("area change"), e.g. `0123-ac_abyssalpathway`, `0446-ac_dfalls`,
`0565-ac_hiddengrove`, … (≈200 of them).

Mechanism: the object's `meta.json` has `"visible": false`, `"solid": false`,
and a `maskId` (`ac_abyssalpathway/meta.json:8-11`). Its only event is
`Collision-0` with the local avatar, whose entire body is one call:
`change_area("Abyssal Pathway")` (`ac_abyssalpathway/Collision-0.gml:2`).

`change_area` (`scripts/0117-change_area.gml`):
```
if(string(global.area) != argument0 && is_string(argument0)){
  if(!global.parea){ global.area = argument0; chatob.ara=0; chatob.carea=1; chatob.alarm[2]=0; }
  global.prearea = argument0;
}
```
So walking your navi's mask over an `ac_*` region updates `global.area` (the
display/label + chat scope), retriggers the area-name banner via `chatob`, and
remembers `prearea`. When you summon/are summoned, `rq_areasend` ships
`global.area`/`prearea` to the peer (server `case 16`) so they spawn into the
right sub-area label.

> **CLIENT.** Pure local collision; the server only stores/relays the area
> *string* (`p_area`, `case 16`) for cross-player display and respawn.

---

## 8. Hidden-tile visibility (a navi on hidden tiles invisible to navis not on the same contiguous body)

**What I found.** Object `0121-hiddentile` (sprite `HiddenTile` + `HiddenTileTop`
overlay). It behaves as **floor for collision** purposes — `tbottomcheck.gml:1`
and `tileborder.gml:4` treat `hiddentile` like solid ground, and the fall check
(Flow 2) does not fire while standing on it. There is a client render toggle
`global.alphaon` (object `0118-alphaq`, `scripts/0004-CmdRec.gml:26-34`) that
draws hidden tiles at 50% alpha so **you** can see where they are
(`hiddentile/Draw.gml:4`). Moving-platform hidden variants exist
(`0333-mplath_lr_r`, `0335-mplath_ud_u`).

**What I could NOT find:** any code that makes a *remote* navi invisible based on
hidden-tile occupancy, and no flood-fill / "contiguous body" computation. The
`player` object's `Draw.gml` draws unconditionally (no hidden-tile/`visible`
gate), and `0000-server/Step.gml` never toggles peer visibility for hidden
tiles. The only per-peer `visible=0` paths are teleport-out
(`player/Other-7.gml:15`, `:28`) and off-room.

**Assessment (flagged as a gap, not a guess):** in extracted **client-5-8**, the
"hidden tiles hide other navis from those not on the same contiguous hidden body"
rule appears **unimplemented or stripped** — hidden tiles function as
secret/visually-cloaked floor, but I see no visibility-culling-by-shared-body
logic. The "contiguous body" determination is therefore undetermined from this
build. (See Open Questions.)

---

## Open questions / couldn't find

1. **Hidden-tile peer invisibility + contiguous-body algorithm (Flow 7).** Not
   present in client-5-8 GML that I traced. Either (a) it was never implemented
   in this revision, (b) it lives in an older/newer revision under
   `legacy/source-archive/`, or (c) it is purely a *self*-cloak (you appear
   hidden, enforced by the watcher/proximity rules) rather than a true
   per-body visibility cull. Recommend grepping the source-archive revisions for
   `HiddenTile` + `visible`/flood-fill before designing the rebuild.

2. **Hexsling "prefers the direction you weren't traveling" (Flow 3).** The only
   explicit direction-reversal I found is the **hexbridge** dead-end bounce
   (`hxbtsideb/Collision-0.gml:8-14`). Plain hexslings with multiple exits wait
   for arrow input (`keywait`); single-exit slings auto-take the lone exit. If a
   true "default = opposite of travel" exists it may be in a hexsling variant I
   didn't open (there are directional hexporter siblings `_u/_l/_r`), or it is a
   description of the bounce behavior. Worth confirming against `hexsling_any`
   plus any `hexsling_*` directional variants.

3. **`global.jid` sentinel map.** Confirmed 99=self-fall, 100=home/self-teleport,
   `-1`=idle; other values are inbound peer PIDs (Join/Summon). The exact
   semantics of `jid == argument0 (a PID)` vs `100` in `Alarm-1.gml:27`
   (`if(global.jid != 100) JoinIn else TeleIn`) are inferred, not spelled out.

4. **Server trust.** The server never re-validates the zone-link gate, the fall
   roll, or the hexbridge IDs — it trusts client messages (`case 3` movement,
   `case 8` inventory, `case 21` hexbridges, `case 15` summon). This is the
   opposite of REBNO's server-authoritative target and is the single biggest
   parity/anti-cheat divergence to flag for Phase 4+.

5. **`dabypass` array indexing.** Set as `global.dabypass[uid]` on the server
   (`server_receive.gml:72`) but read as scalar `global.dabypass` on the client
   (`commandob/Create.gml:42`); the server writes the per-login byte into the
   `case 5` auth reply (`:206`). Consistent across the wire, but the dual
   scalar/array spelling is worth noting when porting.
