---
mvp: yes
subsystem: account-auth
---

# Account & Authentication

The BNO 5-4 server holds player accounts as a flat text file on disk. Login = file read; create-account = append; password check = string-equality. There is **no hashing, no salting, no rate limiting, no transport encryption**. Per [CLAUDE.md hard rule #2](../../CLAUDE.md), the rebuild does NOT faithfully port any of this — argon2id from packet 1 is the contract. This document narrates the original mechanic so Phase 4 (SRV-09 / SRV-10 / SRV-11) and Phase 5 (RESTORE) know exactly which pre-existing data shape they have to migrate.

## Source files

| File | Purpose | Format reference |
|------|---------|------------------|
| `localList.txt` | Cold-start authoritative user table — name + plaintext password line pairs | [save-formats.md](./save-formats.md) (text-delimited) |
| `User_DBUpdated.bnu` | Hot user-table; produced by `users_restore` migration; same shape as `localList.txt` | [save-formats.md](./save-formats.md) |
| `MSettings.bno` | Server-wide settings (server message, latest-client version, alpha-on flag, server playername) — read by 0386-load_settings | [save-formats.md](./save-formats.md) |

The wiki errata-corrected reference is [decomp/wiki/16-bno-bnb-notes.md](../../decomp/wiki/16-bno-bnb-notes.md) — which after Phase 3 plan 01 confirms these are `file_text_*` text files, NOT binary `file_bin_*` streams.

## Login flow (server side, opcode 0)

`extracted/server-5-4/scripts/0359-server_receive.gml` case 0 handles initial username announcement after a TCP connection succeeds. The companion login-response broadcast is opcode 8 (s2c). See [packet-protocol.md](./packet-protocol.md) for byte-level frames and [./protocol.md](./protocol.md) for the canonical opcode table emitted by Phase 3 plan 02.

```gml
// 0359-server_receive.gml — case 0 (login)
case 0:
  /*global.p_name[pid]*/tempstr = readstring(); //p_name is set in case 5.
  tempstr = global.p_name[pid] + " (PID #" + string(pid) + ") has connected to Battle Network Online.";
  if(global.p_tcprec[pid]) tempstr += " (TCP-Rec)";
  dynamicaddline(tempstr);
  allupdate[0] = 1;
break;
```

The actual credential validation is implicit: case 5 (`server_receive` line ~172) is what assigns `global.p_name[pid]` after looking the supplied username up against the in-memory user table populated from `localList.txt` / `User_DBUpdated.bnu`. There is no separate auth opcode in the original — credentials ride alongside the room-change handshake.

## User-init lifecycle

A new socket is accepted in `0358-join_monitor.gml`:

```gml
// 0358-join_monitor.gml
joincheck = tcpaccept(global.joinsocket, true);
if(joincheck <= 0) exit;
setnagle(joincheck,1);
// Find the lowest unused PID slot and bind it
for(i = 0; i < global.pindex; i += 1) if(!global.p_online[i]) break;
if(i == global.pindex) global.pindex += 1;
init_user(i,joincheck);
```

`0360-init_user.gml` initialises every per-player global (`global.p_id`, `global.p_tcpsocket`, `global.p_ip`, `global.p_name = "UNDEFINED"`, default sprite, default room = `Online_Command_Screen`, etc.). Symmetric `0362-uninit_user.gml` clears these on disconnect; `0363-reinit_userr.gml` resets a slot for re-use after a clean logout.

## Session restore from `localList.txt`

`0392-users_load.gml` reads the cold-start file:

```gml
// 0392-users_load.gml
userfile = file_text_open_read("localList.txt");
lu = 0;
while(!file_text_eof(userfile))
{
  nextname = file_text_read_string(userfile);
  if(nextname != "")
  {
    global.u_name[lu] = nextname
    file_text_readln(userfile);
    global.u_pwd[lu] = file_text_read_string(userfile);
    file_text_readln(userfile);
    lu += 1;
  }
}
global.u_total = lu;
file_text_close(userfile);
```

`0367-users_restore.gml` is the migration variant — same shape, but reads `User_DBUpdated.bnu` (if present) and **deletes the file after read** so the next cold-start falls through to `localList.txt`. The "DBUpdated" name is a vestigial marker from a partial DB-migration attempt that never landed.

Both scripts populate `global.u_name[i]` and `global.u_pwd[i]` parallel arrays. Plaintext. Always.

## Plaintext credentials staging path (rebuild migration)

This is the security-critical contract for Phase 4 (SRV-10 + SRV-11) and Phase 5 (RESTORE.md):

> **Plaintext credentials from `localList.txt` land in `legacy_credentials_staging` (see `./tables.ts`); read-once-then-purge by Phase 4 SRV-10 / SRV-11. Production `accounts` table NEVER holds plaintext per [CLAUDE.md](../../CLAUDE.md) hard rule #2.**

The migration sequence (forward-link [../adr/0002-persistence-layer.md](../adr/0002-persistence-layer.md), lands in plan 03-07):

1. SRV-10 reads `legacy/servers/*/localList.txt` line-pairs, writes each row into `legacy_credentials_staging(username, plaintext_pwd, imported_at)` ONCE.
2. On first login of a migrated user, SRV-11 verifies plaintext-equality against the staging row, hashes with argon2id, writes the hash to the `accounts` table, **deletes the staging row in the same transaction**.
3. After all migrated users have logged in once (or after the migration grace period), the `legacy_credentials_staging` table is dropped entirely.
4. The on-disk `localList.txt` archive is **read-only / never re-imported**. New accounts are argon2id from packet 1.

The staging table is a security incident in transit, by design — it exists for as short a window as possible and only to allow existing players to log in once with their old password before being upgraded. See [.planning/codebase/CONCERNS.md](../../.planning/codebase/CONCERNS.md) for the broader risk register.

## Logout

`0359-server_receive.gml` does NOT have a dedicated logout opcode. Detection is socket-level: when `receivemessage()` returns 0 bytes from a player's TCP socket, the dispatcher calls `uninit_user(upid)`. From `0349-operations.gml`:

```gml
// operations step event — disconnect detection
if(size == 0 && global.p_online[upid])
{
  debug_log("Uninit PID " + string(upid));
  caddline(global.p_name[upid] + " has logged out.");
  allupdate[5] = upid+1;
  uninit_user(upid);
  if(global.pindex == 0) break;
}
```

The s2c "user log on/off" broadcast (opcode 5 in `0359-server_receive.gml`) propagates this to remaining clients.

## See also

- [./protocol.md](./protocol.md) — opcode table (login = 0; user log on/off = 5; login-response = 8)
- [./save-formats.md](./save-formats.md) — `User_DBUpdated.bnu`, `localList.txt`, `MSettings.bno` byte grammars
- [./packet-protocol.md](./packet-protocol.md) — 39dll wire framing for these opcodes
- [./persistence.md](./persistence.md) — write cadence + crash-loss surface for the on-disk user table
- [../../decomp/wiki/16-bno-bnb-notes.md](../../decomp/wiki/16-bno-bnb-notes.md) — errata-corrected save format reference
- [../adr/0002-persistence-layer.md](../adr/0002-persistence-layer.md) — forward-link; lands in plan 03-07 (Litestream / staging table contract)
- [CLAUDE.md hard rule #2](../../CLAUDE.md) — "No faithful port of plaintext passwords. argon2id from packet 1."

## Scripts referenced in this subsystem

<!-- AUTOGEN:scripts:start -->
| Script ID | Name | Lines | Used in objects |
|-----------|------|-------|------------------|
| 0358 | join_monitor | 14 | — |
| 0360 | init_user | 78 | — |
| 0362 | uninit_user | 32 | — |
| 0363 | reinit_userr | 52 | — |
| 0367 | users_restore | 27 | — |
| 0379 | users_restore_old | 22 | — |
| 0391 | users_load_old | 15 | — |
| 0392 | users_load | 21 | — |
| 0393 | ip2pid | 7 | — |
| 0394 | locip2pid | 7 | — |
<!-- AUTOGEN:scripts:end -->

## Objects referenced in this subsystem

<!-- AUTOGEN:objects:start -->
| Object ID | Name | Sprite | Mask | Events |
|-----------|------|--------|------|--------|
<!-- AUTOGEN:objects:end -->

## Engine functions used

<!-- AUTOGEN:gml-functions:start -->
| GML function | Call sites | Sample script | Wiki link |
|--------------|------------|---------------|-----------|
| `users_restore` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `users_load` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `init_user` | 1 | 0358-join_monitor | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `uninit_user` | 1 | 0359-server_receive | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
| `reinit_userr` | 0 | — | [wiki/07-gml-core-functions](../../decomp/wiki/07-gml-core-functions.md) |
<!-- AUTOGEN:gml-functions:end -->
