Last week I fixed the remaining problems with Déjà Vu, and this week I opened Déjà Vu II and started again from the top, notebook in hand. It is the same engine, the same tooling, and the same approach as before, but a different game exercises different corners of the code — and it found plenty of them. This being the final week, it was also time to stop fixing and start packaging.
Two crashes hiding in the same week
The first one turned up almost immediately. Using Hit on an object that has no hit handler crashed the game outright. The CALL opcode was popping the script list unconditionally once the callee had run, but the loader only pushes a script when the function actually exists. Call something that isn’t there, and the engine cheerfully popped the caller instead, leaving the reference the interpreter was still holding dangling — and the next assignment freed the same instruction array a second time. The original engine reserves the slot before the call and always removes that same slot, so the list stays balanced either way. Now the pop only happens when a script was really pushed.
The second was much shorter to write down: the routine that asks the player for text deleted the open dialog without clearing the pointer, then called into code that begins by closing the dialog and deleting that very same pointer. Two lines removed, one double free gone.
The flashlight that did nothing
This was my favourite bug of the week, because the fix was really an apology for an older workaround.
In Déjà Vu II you find a flashlight, you click Operate, you click the flashlight — and nothing happens. The cause went back a long way: the destination object was being pushed into the selection queue, so every two-object command also ran a second time on its own destination. Someone had patched around that by skipping the destination while running the queue, which does stop the duplicate — and also makes any command whose target is the source do precisely nothing.
The real fix was to stop putting the destination in the queue in the first place and track it in the highlight list instead, which is what the original engine does. Operating an object on itself works again, and the duplicated command stays gone.
Selecting more than one thing
Shift-clicking is supposed to add an object to the selection rather than replace it, so you can grab a handful of items and drag them together. Dragging already knew how to handle groups; only the selecting was missing, and it was missing in three places at once. The shift state never reached the engine because the cursor code always passed false. The selection call passed its last two arguments in the wrong order, so the shift flag arrived where the double-click flag was expected. And the shift branch itself was still an empty stub.
Fixing the argument order was the interesting part, because it immediately exposed a second call that had been quietly landing in that empty branch on the release of every single click. With the flags the right way round it suddenly started activating objects instead. It turned out to have no other purpose, so it is now gone.
Small corrections, real consequences
Two one-liners worth mentioning. The random opcode was returning a value between zero and the maximum inclusive, where the original returns a value strictly below it — which means every script indexing a table with that result had a chance of reading one entry past the end. And object updates were being dropped whenever the object already had an entry in the queue, except the queue also holds window entries, which are dispatched later. While one of those was pending, an object could change without its window ever being told, so the change only appeared once you re-entered the room.
Buttons that feel like buttons
Dialog buttons were firing their action the instant the mouse went down, and never showed that they were being held. The original inverts a button while it is pressed, de-inverts it when the pointer leaves, and only acts on release inside the bounds — so a misplaced click can still be taken back by dragging away before letting go. The action is now also tied to a press that started on that same button, so a stray release left over from whatever opened the dialog can’t trigger one.
The one that got away
Some sounds in Déjà Vu II still log “unrecognized sound type”. I spent a while on it before working out that those entries are not audio at all: they are 68k CODE resources, and the original played them by executing the code. Fifteen of the twenty sounds play correctly; the rest would need actual 68k emulation, which is well beyond a bug fix. It goes on the list as a known limitation rather than a regression.
Shipping it
With the playtesting done, the last commits of the summer were the boring, satisfying ones: the MacVenture engine is now enabled by default in configure, the Macintosh releases are promoted from unstable to testing, and my name went into the engine credits.
That’s a wrap
And that is the twelfth and final week. I will be putting together a proper final report shortly — one page with everything I worked on this summer, every pull request, and an honest list of what is still left to do — and linking it from here.
Twelve weeks ago I had never touched either of these engines. Chamber of the Sci-Mutant Priestess now runs in EGA, CGA, Hercules and on Amiga, and MacVenture is heading for a release. I have learned more about coordinate systems, byte order and other people’s workarounds than I expected to.
Huge thanks to my mentors for the steady guidance and patience all summer — this was genuinely a great one.