# Alien Fish Exchange - Unity client (demo lobby)

Remake of nGame's 2001 WAP/iTV fish breeding game. This Unity 6 (6000.6) URP project is the
visual demo: a lobby room with a fish tank and a computer desk. Game rules (breeding, economy,
server ticks) are reconstructed in `RULES.md` and `research/fish.json` on the server and are not
wired in yet. Keep simulation separate from presentation so the rules can run on a server tick later.

## Where the code lives (important)

- The source of truth for scripts is on Paul's server: `/home/executeuk/claude/AlienFishExchange/unity/Assets`.
  A server-side Claude session edits there, runs `publish.sh`, and Unity pulls with **AFX > Sync from Server**
  (`Assets/Editor/AFXSync.cs`, from `http://78.110.173.123/afx/`). A Sync overwrites same-named files here,
  so if you edit scripts locally, tell Paul so the server copy can be updated too, or they will be lost on the next Sync.
- The whole scene is generated by **AFX > Build Paper Fish Demo** (`Assets/Editor/AFXDemoSetup.cs`). Change the
  builder, not scene objects by hand, or the next build wipes the change. Never build while Play mode is on.
- Unity MCP is enabled (Edit > Project Settings > AI > Unity MCP Server) for a local Claude session.
- Never run AFX > Sync from Server while Play mode has an emulator on the monitor: the script reload is a domain reload,
  which UnityWebBrowser does not survive ("UWB is shutting down due to incoming domain reload"). Stop Play first.

## What the builder makes

- **Tank** ("AFX Tank"): glass box with frame bars, clear back and bottom glass, no gravel, `AFX/Water` shader surface
  with fish ripples (`WaterRippleManager`), caustics key light (rendering layer 1 only). Sized at build time by
  `SizeTankToStand()` to sit on the TV stand. Clickable close-up (`ViewTarget`).
- **Tank contents**: sand terrain (`research/gen_sand.py` textures) with skirt walls as a separate "Sand Walls" object on rendering
  layer 2 (the caustics key light streaks vertical faces); `AFX/WaterVolume` shader box tints the water by view thickness;
  `BuildTankDecor` places rocks (`Assets/NOT_Lonely`, `Assets/Pizza&Games`) and Splash of Color plants (alpha-clip via
  `MakeCutout`, tinted copies in `Assets/Materials/Plant_*.mat`), sized by fraction of tank height, plants seated on the highest
  dune under their footprint, footprint capped in X/Z for plants only, conservative declip from rocks, a vine draped from the top
  of "Rock Tall", and a bushy backdrop behind the rocks. Plants sway (`PlantSway.cs`) and react to passing fish; rocks and big
  plants are `FishObstacle`s. Shadows only from the desk lamp and right floor lamp.
- **Fish** ("AFX Fish"): inflated paper-card meshes from sprites in `Assets/Sprites/Fish`, species data in
  `Assets/Scripts/Data/AFXSpecies.cs`. Swimming in `PaperFishController.cs` is a port of Fish Alive's FishMotion
  (acceleration + liquid drag, target pings, eased turns, look-ahead avoidance, Agile/Normal/Clumsy styles).
  Fish are on layer 30 so the reflection probe skips them. `FishWiggle.cs` animates fins.
- **Room** ("AFX Room"): 8 m square room of primitives, furnished from two free packs:
  `Assets/LowPolyLivingRoomPack_URP_FREE` (clock, painting frame, tall floor lamps, carpet etc.) and
  `Assets/StylizedFurniturePack` (Tv_stand under the tank, Desk, Monitor, Keyboard, Mouse, TableLamp).
  World scale: 18 units per metre (`RoomScale`). Room renderers use rendering layer 2; room lights use layers 1+2.
- **Lobby layout**: stand + tank on the left, desk with PC on the right (1.5x size, over the knee space), desk lamp over
  the drawers, AFX logo picture centred over the tank, 63 cm clock above the monitor showing the player's local time
  (`WallClock.cs`), floor lamps in the back corners (right one off by default).

## Interaction (lobby)

