This post is about two small features that ended up tangled together because the PS1’s heap rules don’t let you treat them as separate problems.

Scene Set: pool-by-category

The screensaver loop has always picked from a pool called kProvenScenes. As more scenes get validated the pool grows, but the shape stays the same: one big bag, the loop draws a random scene out of it, the FG2 pack plays, the loop ticks the calendar, and around it goes. There was no way to say “I’d like the next half hour to be fishing scenes only” without forcing a pinned scene from the dev console — which then plays the same scene every iteration.

Scene Set lifts that into a first-class menu item. The new item sits right under Resume, defaults to All Scenes, and the only other ship-quality alternative right now is Fishing Only — the eight validated fishing1..fishing8 packs. As more scene families lock in, they get a one-line entry in gSceneSetPools and a label in kSceneSetNames, no other code changes needed. The picker reads pauseMenuSceneSet each iteration and indexes into the pool array.

The interaction model is deliberately different from the rest of the main menu. Left and Right scroll a pending preview, which the row shows as <Fishing Only> with an asterisk if the preview hasn’t been committed yet. Cross or Start commits. Up or Down (which moves the cursor off the row) discards the preview. That separation matters because the alternative — “scrolling lands you in a new pool immediately” — felt like an accidental click hazard the first time the row got tested. The brackets are the visual cue that this row takes horizontal input; everything else still uses Cross-to-select.

Frog clock as a real animation

The PS1 port has had a “meanwhile” frog-clock loading frame since the freeplay branch landed. Up to today it was a single static image: load MEANWHIL.BMP, draw the card sprite at (254, 100), draw one fixed hand frame at (287, 141), and call it done. It looked like a paused clock, which is exactly the wrong feel for a loading transition.

Replacing the static draw with a hand-cycle was a one-line change in isolation. Replacing it with a hand-cycle that looked right needed the original script. MEANWHIL.TTM carries 16 hand sprites split between an hour-hand set (sprites 5–16) and a minute-hand set (sprites 1–4), and the script draws each frame at a sprite-specific (x, y) because the bounding box of a rotated hand changes per angle. Drawing every hand frame at the same top-left position made the hand visibly drift around the clock face on each frame.

A short Python decoder over the TTM bytecode pulled out the per-sprite draw positions; they’re now a static kMeanwhilePos[17][2] table inside grShowMeanwhileLoadingFrame. The animation cycles the hour hand every four frames and the minute hand every frame, for a loop of 36 vblanks (~0.6 s at 60 Hz). On each frame: stamp the card, then the current hour hand at its baked anchor, then the current minute hand at its baked anchor.

Walk vs frog clock: why the cycle does a sequence reset

The honest part of this post is what happened when those two features met for the first time.

Cycling Scene Set fires the frog clock as its loading transition. The frog clock calls grInitEmptyBackground to give the card a clean black backdrop — that zeros the four background tiles in RAM. That was always the case; the change is that the next thing now is a walk transition (the screensaver loop tries to walk Johnny from his last spot to the new scene’s start). And the walk subsystem composes Johnny over those background tiles. Black tiles → black borders around Johnny.

The first attempt was to snapshot the tiles before the wipe and restore them after. On desktop that’s a nothing change. On PS1 it’s 600 KB of memcpy across four bgTile* buffers, and the heap is already tight enough that the snapshot allocation introduced a noticeable hitch on the first cycle of the run. Restoring also didn’t fully solve the problem because the rect-mode clean buffers walk actually reads from are not the same set as the full-tile clean buffers grSaveCleanBgTiles populates.

The fix that actually worked is structural. Cycling Scene Set is a sequence reset by definition: the user just told the loop to throw out its remembered context. So pauseMenuRequestSceneSetCycle consumers now also clear storyCurrentSpot and storyCurrentHdg and set fgLoopSequenceJustReset = 1. With the reset flag set, the existing guard at the top of the loop skips the walk for that iteration. The next scene’s foregroundPilotPlay does its own grLoadScreen and pulls the bg back to a known good state before any compositing happens. No 600 KB snapshot, no walk corruption, no extra moving pieces.

Walks resume on the very next iteration because the next scene updates storyCurrentSpot/Hdg on completion, so the user-visible behavior is just: change the pool, see the frog clock, see “Scene Set: Fishing Only” caption, see a clean cut to the next fishing scene.

What this is not

This release does not add new scene sets beyond Fishing Only. Johnny-only, Mary-only, and Visitors-only sets are obvious next steps, but they need their own scenes validated under the Scene Set flow before they ship. The gSceneSetPools table is structured so that adding them is a label and an array literal, not a rewrite. This release also does not add a separate art asset for the animation; the frog clock uses Sierra’s existing 17-frame MEANWHIL.BMP exactly as shipped.

The PS1 port keeps trying to make every player-visible feature land on top of the same handful of trusted runtime pieces. A new menu item and a smarter loading frame are exactly the kind of additions that should not need new pipelines, and after today they don’t.