CrashPlayer 1.32.0 released!
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 ofLOADIMG/LOADWAV/LOADMP3that take base64 instead of a
filename. The intentional pair toFETCH.
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.
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 42ASYNC 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:
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 FUNCTIONs, BASIC SUBs, native
built-ins. Misspell the name or get the arity wrong and you halt at theASYNC 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
r = FETCH("https://api.example.com/score?player=42", "GET")
IF r.STATUSCODE = 200 THEN
PRINT "got: " + r.MESSAGE
END IFThat'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.0when 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 whenSTATUSCODE = 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:
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:
DIM headers AS OBJECT
headers.Authorization = "Bearer " + token$
r = FETCH(url, "GET", "", headers)Pass anything other than an Object as headers and you getSTATUSCODE = 0 with a useful message. No halt.
Parallel fetches
This is where ASYNC FETCH(...) earns its keep:
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:
r = FETCH("https://cdn.example.com/player.png", "GET")
IF r.STATUSCODE = 200 THEN
BASE64TOIMG(r.MESSAGE, "player")
PUTIMAGE (100, 100), "player"
END IFSame 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 fulldata: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 byPLAYWAV "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 newBASE64TO*) is case-insensitive. - File paths are case-insensitive on native too.
LOADWAV "HELLOWORLD.WAV"now findsHelloWorld.Wavon 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-serverat a live Stripe account with ask_live_…key and
a customer hits Buy, and money moves from their card to your bank. Test
in sandbox first withsk_test_…keys and Stripe's test cards
(4242 4242 4242 4242succeeds;4000 0000 0000 9995declines — 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-serveryou
(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 setSTRIPE_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. stripePriceIdper product iniap.jsonlinks 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, theirPURCHASE.PURCHASEDflips 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-secretflags,STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRETenv 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
--tokenJWT 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 stripewarn and fall back to mock; webhook signature
failures return 400; products without a price ID refuse individually.
For local webhook testing, runstripe listen --forward-to localhost:9005/iap/webhook (install the
Stripe CLI: https://docs.stripe.com/stripe-cli) — it prints thewhsec_… 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. ASYNCis a reserved prefix.ASYNC funcName(...)is the new
form; identifiers that merely start with those letters (ASYNCfoo)
still parse fine — only literalASYNCbefore 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 (idempotentCREATE 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/iaproute 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