- `LobbyCamera.cs`: fixed lobby view. Left click a `ViewTarget` (tank, monitor) glides in face on; right click glides back.
- `ILobbyClickable` (`LobbyClickable.cs`): in-place clicks. `LampSwitch.cs` toggles lamps; `Drawer.cs` slides the desk
  drawers (overlaid on the desk mesh's carved fronts by `AddDeskDrawers`).
- `TankOrbitCamera.cs` is the old free orbit/room camera, no longer used.

## PC emulators on the monitor (PCjs in an embedded browser)

- Clicking the Mouse boots Windows 95, the Keyboard Windows 3.1 (`MonitorBrowserLauncher.cs`), in a UnityWebBrowser
  (CEF) page shown on the monitor quad (`MonitorBrowser.cs`). Both boot from OUR mirrors on the server
  (`http://78.110.173.123/VM/win95/...`, `/VM/win31/...`), not pcjs.org: the mirrored machine.xml files carry a hidden
  `<control binding="save">` and the embed lines add a second data disk (Win95: 128Mb COMPAQ type 25 `/VM/disks/AFX128.json`,
  Win31: 32Mb AT type 3 `/VM/disks/AFX32.json`, both pre-formatted FAT16, made with PCjs's diskimage tool). Bigger Win95
  drive types with 33/34 sectors per track (e.g. type 42, 272Mb) hang Windows 95 at the logo; 17 sectors per track works.
  Win95's template is PCjs's stock boot-sector state with its BIOS data area (0x475 = 2 disks, INT 46h vector) and CMOS
  patched to know D: (see `server/templates/README.md` and `server/tools`). `browserClient.noSandbox = true`
  (set in `MonitorBrowser.Awake()`): CEF's sandboxed renderer/GPU child processes need a window station object
  Remote Desktop sessions often restrict, so under RDP the sandbox silently fails and the child process never
  completes its handshake, reproducibly leaving `IsConnected`/`ReadySignalReceived` stuck false and a black screen
  (Paul works over RDP); only our own mirrored pages ever load here, never arbitrary sites, so disabling the
  sandbox is an acceptable trade to make it start reliably under RDP. `MonitorBrowser.Awake()` also picks a fresh
  TCP comms port pair per boot (`NextPortPair()`, cycling 55000-65000) instead of the fixed 5575/5576 every
  instance used to share: repeated boots in one session (F1, OS swap, retries, self-heal) could otherwise hit
  Windows' TIME_WAIT on a just-closed socket ("Only one usage of each socket address is normally permitted"),
  which silently stopped the comms layer (and so the whole engine) from ever coming up, black screen with no
  CEF-side error at all. Confirmed as the actual cause of a run of "engine did not get ready" reports via that
  exact SocketException in Paul's log.
- The browser picture is sampled with a vertical flip only (`flipScale` (1,-1), `flipOffset` (0,1), re-applied every
  frame; confirmed on the monitor, (-1,-1)/(1,1) mirrors it left to right). The fullscreen script hosts only the
  emulator's screen element (canvas + keyboard overlay), hides everything else, keeps 4:3 centred on the 16:9 quad
  (re-fitted on a timer, not just once, so a boot-time text-mode aspect doesn't stay stale once the desktop switches
  video mode) and reports back over the uwb JS bridge so Unity re-sends it until it took effect.
- The 16:9 quad is bigger than the letterboxed 4:3 picture inside it (black bars either side), so `MonitorBrowser`
  gets the picture's own on-screen box from the page (`AfxContentRect`, `contentRect`) and only treats the real mouse
  as "on the emulator" when it lands inside that box, not just anywhere on the quad: this is what keeps the emulated
  cursor lined up with the real one (instead of reacting while still over a black bar) and lets right click/Esc/F1
  reach the lobby again once the player is off the actual picture, even while still over the quad. Keyboard input is
  still forwarded any time the close-up is showing, regardless of exact mouse position (typing shouldn't need the
  cursor pinned to the picture).
- Exact cursor: PCjs only gives Windows a relative serial mouse and Windows 95 adds speed-dependent acceleration, so raw
  movement never lined up. `AbsoluteMouseJs` (sent with the fullscreen script, installed once the canvas exists) blocks
  the real mouse events from reaching PCjs, finds Windows' arrow on the canvas (full template match of the standard
  arrow), and sends small synthetic moves until it sits on the guest pixel under the pointer; clicks wait until it has
  arrived (max 400 ms). Dead reckoning while the arrow isn't visible (hourglass, I-beam). Headless test harness:
  `server/tools/mousetest.js` (+ `afxmouse.js`, the readable source).
- `pictureInsetTop`/`pictureInsetBottom` (default 0.01 / 0.035 of the height, live-editable in Play mode) keep the
  picture clear of the bezel's bottom lip, which covered the Start bar when the picture used the full quad height.
- Save states: `MonitorBrowser` clicks that Save control every `autoSaveEvery` seconds and again on F1 / OS swap
  (`ShutDown()`), which makes PCjs snapshot the RUNNING machine and POST it to `/api/v1/user` (afx-api, PM2, port 5050,
  files in `/var/www/afx/VM/states/<player>/<key>.json`). The launcher asks `?req=latest` and boots with
  `?state=/VM/states/...`. `MonitorBrowserLauncher.resumeMode` is Disk for BOTH machines (Paul's choice: behave like real
  PCs, files persist, every boot is a normal boot): afx-api `mode=disk` serves the machine's TEMPLATE (the "fresh PC"
  state every player starts from, `/VM/templates/<win95|win31>/template.json` + `manifest.json`, repo copy in
  `server/templates/`, README there) with the player's hard disk controller state merged in. `req=new` provisions a
  player from the template, `req=reset` wipes their saves for that machine and provisions again, `req=templates` lists
  them. Full mode (exact snapshot resume) exists but only works for Windows 3.1; PCjs faults restoring a protected-mode
  Windows 95 snapshot. Both page embeds boot from the same template via a `state:` parm rather than `resume:1`, which
  keeps PCjs's Save control enabled without it ever resuming from browser localStorage.
  Player id = `AfxPlayer.Id` (PlayerPrefs GUID for now; swap in the account id there when accounts exist).

