# HollowCade

Retro games in the browser. Short boot-up, then play.

## Run (dev)

```bash
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

## Production deploy

Use a host with **persistent disk** if you want public ROM uploads (`/contribute`):

- **Good fit:** VPS or Docker with volumes for `public/roms` and `data/`
- **OK for now:** [Vercel](https://vercel.com) — browse + **play committed seed ROMs** works (static CDN). **`/contribute` uploads will not persist** (no durable disk). Disable open contribute; treat as play-only until Docker/VPS.
- **Poor fit for uploads:** any serverless host without durable disk or object storage

### Why the library looked empty on Vercel

Server code used to hide games whose ROM file wasn’t found via `fs` on disk. On Vercel, `public/roms` is served from the CDN and often **isn’t visible** to that filesystem check — so every title was filtered out. That check is skipped on Vercel now; the catalog trusts `data/games.json`, and ROMs still load from `/roms/...` URLs.

### Vercel notes

1. Set `NEXT_PUBLIC_SITE_URL` to your `*.vercel.app` (or custom) domain in Project → Settings → Environment Variables.
2. Optionally set contact/legal `NEXT_PUBLIC_*` placeholders.
3. Do **not** set `HOLLOWCADE_OPEN_CONTRIBUTE=1` on Vercel.
4. Redeploy after pushing; play the shared seed catalog from `data/games.json` + tracked `public/roms`.

### Docker (recommended for full features / Linux VPS)

On the **server** (not inside the running container):

```bash
cd /path/to/Retro-Games-Site   # your clone
cp .env.example .env
nano .env   # or vim / micro — edit THIS file on the host
```

If `nano .env` says permission denied:

```bash
sudo chown "$USER:$USER" .env
# or if the whole repo is root-owned:
sudo chown -R "$USER:$USER" .
nano .env
```

Do **not** try to edit env inside the container — that won’t stick. Put secrets in the host `.env` next to `docker-compose.yml`.

**Important:** `NEXT_PUBLIC_*` (site URL, Supabase) are baked in at **image build**. After changing them:

```bash
docker compose up -d --build
```

Server-only vars (`HOLLOWCADE_UPLOAD_TOKEN`, etc.) apply on restart:

```bash
docker compose up -d
```

**Ports**

Something on this VPS already owns **port 80** (common under `/var/www/html` — nginx or Apache). Do **not** run the Compose Caddy profile in that case.

1. Keep HollowCade on `127.0.0.1:3000` only:
   ```bash
   docker compose up -d --build
   # do NOT use --profile caddy
   ```
2. Point your existing web server at the app — examples in [`deploy/nginx-kastia.net.conf`](deploy/nginx-kastia.net.conf) and [`deploy/apache-kastia.net.conf`](deploy/apache-kastia.net.conf).
3. **Apache:** Alias `/roms/` and `/covers/` to the host folders and `ProxyPass /roms/ !` so Node does not serve them (avoids EmulatorJS Network Error). See the Apache example file.
4. Check what owns 80: `sudo ss -tlnp | grep ':80'`

Only if nothing uses 80/443: `docker compose --profile caddy up -d --build` with `SITE_ADDRESS=your.domain`.

**Env file tips:** no spaces before names (`NEXT_PUBLIC_SITE_URL=...` not ` NEXT_PUBLIC_...`). After changing `NEXT_PUBLIC_*`, always `--build`.

Volumes (bind-mounted so host edits match the live site):

- `./public/roms` → `/app/public/roms`
- `./data` → `/app/data` (`games.json`, `user-games.json`)
- `./public/covers` → `/app/public/covers`

If you previously used named Docker volumes, copy them out once before switching:

```bash
docker compose cp hollowcade:/app/data/. ./data/
docker compose cp hollowcade:/app/public/roms/. ./public/roms/
docker compose up -d --build
```

Then either use **Contribute** on the site, or drop ROMs into `public/roms/<system>/` and run `npm run scan-roms` **or** Admin → “Rescan public/roms” / `POST /api/catalog/scan`.

Back up those folders regularly.

### Bare metal

```bash
npm ci
npm run build
npm start
```

Requires Node 20+. Same env vars as Docker.

### Env (see `.env.example`)

| Variable | Purpose |
|----------|---------|
| `NEXT_PUBLIC_SITE_URL` | Canonical site URL (SEO, sitemap, legal pages) |
| `NEXT_PUBLIC_CONTACT_EMAIL` | Privacy / DMCA / Contact mailto |
| `NEXT_PUBLIC_LEGAL_ENTITY` | Operator name on legal pages |
| `NEXT_PUBLIC_LEGAL_JURISDICTION` | Governing-law phrase on Terms |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL (enables Discord OAuth) |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anon/public key |
| `HOLLOWCADE_UPLOAD_TOKEN` | Shared secret for `/contribute` (recommended in production) |
| `HOLLOWCADE_OPEN_CONTRIBUTE=1` | Allow uploads with no token (open cabinet) |
| `HOLLOWCADE_MAX_UPLOAD_MB` | Max ROM size (default `256`) |

Without a token and without `OPEN_CONTRIBUTE`, production uploads return 403. Local `npm run dev` allows uploads without a token.

### OAuth / cloud profiles

1. Create a [Supabase](https://supabase.com) project.
2. Auth → Providers: enable **Discord** (add redirect URLs).
3. Auth → URL Configuration: Site URL + `…/auth/callback` (localhost + production).
4. SQL Editor: run [`supabase/schema.sql`](supabase/schema.sql).
5. Set `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` in `.env.local` / Vercel.

Without those env vars the site still works with **local** IndexedDB accounts. With them, `/account` shows Discord sign-in; first login imports this browser’s play metadata into the cloud profile. Emulator save blobs stay local until a later Storage phase.

### Legal pages

Footer links: [About](/about), [Privacy](/privacy), [Terms](/terms), [Cookies](/cookies), [DMCA](/dmca), [Contact](/contact).

Fill the `NEXT_PUBLIC_*` contact placeholders before treating the site as a public launch.

### Disclaimer

HollowCade is a **player**. Uploaders and whoever hosts the site are responsible for files in the shared catalog. The app does not verify redistribution rights.

### Accounts note

- **Local:** profiles, passwords, and leaderboards in IndexedDB (per browser).
- **Cloud (optional):** Discord via Supabase — profile metadata, history, favorites, achievements, competitive boards.
- First visit redirects to `/account?welcome=1`, then the UI theme picker.
- Admin role is only granted to usernames listed in `NEXT_PUBLIC_ADMIN_USERNAMES` (default: `admin`).
- `/admin` metrics are still that browser’s IndexedDB events (not a global server dashboard yet).

## Launch smoke checklist

1. Home → play a seed NES title → keyboard works.
2. Gamepad: press a face button once if undetected.
3. Phone: on-screen EmulatorJS pad + fullscreen.
4. Footer → open Privacy / Terms / DMCA / Contact (placeholders OK).
5. `/contribute` → upload (with token in prod) → appears in library → play.
6. Restart container/`npm start` → contributed game still listed.
7. `/robots.txt` and `/sitemap.xml` resolve.
8. Unknown URL → branded 404.

## Controls

| Input | How |
|-------|-----|
| Keyboard | EmulatorJS defaults (remap in the in-emulator **Settings** menu) |
| USB / Bluetooth gamepad | Play iframe `gamepad` permission; press a button once if undetected |
| Phone / tablet | EmulatorJS virtual gamepad; prefer landscape + Fullscreen |

Emulator assets load from `cdn.emulatorjs.org` (requires network).

## Shared catalog

Titles in `data/games.json` with ROM files under `public/roms/` are **shared**. Browse only lists games whose ROM file exists on disk.

Personal-only list: `data/user-games.json` (starts as `[]` in git). Template: `data/user-games.example.json`. Prefer `npm run scan-roms` locally; do not commit commercial dumps.

### Add via the site

1. Open `/contribute`.
2. Accept the disclaimer (links to Terms + DMCA), upload, optional token.
3. ROM written under `public/roms/` + catalog updated.

### Add via CLI

```bash
npm run add-shared -- --file=public/roms/nes/my-game.nes --title="My Game" --system=nes
```

## Profiles & accounts

- Local profiles / saves: `/profiles`
- Accounts (OAuth or local) + Competitive: `/account` → `/settings`
- Cabinet UI picker: `/setup/ui`
- Admin usernames in `NEXT_PUBLIC_ADMIN_USERNAMES` can use `/admin` on that browser

## Supported systems

NES, SNES, GB/GBC, GBA, Genesis, N64, PS1, Arcade, NDS, PSP, Master System, Game Gear, Saturn, Sega CD, Atari 2600, Virtual Boy.

**Not planned:** PS2, GameCube, Wii.

## Scripts

- `npm run add-shared` — shared catalog CLI
- `npm run scan-roms` / `import-roms` — personal catalog
- `npm run covers` — box art refresh
- `npm run fetch-legal` — allowlisted legal ROM URLs
- `npm run sfx` — boot sounds

## Stack

Next.js · TypeScript · Tailwind · GSAP · EmulatorJS

See [ATTRIBUTION.md](ATTRIBUTION.md) for credits.
