# Crash BASIC 1.32.0 Release Notes

*~written by Crash-tan~*

## THE "GO FETCH" RELEASE

This is a big one. Three cycles of work that never shipped publicly are
rolling out together: **first-class concurrency + networking** (the
headline), the **"write once, run anywhere, but honestly"** cleanup pass,
and the foundation for **in-app purchases through your own CrashNet
server** (mock-tested, with real Stripe wiring — flagged alpha, see
that section).

The short version of the headline: your `.crash` can now call an HTTP
endpoint, fire a bunch of calls in parallel, and turn the bytes that come
back into a sprite or a sound — without ever leaving BASIC.

- **`ASYNC` + `RESOLVE()`** — first-class concurrency. Start the work,
  get a promise, resolve when you need the answer.
- **`FETCH()`** — synchronous HTTP. JSON bodies, headers, the same shape
  on every platform.
- **`BASE64TOIMG` / `BASE64TOWAV` / `BASE64TOMP3`** — function-form
  siblings of `LOADIMG`/`LOADWAV`/`LOADMP3` that take base64 instead of a
  filename. The intentional pair to `FETCH`.

---

## `ASYNC` + `RESOLVE` — concurrency you can actually USE

CrashBASIC already had `CALL THREAD` for fire-and-forget work. What it
didn't have was a way to start something in parallel and *get a value
back*. That's the entire `ASYNC` story.

```basic
FUNCTION Double(n)
    Double = n * 2
END FUNCTION

p = ASYNC Double(21)     ' spawns a worker, returns a Promise immediately
result = RESOLVE(p)      ' blocks until done, returns 42
```

