BLIT386 1.4.0 - edit the game while it is still running
The hot reload release - a three-tier swap runtime, a blit386/vite dev plugin, in-place asset replacement, and an asset loading count.
The old loop went like this: you tweak a number in update(), hit save, the page blinks, and you are back at the title
screen with a fresh palette, a camera at the origin, and a character standing exactly where they were not when you found
the bug. So you walk them back. Twelve seconds, maybe fifteen, and you do it four hundred times a day. Nobody complains
about that out loud because it has always been that way, which is the most reliable sign that something is worth fixing.
1.4.0 fixes it. You edit, you save, and the game keeps running - same tick count, same camera, same palette, the sprite still mid-jump where you left it.
Three tiers, and you do not get to pick
Every save, bootstrap() compares the newly evaluated class against the one currently on screen and decides how far it
has to go. There are three answers:
If only method bodies changed, the running instance keeps all of its fields and just gets a new prototype. init()
never re-runs, music keeps playing, the loop does not skip. Module-level constants count as method bodies here, which
surprised me the first time - change a SPEED at the top of the file and the next tick picks it up, because the new
update closes over the new module scope.
If init(), the constructor, or a field initializer changed, none of that is safe, so a fresh instance gets built and
its init() runs while the old instance is still driving the loop. Only when it succeeds does the engine swap it in.
A broken save leaves the previous instance running, which is the whole point - the thing you are editing is a game you
are also playing, and it should not die every time you leave a bracket open.
And if configure() changed, the canvas, the WebGPU device, or the audio graph would have to be rebuilt, so the
engine gives up honestly and reloads the page.
Nothing about any of that requires your demo to change. onHotReload is optional, and it exists only for the case where
you want to carry state across a re-init instead of accepting a cold boot:
import { , type HotReloadContext, type IBTDemo, } from 'blit386';
class implements IBTDemo {
private = 0;
async () {
return true;
}
(: HotReloadContext) {
if (. !== 'reinit' || !.) {
return;
}
if (typeof .. === 'number') {
this. = ..;
}
}
() {}
() {
.(new (8, 8), 1, `score: ${this.}`);
}
}The full tier rules, the blit386:hot-reload DOM event, and what happens under ?backend=software are all in the new
Hot Reload Guide.
The plugin that makes it happen
None of it runs until Vite knows about it, and that is one line:
// vite.config.js
import { defineConfig } from 'vite';
import { blit386 } from 'blit386/vite';
export default defineConfig({
plugins: [blit386()],
});The plugin appends a small registration snippet to any served module that imports blit386 and calls bootstrap(...),
and it watches your asset directories. It is dev-server only - apply: 'serve', so a production build never sees it,
and it has no runtime dependency on vite itself. If you scaffolded with npm create blit386, it is already wired up
and you can skip this section entirely.
Assets swap in place too
Editing a sprite sheet in Aseprite and having the browser throw away your game state was, if anything, more annoying
than editing code, because the whole reason you were in Aseprite was to see the sprite in context. So images, audio, and
.btfont files now replace themselves in place: the sheet re-decodes, the font re-parses, and music restarts if the
buffer under the current track is the one that changed. A broken asset cannot take the game loop down with it - you get
the error, the old asset stays, and you fix the file.
Other changes
A number for the waiting you cannot avoid
Hot reload removes most of the waiting in development, and precisely none of the waiting your players do on first load.
So BT.loadingAssetsCount is there now: the combined count of images and audio clips still in flight, which is exactly
what a loading screen wants to poll. Per-sheet there is SpriteSheet.status - 'loading', 'ready', or 'failed' -
and a coarse progress. Details in API: Assets.
Touch scrolling stopped fighting the page
Small, but it was a real bug in disguise. The canvas used to pin touch-action: none on attach, which meant that on a
phone the page under your demo refused to scroll, whether or not the demo had asked for scroll capture. It now follows
HardwareSettings.isCapturingPointerScroll - none while capturing, pan-y otherwise, updated live. Default is
false, so a demo embedded halfway down a page behaves like the rest of the page again. See
Input Guide.
Housekeeping, briefly
Every remaining @deprecated tag in the source now carries the version it was deprecated in and a 2.0.0 removal
target, so the deprecations page answers "how long do I have" instead of implying it.
The expired esbuild audit exception is gone for real, not silenced. Lint runs with --max-warnings 0 and three Biome
rules moved from warn to error, which mostly means CI now agrees with the pre-commit hook instead of quietly
disagreeing. Post-process effects grew their own 80 percent coverage floor. Renovate, which had been sitting in the repo
doing nothing for months, was repaired and now signs off its own commits.
And CLAUDE.md went from a monolith that ate half a session's context to a set of short summaries pointing at rule
files, with a drift checker - agents:check - that fails the build when the Claude and Copilot copies disagree. If you
are running an agent against this repo, that is the change you will feel.
The full list, in the usual Keep a Changelog shape, is at the changelog.
npm create blit386@latest my-gameGo break something and watch it keep running.