## Windows 98 in v86 (replacing the PCjs Windows 95 on the Mouse launcher)

- PCjs's Windows 95 has an unfixable-for-us emulation bug (My Computer / Recycle Bin open the C: view; confirmed the
  same disk works in DOSBox-X and QEMU) and the compact install has no network drivers, CABs or browser, so the Mouse
  now boots our own page `http://78.110.173.123/VM/v86/win98.html?user={user}&api=...` (source
  `server/v86/win98.html`): Windows 98 SE freshly installed in QEMU on the server from Paul's ISO (`iso/`, unattended
  MSBATCH.INF, CABs kept in C:\WIN98), 2 GB disk, NE2000 (RTL8029) network card whose HTTP goes through v86's
  in-browser network stack to the fake game pages at `/game/` on the server (CORS * there).
- The page keeps the PCjs bridge contract (AfxContentRect / AfxFullscreen / AfxSaved) so `MonitorBrowser` is shared:
  its fullscreenJs and SaveJs early-out to `window.__afxNative()` / `window.__afxSave()` when present. Saves are the
  disk's written blocks only (binary blob, afx-api `/api/v1/user/blob`), restored before boot: normal boot every time.
- `MonitorBrowserLauncher` treats a url containing `{user}` as one of these pages (substitutes the player id, no
  `?state=` lookup).
- Boot is a snapshot restore, never cold (a cold Windows 98 boot takes 5 to 7 minutes in v86): `win98.state.gz`
  next to the page is the idle logged-on desktop (10 MB), live about 5 s after the page loads. Player saves
  (`window.__afxSave()`, same MonitorBrowser autosave/ShutDown path) are full machine snapshots gzipped in the
  browser (about 10 MB, 4 s) stored by afx-api under key `v86.win98.state`; reload resumes exactly where they were.
  A restart from inside Windows (or a hang) is turned into "fresh PC": the page sees the picture leave 640x480 and
  reloads the base snapshot in about 10 s; `window.__afxReset()` does the same on demand. Game progress must live on
  the web side (the /game/ pages), never on the Windows disk. Verified 2026-09-21: Internet Explorer reaches the
  /game/ pages after a restore; the guest clock resumes from the snapshot's time.

## Conventions

- No em dashes in copy or comments. Free assets only (never buy Asset Store items).
- Packs ship Built-in materials; `AFX > Convert Furniture Pack Materials` remaps them to URP/Lit.