`ASYNC funcName(args)` is a prefix on a function call. It evaluates the
args right there in your scope (so changes to your locals after the spawn
don't leak into the worker), kicks off a thread, and hands you back a
Promise. Store it in a variable, pass it to a function, stick it in an
Object — it's just a value.

`RESOLVE` is the consumer. Hand it one promise and it gives you the value
as-is. Hand it several and it gives you an array of values, in
declaration order:

```basic
p1 = ASYNC LoadLevel(1)
p2 = ASYNC LoadLevel(2)
p3 = ASYNC LoadLevel(3)

' Three loads in parallel. Total wall-clock = the slowest one.
results = RESOLVE(p1, p2, p3)
level1 = results(0)
level2 = results(1)
level3 = results(2)
```

Re-resolving the same promise gives you the cached answer — no second run.
Works on every kind of function: BASIC `FUNCTION`s, BASIC `SUB`s, native
built-ins. Misspell the name or get the arity wrong and you halt at the
`ASYNC` line — not buried inside a promise you'll never check.

One thing to know: errors *inside* the async body still halt the whole
program. `ASYNC` is for parallelism, not for "try this and pretend it
didn't fail." Defensive recovery is still on you.

---

## `FETCH()` — the network in two lines

```basic
r = FETCH("https://api.example.com/score?player=42", "GET")
IF r.STATUSCODE = 200 THEN
    PRINT "got: " + r.MESSAGE
END IF
```

That's the whole shape. `FETCH(url, method, body?, headers?)` always
returns an Object with exactly two fields:

- **`STATUSCODE`** — the HTTP status (200, 404, 500, …) on a real
  exchange. **`0`** when the request never got there: bad URL, DNS
  failure, connection refused, TLS hiccup, timeout. Anything pre-HTTP.
- **`MESSAGE`** — the response body as text on success, the error
  description when `STATUSCODE = 0`. 4xx/5xx? You still get the server's
  body.

The important part: **network errors don't halt your game.** A flaky API
is a runtime condition you can detect and recover from, not a fatal
crash. Bad arity or a non-Object headers argument *will* halt — those are
call-site bugs and we want them loud.

### Bodies, automatically

Strings go through as-is. Objects and Arrays auto-encode as JSON with the
right Content-Type:

```basic
DIM payload AS OBJECT
payload.player = "alice"
payload.score = 4200
r = FETCH("https://api.example.com/scores", "POST", payload)
```

### Headers

Optional 4th arg, just an Object:

```basic
DIM headers AS OBJECT
headers.Authorization = "Bearer " + token$
r = FETCH(url, "GET", "", headers)
```

Pass anything other than an Object as headers and you get
`STATUSCODE = 0` with a useful message. No halt.

### Parallel fetches

This is where `ASYNC FETCH(...)` earns its keep:

```basic
p1 = ASYNC FETCH("https://api.example.com/leaderboard", "GET")
p2 = ASYNC FETCH("https://api.example.com/news", "GET")
p3 = ASYNC FETCH("https://cdn.example.com/sprite.png", "GET")
results = RESOLVE(p1, p2, p3)
```

Three calls go out simultaneously. You wait once. Total time = the
slowest call.

### Across platforms

Same return shape, same field names, same error sentinel everywhere:

- **Mac / Windows / Linux / iOS / Android** — real HTTP client, 30-second
  timeout.
- **WASM (browser)** — synchronous fetch in the Worker your interpreter
  runs on. **Cross-origin URLs obey the browser's CORS rules**, like any
  web app: same-origin always works; third-party APIs work if the server
  sends the right CORS headers (enforced by the browser, not by us).

Synchronous `FETCH` blocks the thread it's on. If you don't want your
game to freeze for a slow request, wrap it in `ASYNC FETCH(...)`.

---

## `BASE64TOIMG` / `BASE64TOWAV` / `BASE64TOMP3`

The natural completion of `FETCH`. You got a `.png` back from the
network — now what? Now this:

```basic
r = FETCH("https://cdn.example.com/player.png", "GET")
IF r.STATUSCODE = 200 THEN
    BASE64TOIMG(r.MESSAGE, "player")
    PUTIMAGE (100, 100), "player"
END IF
```

Same downstream behavior as `LOADIMG` / `LOADWAV` / `LOADMP3` — they
register the asset under the name you give, and from there it's just
another sprite or sound clip. Errors halt loud, exactly like the `LOAD*`
statements.

The base64 input can be a raw base64 string OR a full
`data:image/png;base64,...` URI — the prefix is stripped automatically.
`BASE64TOIMG` takes the same sprite-sheet shape as `LOADIMG`
(`BASE64TOIMG(data$, "tileset", 16, 16)`). `BASE64TOWAV` / `BASE64TOMP3`
are strictly `(base64, name)`, and like their `LOAD*` siblings, audio
loading is client-only — server-side multiplayer interpreters reject it.

---

## Write once, run anywhere — but honestly

Two case-sensitivity gaps that made the same `.crash` behave differently
depending on the host. Both closed:

- **Audio handles are case-insensitive now.** `LOADWAV "shoot.wav",
  "Shoot"` followed by `PLAYWAV "shoot"` used to silently find nothing;
  the image side already worked this way. Now every audio handle lookup
  (`LOAD*`/`PLAY*`/`STOP*`/`FADE*`/`UNLOADSOUND`/`SET INSTRUMENT … WAV`/
  the new `BASE64TO*`) is case-insensitive.
- **File paths are case-insensitive on native too.** `LOADWAV
  "HELLOWORLD.WAV"` now finds `HelloWorld.Wav` on Linux and case-
  sensitive WASM hosts, not just Mac/Windows. The runtime tries the exact
  path first (fast path) and falls back to a case-insensitive walk inside
  the sandbox — directory names and filenames both count. `FILE_EXISTS$`
  uses the same resolver, so the two always agree.

---

## In-app purchases via your own CrashNet server

> 🚧 **The IAP layer is alpha.** The rest of Crash BASIC is past that
> point, but the in-app-purchase surface — the protocol, the Stripe
> wiring, the refund handling — is brand new and hasn't soaked in
> production-shaped traffic yet. Build cool things, run sandboxes, do
> **not** bet your bank statement on it. If you flip the Stripe backend
> live anyway, you're taking on the risk that an alpha-cycle bug eats a
> real customer's money, and there's no support contract underneath you.

> ⚠️ **This is the part that takes real money.** Point your
> `crashnet-server` at a live Stripe account with a `sk_live_…` key and
> a customer hits Buy, and money moves from their card to your bank. Test
> in sandbox first with `sk_test_…` keys and Stripe's test cards
> (`4242 4242 4242 4242` succeeds; `4000 0000 0000 9995` declines — full
> list at <https://docs.stripe.com/testing>). The mock backend is the
> default; nothing charges until you opt in.

> 📡 **CrashNet-only for now.** IAP runs through a `crashnet-server` you
> (or your host) run — catalog, ledger, and the Stripe round-trip all
> live server-side. **Pure single-player games that don't connect to a
> server can't take payments yet.** Apple StoreKit / Google Play Billing
> are stubbed in the iap.json schema but not wired to working backends.

For BASIC programs, the IAP surface is unchanged — same `PURCHASE NEW`,
`FOR PURCHASE`, `PURCHASE.*` pseudo-variables, and the `"purchase"`
event. What's new is that **your CrashNet server can be the payment
authority.** It already has JWT auth, a per-user database, and a
WebSocket listener, so now it also mounts an `/iap` route (automatically,
whenever the game directory has an `iap.json`).

- **Two backends, same wire shape.** `MockIapBackend` (the default)
  pretends every purchase succeeds after 500ms and never charges anyone —
  ideal for development and reward-grant models. `StripeIapBackend`
  (when you set `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET`) creates
  real Stripe Checkout sessions, opens the user's browser, and completes
  on the webhook. Your game code can't tell which is on the other end.
- **`stripePriceId` per product in `iap.json`** links each product to a
  Stripe Price. The mock ignores it; Stripe requires it and refuses
  (per-product, with a clear error) any product missing one — silent
  fallback would be a money leak.
- **Refunds and disputes auto-revoke.** A full refund or a lost dispute
  pulls the matching non-consumable: the DB row is deleted and, if the
  player is online, their `PURCHASE.PURCHASED` flips to false in real
  time with a `"purchase"` event (`success = 0`). Consumables stay as
  history — clawing back already-spent coins is a game-policy call you
  make manually.
- **Three ways to configure secrets:** `--stripe-secret-key` /
  `--stripe-webhook-secret` flags, `STRIPE_SECRET_KEY` /
  `STRIPE_WEBHOOK_SECRET` env vars, or the new **Stripe panel in the
  crashnet-server admin UI** (with a TEST/LIVE/NOT-CONFIGURED badge).
  CLI/env wins over the DB; setting one via env locks that field in the
  UI.
- **One login covers multiplayer AND IAP.** The same `--token` JWT works
  for both routes. Players already in your lobby buy without
  re-authenticating.
- **Server-side BASIC sees purchases**, including mid-session ones —
  per-player, isolated, hydrated at join and updated live.
- **Fail-loud safety nets everywhere:** half-configured Stripe secrets
  abort at startup; secrets set against a binary built without
  `--features stripe` warn and fall back to mock; webhook signature
  failures return 400; products without a price ID refuse individually.

For local webhook testing, run
`stripe listen --forward-to localhost:9005/iap/webhook` (install the
Stripe CLI: <https://docs.stripe.com/stripe-cli>) — it prints the
`whsec_…` you paste into the admin UI.

---

## Compatibility notes

- **No multiplayer/protocol changes.** CrashNet, crashgfx, crashaudio —
  wire-compatible with 1.29.x. Mix and match while you roll out.
- **`ASYNC` is a reserved prefix.** `ASYNC funcName(...)` is the new
  form; identifiers that merely start with those letters (`ASYNCfoo`)
  still parse fine — only literal `ASYNC` before a function call triggers
  it.
- **Audio handle behavior changed.** Code that intentionally relied on
  `"Boom"` and `"boom"` being different sounds no longer works. (You
  almost certainly don't have any.)
- **DB schema is additive.** Existing crashnet databases auto-migrate on
  next open (idempotent `CREATE TABLE IF NOT EXISTS` + `ADD COLUMN IF NOT
  EXISTS`); SQLite and Postgres both have full IAP parity.
- **Default behavior unchanged.** No Stripe env vars = mock backend; no
  `iap.json` = no `/iap` route at all.

## What's NOT in 1.32.0

- **Subscriptions / recurring billing.** Stripe Checkout supports it; we
  do one-time payments only. Needs BASIC-side design.
- **Auto-restore on a won dispute.** If you win a chargeback back after
  we've revoked, the customer is re-granted manually.
- **In-process Stripe secret hot-swap.** Saving secrets in the admin UI
  needs a server restart to take effect.
- **Platform-native IAP (Apple / Google) and pure-offline IAP.** Server-
  mediated only for now.

---

*Go fetch. (And maybe charge a couple bucks for the privilege — once
you've read the test-card list.)*

*— Crash-tan*
