Game Detection and ImageDiff Integration

Tuesday

Not a lot happened this week because I was travelling.

the hfs file system

The file system used by old mac computers was the Hierarchical File System (HFS). And for director engine that meant a bunch of issues, like allowing the weird file names (solved by punycode) and resource forks.

Mac files have two parts: a data fork and a resource fork. For Director games, the projector executable lives in the data fork, but the startup movie (the first thing the game loads) is in the resource fork.

When ScummVM detects a Mac Director game, it hashes the resource fork specifically (using the r: prefix in detection entries) rather than the data fork, because the data fork is just the generic Director player and would be identical across many games.

This week I added detection entries for Gus Goes to Cyberopolis, both Windows and Mac versions. The process was interesting: you add a placeholder entry with a garbage hash, compile, point ScummVM at the game folder, and it reports back the real hash and file size for you to fill in.

I also learned why two-file detection entries (MACGAME2, WINGAME2) matter: with a single file, ScummVM just picks whichever game has the most files matching, which can cause false matches when multiple games share a disc. Two files makes the match unambiguous.

For Windows, it’s a bit different. The .exe file is actually three things concatenated together: the generic Director projector code, the startup movie, and a small header at the end that stores the offset to the movie. The executable would read its own tail to find the header, then seek back to load the startup movie. This is why ScummVM can’t hash the first 5000 bytes of a Windows Director executable, they’re always identical across every game built with the same Director version. Instead it hashes the last 5000 bytes (t: prefix), which come from the embedded startup movie and are unique to each game.

ImageDiff Integration (final)

I also worked on integrating ImageDiff into the ScummVM buildbot.

I have mentioned about the imagediff tool in my past blogs in detail. It was currently running on a detached tmux session, which is kind of a hack, so sev gave me some resources to read and integrate the tool into actual buildbot.

This process unfortuntely took a lot of time for me, because this was my first time in a long time dealing with a different kind of code.

The integration used a buildbot plugin called buildbot-wsgi-dashboards, which lets you embed a Flask web app directly into the buildbot UI. Getting it to work involved fixing a few issues. Most of the issues were trivial but there was this one issue which was causing a lot of problem and I wasnt able to figure out the root cause for, for the longest time.

The issue was, whenever I loaded the imagediff page on the buildbot, it would load unreliably i.e every time I would refresh the page, I couldn’t predict whether I would get a “Resource not found” error or the page would actually load. On top of that the table wasn’t loading properly.

The fix was actually two separate issues. First, the environment variables weren’t being loaded early enough, the SCREENSHOTS_DIR variable was being read at import time before the .env file had been loaded, so it defaulted to ./screenshots/ which didn’t exist on the server. Adding load_dotenv() at the top of config.py fixed that.

The second issue was the JavaScript on the frontend constructing API URLs without the correct path prefix. Since ImageDiff is served under /plugins/wsgi_dashboards/imagediff/, a hardcoded /api/target_data/... URL would 404 every time. The fix was deriving the base path dynamically from window.location.pathname at runtime. That’s why the page was loading unreliably, depending on timing and caching, sometimes the old URL worked by accident and sometimes it didn’t.

After sorting all of that out, ImageDiff is now properly integrated into the ScummVM buildbot at john.scummvm.org. But it’s still rough around the edges, and the remaining issues will be handled by sev.

PRs:

Imagediff

Detection Entry

 

When Structs Lie

Hey everyone! This week I finally got back to the task given by Sev in my first week — converting all the raw struct casts in the Dungeon Master engine to portable byte-offset macros. This was responsible for undefined behavior that could cause crashes or wrong results on platforms with different endianness or alignment requirements.

Earlier it used to be like this:

// Struct definition
class TextString {
    Thing _nextThing;
    uint16 _textDataRef;
public:
    explicit TextString(uint16 *rawDat) : _nextThing(rawDat[0]), _textDataRef(rawDat[1]) {}
    bool isVisible() { return _textDataRef & 1; }
    void setVisible(bool visible) { _textDataRef = (_textDataRef & ~1) | (visible ? 1 : 0); }
};

Now it looks like this:

#define TEXTSTRING_nextThing(address)   (Thing(READ_LE_UINT16((address) + 0)))
#define TEXTSTRING_textDataRef(address)  READ_LE_UINT16((address) + 2)
#define TEXTSTRING_isVisible(address)    (READ_LE_UINT16((address) + 2) & 1)
#define TEXTSTRING_setVisible(address, visible) WRITE_LE_UINT16((address) + 2, (READ_LE_UINT16((address) + 2) & ~1) | ((visible) ? 1 : 0))

And the call site changed from:

TextString textString(_thingData[kDMstringTypeText] + thing.getIndex() * _thingDataWordCount[kDMstringTypeText]);
if (textString.isVisible()) { ... }

To:

byte *textString = getThingData(thing);
if (TEXTSTRING_isVisible(textString)) { ... }

Why this refactor was necessary

This refactor was necessary because the original code relied on casting raw game data directly to C++ structs, which assumes specific object layouts, alignment, and byte ordering. These assumptions are not guaranteed across different architectures, especially on big-endian machines. By switching to explicit byte offsets and endian-aware accessors, the engine now accesses game data in a portable and predictable way.

Lesson 1:

It was a massive refactor across 13 structs in the engine and required multiple attempts. My first approach was still broken — I wrote:

uint16 getTextDataRef() const { return READ_LE_UINT16((const uint16 *)this + 1); }

I thought using READ_LE_UINT16 alone would fix the portability issue, but I missed the real problem: the C++ standard does not guarantee that a struct’s members are packed without padding. A compiler is free to insert padding between members, so the second field is not guaranteed to be located exactly one uint16 after the start of the object. As a result, (const uint16 *)this + 1 may not point to the second field at all. READ_LE_UINT16 handles endianness correctly, but it can’t save you if you’re reading from the wrong offset in the first place.

Lesson 2:

After converting all the struct methods to macros operating on byte *, I still had an issue at the call sites. I was passing non-byte * pointers directly to the macros, relying on the cast inside the macro definition to handle it. While this compiled and ran, Sev pointed out that casts hide problems — you are forcing the compiler to interpret memory as a different type, relying on assumptions about object layout that may not hold on all platforms. By switching the call sites to use byte * explicitly, the macros now operate directly on raw data using fixed byte offsets rather than through potentially misleading type casts.

Result

In total, this refactor touched 13 classes throughout the Dungeon Master engine. After several iterations, the final result is a portable implementation that accesses serialized game data explicitly through byte offsets.

Another Freeze

Towards the end of the week I continued my playthrough of the game and reached level 9 out of the 13 dungeon levels. During testing I encountered another freeze when interacting with a Wizard Eye monster. After some debugging, I found that the issue originated from projectile allocation. Wizard Eyes attack by spawning projectile objects, which are allocated through getUnusedThing(). However, the size of projectile data was being incorrectly divided by two using >> 1, causing the engine to allocate only 2 words instead of the required 5. In ScummVM, _thingDataWordCount already stores sizes in words, making the division unnecessary.
Before:

int16 thingDataByteCount = _thingDataWordCount[thingType] >> 1;

After:

int16 thingDataWordCount = _thingDataWordCount[thingType];

As a result, newly spawned projectiles overlapped existing objects in memory and corrupted their linked-list pointers. In the failing case, a projectile’s next pointer was overwritten and linked back to Door 0 instead of terminating at _thingEndOfList, creating a cyclic linked list on that map square. The rendering function drawObjectsCreaturesProjectilesExplosions() then looped infinitely while traversing this list, freezing the game. Removing the obsolete >> 1 division restored the correct allocation size and resolved the issue.

 

That was it for this week. Next week I plan to finish my gameplay testing of Dungeon Master, continue hunting for any remaining issues, and start implementing the remaining unimplemented methods in the engine. See you in the next one. Until then, goodbye! 🙂

PS3 twin package

Hey, now you can run bigger games on PS3 with our new multi-engine modules version package.

This package reduces ScummVM's memory usage by approximately 90 MB by loading engines as separate modules, leaving more memory available for games.

For more information, see the PS3 platform documentation page.

WEEK 4

Week 4: Phantom Pixels, a Lying Clock, and the Effects That Never Played

Welcome back! Last week ended on a high note: the endgame finally ended, and the Kult EGA port was playable start to finish. This week was all about the ultimate QA test: a full, end-to-end playthrough to hunt down the final remaining gremlins. Quick heads-up: I have more stories than usual to tell you this time! 🙂

And spoiler alert: we did it. The game is now incredibly stable and essentially bug-free! The final polish came down to fixing subtle rendering gremlins that survive a normal playthrough, and finally implementing the EGA visual effects the port had quietly left as empty stubs.

A theme ran through almost everything: EGA stores four bytes where CGA stored one, and a lot of inherited code never got the memo—either by doing CGA math in an EGA world, or by not doing the work at all. Here are the final stories that stood between the EGA port and perfection.

Story 1: The Garbage at the Top of the World

Some rooms—like Placating the Powers, where you face the High Priestess—have animated objects scattered around: flickering torches, glowing runes, things that breathe. And in those rooms, a band of garbage pixels kept getting stamped across the very top rows of the screen. Not flickering. Not reverting. Just… smeared there, like the engine wiped its hands on the ceiling.

The mechanism behind animated spots is clever and frugal. Before the engine draws a moving object, it backs up the background underneath it so it can restore it cleanly next frame. Those backups live in a scratch buffer, and right after them—at a fixed +1500 byte offset—the engine loads the animation sprites themselves (lutin sprites).

scratch_mem1  ──► spot backups
scratch_mem2  ──► scratch_mem1 + 1500  ──► lutin sprites

That 1500-byte gap is the original CGA budget. And there’s the trap: in CGA, each backed-up pixel is packed 4-to-a-byte. In our EGA port, every pixel is stored as a full CLUT8 byte—four times the size. A backup that needed 1500 bytes on CGA needs up to 6000 on EGA.

So, the backups overran their 1500-byte fence and spilled straight into the lutin region. The next sprite load then clobbered the backup headers—and a corrupted header means a corrupted offset, with an X coordinate no longer aligned to 4. When the engine dutifully restored those backups, it pasted them at a bogus, top-of-screen position. The garbage band was the engine faithfully restoring backups it could no longer find.

The fix was to teach the memory layout about EGA’s appetite. I sized the gap for the EGA worst case, and grew the scratch buffer so neither a fat backup nor a fat sprite can overrun its neighbor:

C++

// Spot-backup gap: EGA worst case is 4x the CGA budget
gap = 6000;                 // was 1500
scratch_mem1 = gap + 12800; // gap + EGA lutin worst case

No more overrun, no more clobbered headers, no more graffiti on the ceiling.

Story 2: The Clock That Lied

Vorts and turkeys (yes, turkeys) are timed encounters. They’re supposed to wander into a room, hang around, and leave on a schedule. In my build, they were teleporting in and out at the wrong moments, blinking on for a frame, vanishing, and leaving little visual scars behind. The room felt haunted.

The encounter logic is a simple comparison: has enough time passed yet? It checks an encounter deadline (next_vorts_ticks, next_turkey_ticks) against the global timer (timer_ticks2). Straightforward—except the two numbers weren’t speaking the same dialect.

The deadlines are stored as plain numeric values (host endianness). The global timer is stored big-endian. Comparing them directly is like comparing 0x0100 against 1 and wondering why the alarm keeps going off early. The clock wasn’t broken—it was just lying about what time it was.

The fix is a one-sided byteswap so both operands are honest numbers before the comparison:

C++

if (next_vorts_ticks <= Swap16(script_word_vars.timer_ticks2)) { ... }

With both sides numeric, the vorts and turkeys finally keep their appointments instead of strobing through the walls.

Story 3: The Scan That Gave Up a Quarter of the Way

This one I caught live, mid-playthrough. The Zone Scan PSI power sweeps a horizontal line down the room to reveal hidden objects. The line is supposed to travel cheek to cheek across the whole play area.

In EGA, it covered exactly a quarter of the width and stopped dead.

You can probably guess the villain by now—it’s Story 1’s twin. Room coordinates in this engine are measured in 4-pixel blocks. The scan’s starting offset is computed by calcXY_p, which correctly scales the block coordinate by 4 for EGA’s one-byte-per-pixel buffer. But the width of the line being inverted and blitted was the raw block count, used directly as a byte count:

  • CGA: one byte = 4 pixels, so w bytes cover the full width.

  • EGA: one byte = 1 pixel, so w bytes cover… one quarter.

Same root cause as the backup overflow—a CGA-era width used unscaled in a 4x wider EGA world. The fix is to scale the width by 4 in EGA (and widen the loop counter, since w * 4 no longer fits in a byte):

C++

// Room coords are 4-pixel blocks; EGA is 1 byte/pixel
uint16 pw = (videoMode == kRenderEGA) ? (uint16)w * 4 : w;
for (px = 0; px < pw; px++)
    frontbuffer[offs + px] = ~frontbuffer[offs + px];

The scan now sweeps the full width, the way the designers intended—and the hidden flask reveals itself like it’s supposed to.

Story 4: The Ending That Almost Wasn’t

Fixing last week’s confrontation loop got me to the victory sequence—which had its own pile of problems. You win, the screen pans up to a flying saucer receding into the sky, and THE END drops in. Instead, my EGA build crashed with a divide-by-zero, and on the runs that somehow survived, the saucer shot off-screen and the logo was clipped or missing. One ending, five bugs:

  1. The crash: The end-logo frame descriptor was missing from the table, so the portrait builder read a frame width of zero and divided by it. Restoring pers_frames[9] (plus a defensive guard) killed the SIGFPE.

  2. The runaway saucer: Our recurring villain again—the saucer’s path X, read from SOUCO.BIN, was treated as a 4-pixel-byte column and multiplied by 4. In EGA it’s a raw pixel column. Dropping the *4 put it back on its flight path.

  3. The saucer that wouldn’t shrink: The EGA zoomImage and zoomInplaceXY functions were non-scaling stubs that ignored the target size and redrew at native size every frame. I wrote a real nearest-neighbour scaler so the saucer actually recedes properly.

  4. The clipped ‘D’: The scaler sampled with (srcW-1)/dstW, which drops the final source column—shearing the right stroke off the D in THE END. Switching to srcW/dstW makes a 1:1 draw the identity map.

  5. The rising red dot: The cutscene cleared its buffer with sizeof - 2, leaving the bottom-right two pixels uncleared. CGA never noticed; EGA’s scroll-reveal lifted them up the screen as a tiny red balloon riding the saucer. Clearing the whole buffer sent it home.

Now, the saucer rises, shrinks, and slips away; THE END lands cleanly; and nothing divides by zero on the way to the credits.

Story 5: The Effects That Never Played

The last piece of polish wasn’t a bug—it was a blank. Walk between rooms in CGA and the world animates: when you stride The Ring or the passages, the background spirals in over the old room before the new one appears; elsewhere there are lift wipes, a dot dissolve, and zoom-in reveals. In EGA, all of these were stubbed out—rooms just snapped. Functional, but lifeless, and not what the designers built.

So, I implemented the EGA renderer’s transition effects to match the original CGA:

  • The spiral reveal for The Ring and passages (finally wiring up a flag, skip_zone_transition, that had been sitting unused, which decides when the spiral should play).

  • The lift wipes (the room block slides up / down / left / right one line per step).

  • The dot dissolve.

  • The zoom-in reveals.

It’s the kind of work that’s invisible when it’s done right—and that’s exactly the point. Room changes in EGA now have the same texture and rhythm as the original, instead of cutting like a cheap slideshow.

What’s Next: Expanding the Scope

With these final quirks down, a clear pattern emerged: the deepest EGA bugs weren’t logic errors, they were unit mismatches and missing pieces. Fixing them was the final piece of the puzzle.

After applying these patches, I completed a full start-to-finish playthrough of the main EGA port without encountering a single glitch, crash, or graphical artifact. But Kult isn’t just one version. Now that this primary EGA build is rock-solid, my focus for next week shifts to full regression testing. I will be diving into the other EGA releases and the original CGA versions, playing through them to ensure my engine fixes didn’t break anything else, and making sure the entire Kult family runs perfectly in ScummVM.

Thanks for reading, and see you next week!

ScummVM 2026.3.0 "Carousels & Killer Whales" comes to fruition

So far, we are able to keep the release cadence. This quarter, the team is bringing you a set of new and old, all exciting features!

Newly Supported Games:

  • Nancy Drew: The Haunted Carousel
  • Nancy Drew: Danger on Deception Island
  • Noctropolis
  • Cartoon Carnival
  • Alfred Pelrock: En Busca de un Sueño
  • Pilot Brothers: On the Track of Striped Elephant
  • Pilot Brothers: The Case of Serial Maniac and 5 more games on the same engine

General enhancements

This was the period of the Google Summer of Code start, so we had an influx of new contributors, who were busy with improving our GUI, adding small and big quality-of-life features, such as improved search, kinetic scrolling, and revamped About dialog.

Engine bugfixes and improvements

Ten supported engines got their tweaks and improvements. Martian Memorandum got script bug fixes, Simon 2 multilanguage support became better, there is a new whole control panel for the GLK engine, allowing to adjust text rendering, Last Express had some corner case bugs squashed, which also happened to the Rex Nebular. Might and Magic 1 had major rewiring of its keymapper, and the enhanced patch support plays better than ever.

There was active work on the Nancy engine, and besides the two newly added games (number 8 and 9), most of the already supported games in the series received their bug fixes (thank you very much for submitting bug reports).

Two new games landed to the SLUDGE engine, and we made them downloadable from our website. And last but not least, the TwinE engine, used for the Little Big Adventure series, got its own measure of bugs fixed.

On the ports side, the Atari port is an ongoing constant series of big and small optimizations, and the PlayStation 3 port is easier on the memory consumption, allowing heavier games to be played.

For more detailed itemization of the improvements, you may refer to the release notes.

And as per our new release procedure, the port builds will be arriving within the next few weeks.

ScummVM on track for striped patience

ScummVM has officially wrapped up on Gamos’s Windows proprietary game engine. The engine originally powered Pilot Brothers 1 & 2, launching the iconic adventure series before it later transitioned to qdEngine and NGI tech. The engine was also widely used to build a wave of office time-killer games.

This engine adds support for 7 games:

  • Patience
  • Flip-Flop
  • NetWalk
  • Pilot Brothers: On the Track of Striped Elephant
  • Pilot Brothers: The Case of Serial Maniac
  • Vitamin
  • WildSnake

So, if you would like to remember the past and spend some time with two eccentric detectives, Chief and Colleague, or kill some time with Solitaire-style games, you need the latest daily development build and the game’s required data files. As always, if you encounter any irregularities or oddities, please submit those to our bug tracker.

Read the Error

Wednesday

This is the follow-up to the previous post.

The Blunder

The first task was integrating ImageDiff into the buildbot repo.

The first PR was wrong immediately. I added the files directly without bringing in the git history from the original ImageDiff repo. Sev had asked for history. The PR wasn’t mergeable, and sev fixed the git situation himself rather than have me fight with it. First mistake.

Then the actual blunder. When wiring ImageDiff into the buildbot’s Python pipeline, I hit an import error imagediff.py does from config import SCREENSHOTS_DIR at module level, which breaks when imported from a different directory. Instead of reading the error and fixing it, I asked an AI, got an importlib workaround, and pushed it.

Sev said in the chat, “please don’t do that anymore, asking AI and pushing the slop I mean.”

The correct fix was a one-line sys.path insert, something I’d have found in two minutes if I’d just read what the error was saying.

Deployment

After the ImageDiff PR merged, I was hesitant to deploy directly on the buildbot server as I didn’t want to break anything.

In response I was given the green light to break it, because we already have a VM snapshot saved.

Deployed, it worked, buildbot was up with ImageDiff integrated.

The Misunderstanding That Cost the Most Time

Once it was running, sev noticed the tool was timing out on target: theapartment-mac, he found it and pointed toward the cache logic.

That was a part of it. The cache logic was kind of flawed. But we had a bigger elephant in the room.

The real problem was something I hadn’t understood about how the tool was supposed to work at all.

The movie_diff() function was opening every PNG frame from both builds with PIL, running ImageChops.difference() on each pair, and checking to detect any pixel difference.

But it was completely wrong. The Director engine already does the pixel comparison during playback, in score.cpp. It compares each new frame against the stored reference using a pixel difference threshold, and only saves the file to disk if the difference exceeds that threshold.

So, file presence is the diff signal.

The fix was replacing all the PIL logic with: does any file matching {movie}-*.png exist in the comparison build’s directory?

I didn’t know the engine had this mechanism. PIL was redundant work the engine had already done.

The Punycode Crash

After the performance fix, a new crash appeared:

ValueError: chr() arg not in range(0x110000)

The movie name causing it was xn--xn--File IO-oa82b-. Sev explained why this exists.

Mac HFS allowed file names with characters that would be illegal on any normal filesystem  /, *, newlines, etc. “The Apartment” has movies named things like File I/O and •Main Menu.

To store these on a normal filesystem, sev designed an encoding scheme based on Punycode: files with special characters get an xn-- prefix, the special characters are removed from the name, and their positions are encoded in the tail.

xn--xn--File IO-oa82b- is doubly encoded, it went through the encoder twice, which is valid, it just needs two decode passes to get back to File I/O.

The bug was in decode_string(). There was a loop that stripped trailing dashes from the string before handing it to the punycode decoder:

i = len(orig) - 1
while i >= 0 and orig[i] == "-":
i -= 1
orig = orig[:i+1]

For xn--xn--File IO-oa82b-, this strips the trailing - and corrupts the input before the decoder even runs. Removing it was the entire fix. Without the loop, Python’s punycode decoder handles the string correctly.

Director Debugger

Three things on the dt-new branch:

Cast Details Panel: was showing ... for almost everything. Now shows : script text previews on hover with click-through. The old showScriptCasts pipeline was removed entirely everything now goes through renderScript.

Scripts Window: decoupled from the execution context, which previously shared state with it. The scripts window now has its own handler list and browser-style back/forward navigation.

Also fixed a bug where the Lingo/Bytecode toggle was a single global flag shared across all open handlers.

Keyboard Shortcuts: Cmd+2/3/4 toggle Control Panel, Cast, Score. On Mac, cmd instead of ctrl must be used.

PRs

scummvm-sites #39 · #40 · #41 · scummvm #7577

 

Reading Between the Bytes

Hi, this is week 3 of my GSoC journey. After spending the last two weeks chasing crashes across the DM engine, this week turned into something more like detective work — digging into array indices, stale pointers, and byte alignment bugs that were quietly corrupting things under the hood.
This week, I faced freezes along with crashes. Some of these bugs only revealed themselves when stress-testing specific actions, like throwing explosives at a creature group, which would lock up the game entirely instead of just throwing an error.

Stride and Seek

Flying items rendering with garbled pixels when shrunk on screen. The cause was in blitToBitmapShrinkWithPalChange, which computed the destination row stride from destPixelWidth, the raw pixel width. But allocated bitmap rows are actually 8-byte aligned via getNormalizedByteWidth, so the real stride is wider than that. Using the raw width meant every row started at the wrong offset, bleeding into the next and corrupting pixel data.

Fix: compute destStride = getNormalizedByteWidth(destPixelWidth / 2) * 2 and use it instead of destPixelWidth when indexing destLine = &destBitmap[destY * destStride]

Before:

After:

 

The Defense Drift

Next was a crash in processEventEnableChampionAction, but the more dangerous part of this bug wasn’t the crash. _actionDefense is a lookup table indexed by action type, via _actionIndex, but the code was indexing it with curChampion->_actionDefense instead, the champion’s current defense value. That’s an arbitrary runtime number, not a valid index, so when it grew large enough to exceed the table size, the read went out of bounds and crashed outright. That part was at least loud and easy to notice.

The real problem showed up when the stat stayed within bounds. The lookup would quietly succeed, but against the wrong table entry, subtracting an incorrect defense bonus every time a combat action ended. There was no crash, no error, nothing to flag it. Just a small wrong number applied again and again, every fight, every action. Over enough time, that drift compounded into a champion whose defense stat had wandered far from what it should be, sometimes ending up nearly immortal, sometimes alarmingly fragile, with nothing in the logs to explain why.

Fix: a one-character typo fix, replacing _actionDefense[curChampion->_actionDefense] with _actionDefense[curChampion->_actionIndex].

The Gimme Slot Bug

Next one came from the gimme debug command. Cmd_gimme loops over every slot for a given thing type, but unused slots have their first word set to thingNone and the loop wasn’t checking for that before passing them to getIconIndex, which calls into getWeaponInfo or getObjectType. Those ended up reading garbage from empty slots, sometimes crashing.

Fix: read the raw first word of each slot, and skip it with continue if it equals thingNone.

The Duplicate Item Bug

Another gimme bug. When it allocated a new item slot by copying thing data and bumping _thingCounts, it forgot to update dummyThing’s index to point at the new slot, still handing over the original thingIndex instead of thingCount. Two references ended up pointing at the same thing slot, causing duplicate item corruption and crashes whenever one reference was modified or deleted.

Fix: one line, dummyThing.setIndex(thingCount), after the allocation.

The Object Aspect Crash

Next one was inside drawObjectsCreaturesProjectilesExplosions, the function responsible for rendering objects, creatures, and projectiles in the dungeon view. It chained getObjectInfoIndex straight into _objectInfos[...]._objectAspectIndex and then into _objectAspects209[...], all in one expression, with no validation along the way. But getObjectInfoIndex can return an out-of-range value for invalid or corrupt things, and _objectAspectIndex itself can exceed k85_ObjAspectCount, causing crashes.

Fix: guard both indices before use, skipping the object with continue if either is out of range.

An Enum That Wasn’t One

Next one was about ActiveGroup::_directions, typed as Direction, an enum. But the field doesn’t actually store a single direction, it stores packed 2-bit direction values for up to 4 creatures combined together, built via getGroupValueUpdatedWithCreatureValue. That’s a bit-packed integer, not a valid enum value, and casting a packed integer to an enum type is undefined behavior in C++.

Fix: change the field type from Direction to uint16, and remove the (Direction) casts at every assignment site, letting the packed value be stored and manipulated as a plain integer like it should’ve been.

The Freeze That Wouldn’t Crash

This one ate up most of my week, and it didn’t go down easy. The game would freeze, not crash, whenever explosives were thrown at certain creature groups. No ASan output to point at anything, since nothing was actually invalid memory. GDB’s backtrace wasn’t any more helpful either, it didn’t even surface the method actually responsible.
So it came down to reasoning it out manually. The freeze only ever happened when throwing explosives at a group with loot to drop, which meant something to do with damaging groups and handing out their possessions. That pointed toward GroupMan, so I started going through its methods one by one, comparing them against the original reversed code to spot where our implementation had drifted.
That led to dropCreatureFixedPossessions, which iterates a fixedPossessions pointer array to hand out loot. Two early-exit continue paths, one for skipping a random drop, one for when no unused thing slot was available, both forgot to advance the pointer before looping back. Without that advance, the loop kept re-reading the exact same array entry forever, spinning in place instead of crashing.

Fix: advance currFixedPossession = *fixedPossessions++ in both continue branches before skipping, so the loop always makes forward progress.

The Projectile Crash

Next one started simple, throw a sword or any projectile, and the game crashed. No obvious cause at first. Going through the methods from the GDB backtrace one by one eventually led to initializeGraphicData, where the loop over 6 projectile scale levels was writing all 6 byte-counts to the same fixed offsets, derivedBitmapIndex, derivedBitmapIndex+6, derivedBitmapIndex+12, every iteration, missing + projectileScaleIndex. Only the last iteration’s values stuck, leaving every earlier entry sized incorrectly.

Fix: add + projectileScaleIndex to all three cache offsets, so each scale level writes to its own correct slot.

New Addition

Added a nuke debugger command. Killing monsters manually just to clear a path for testing wasted time, so nuke finds the group directly in front of the party via mapCoordsAfterRelMovement and groupGetThing, then removes it instantly with groupDelete. No gameplay impact.

Looking Ahead, Faster

I’m going to push to finish the DM engine faster going forward, since there are more engines waiting in line after this one. I’ll do my best to wrap it up as quickly as I can, though I’ll admit, it’s turned out to be far more complex and broken than I initially expected.

Thank you for reading. See You in the Next One 🙂


 

Before I disappear

Tuesday

No, this is not my last post. I am just rushing because today is the last day to post for the previous week and I am not sure how long will I have a stable internet connection.

I will not be online (probably) for the next 2 days, because I am in a very remote location (mountains) so internet access might be a problem. So, this post will get straight to the point.

I made a very big blunder this week, and a lot of code was rushed. But the week ending was good. Got to learn a lot. And very interesting story to tell.

I will make a detailed post for this week after I get a stable internet connection.

See you soon.

WEEK 3

The Endgame That Ate Itself

Welcome back! After last week’s rampage through tunnels, ghosts, and greedy opcodes, the EGA port was finally playable. So this week, I did what any reasonable person does with a newly playable game: I tried to actually beat it.

That’s when the final boss fought back—not in the story, but in the engine.

Story 1: The Confrontation That Wouldn’t End

The endgame features a tense confrontation sequence. You whittle the enemy down, and at the climax, you use the “PSI POWERS” menu to win the game. In my playthrough, I successfully gave the enemy the winning item… but nothing ended.

Instead, the confrontation menu re-prompted me as if nothing had happened. Because the win state had already been consumed, the engine immediately threw me into a forced game-over. Worse, the longer this loop dragged on, the more the screen fell apart: sprites glitched, the rendering corrupted, and the game’s global variables seemingly rewrote themselves at random.

Two symptoms—a looping menu and creeping visual corruption—almost always point to one thing: a stack overrunning its bounds.

I dug into the engine and found the culprit: script_stack, a tiny 5-frame array used to track nested script calls. The original DOS game had a “priority command” mechanism. When a priority command fired, the original assembly engine would yank the script stack pointer all the way back to a known baseline, effectively throwing away whatever nested call chain was in progress.

Our C++ port never restored that pointer. So, every time a priority command fired from inside a subroutine, it quietly leaked one script_stack frame. In the endgame, every single use of “PSI POWERS” fires a priority command. If you use it five times, the 5-frame array overflows, scribbling over the globals that live right next to it in memory. That explained the sprite garbage and the corrupted script state!

Furthermore, each C++ call nested the menu one level deeper. When I finally won, the stack unwound only one level, landing me back in a stale, “zombie” confrontation menu.

The fix was an architectural shift to make our port honor the original DOS stack discipline. I restored the script stack pointer to its baseline when a priority command runs:

C++

case 0xF000:
    script_stack_ptr = script_stack;

Then, instead of re-entering the priority handler at whatever deep nesting level we happened to be at, I propagated the pending flag straight up to the main gameLoop, draining all pending commands from a safe baseline:

C++

do {
    g_vm->_prioritycommand_1 = false;
    g_vm->_prioritycommand_2 = false;
    res = runCommand();
} while (g_vm->_prioritycommand_1);

No more accumulating menu frames, no more overflow, and no more haunted endgame. The final puzzle actually triggers the victory sequence now!

Story 2: The Bugged Wall

Deep in the game, there’s a room called The Wall — a massive gate you open and close by stepping toward it. Press the arrow once, and the gate slides halfway shut; press again, and it should slam closed, sealing the room in front of you. That’s the drama the designers intended.

In my EGA build, the drama fell completely flat. The gate would slide in from the sides… and then just stop, leaving a big black void in the middle where solid stone was supposed to be. Half-closed left a small black gap; fully closed left a massive black hole. The wall looked less like an ancient sci-fi gate and more like someone forgot to finish painting it.

The Wrong Suspects

My first instinct was that the closed wall was being drawn correctly, but then immediately erased. The engine keeps two copies of the screen—a visible “front” buffer and a persistent “back” buffer used to redraw things. I figured the freshly closed gate was living only in the front buffer and getting reverted by the back buffer a frame later. So, I wrote code to explicitly copy the gate into the back buffer after the animation.

No change.

So, I went bigger: I forced the game to re-run its full room-entry redraw routine after the gate moved. That immediately earned me a segmentation fault. Those routines simply aren’t safe to call in the middle of a script command. Two dead ends.

The thing that finally turned the investigation around was a more precise look at the symptom itself: the center wasn’t reverting to black, it was never drawn in the first place. And the more the gate “closed,” the more black space appeared. That didn’t sound like a state bug anymore. That sounded like a broken sprite.

Reading the Bytes

The wall sprite isn’t one solid image—it’s a little mosaic. The game stores it as a list of small tiles, each with an offset instructing the engine: “place me at this position in the door.”

The original CGA code lays those tiles into a buffer that’s 20 bytes wide (80 pixels). Therefore, a tile’s offset maps to a grid spot like this: row = offset / 20, column = offset % 20.

The EGA port I inherited was doing the math differently. It divided by 40 and halved the column. Rather than argue with myself about which formula was right, I opened the data file and printed the actual tile positions using both methods:

  • Correct (CGA layout): columns 0, 16, 32, 48 | rows 0, 30 → a full 4×2 grid.

  • The EGA code’s version: columns 0, 8, 16, 24 | rows 0, 15 → everything crushed into one corner.

There it was. The EGA formula squashed every tile to half spacing on both axes, piling the entire door into the top-left quarter of its own frame. The rest of the gate—most of it—stayed completely empty. Black.

The animation and the draw code had been perfectly innocent the entire time; they were faithfully rendering a sprite that had been assembled wrong from the start!

The Fix

The fix took just two lines. I made the EGA assembler agree with the original CGA math:

C++

// Before — squashed into a corner
uint16 row    = cgaOfs / 40;
uint16 colCga = (cgaOfs % 40) / 2;

// After — full 20-byte-wide layout, like CGA
uint16 row    = cgaOfs / 20;
uint16 colCga = cgaOfs % 20;

Because that single function builds every wall sprite, the fix lit up everything at once. The half-closed gate, the fully closed gate, and the sliding animation in between all snapped into solid, properly-textured stone. The wall finally closes the way it was meant to.

Honorable Mention: A Faithful Roll of the Dice

Last week, I seeded the RNG from the host’s millisecond timer to make encounters properly random again. However, my mentor Sev rightly pointed out that ScummVM has a native, deterministic way to handle randomness: Common::RandomSource. If a tester wants to provide a fixed seed via the GUI to reproduce a bug, my timer hack would break that.

But here was the catch: the original game didn’t actually generate math-based random numbers on the fly. It used a hardcoded lookup table (aleat_data[]) to guarantee a specific probability distribution. If I replaced the game’s dice rolls entirely with ScummVM’s modern RNG, I would lose the authentic feel of the original game.

I found a perfect middle ground. I kept the original DOS lookup table, but I used ScummVM’s Common::RandomSource to pick the starting offset of that table:

C++

void randomize(void){
    rand_seed = (byte)g_vm->_rnd->getRandomNumber(255);
    getRand();
}

This honors ScummVM’s architecture (we get GUI configuration support for free) while keeping the original game’s exact probability sequence completely intact.

What’s Next

The endgame actually ends! This feels like a massive milestone—you can now play the Kult EGA port from start to finish. Next week, I’ll focus on polishing, chasing down the remaining EGA-specific rendering quirks, and ensuring the whole experience is as stable as possible.

Thanks for reading — see you next week!

Alfred Pelrock joins ScummVM

Good news for adventure game fans: Alfred Pelrock is now supported in ScummVM!

Originally released in 1996, Alfred Pelrock is a humorous point-and-click adventure featuring cartoon-style graphics, quirky puzzles, and an irreverent sense of humor. After dreaming of an Egyptian princess imprisoned in the distant past, Alfred embarks on a time-travelling adventure to Ancient Egypt to rescue her, encountering bizarre situations and eccentric characters along the way.

While the game is packed with comedy and satire, players should be aware that it also includes mildly suggestive themes and occasional adult-oriented humor typical of its era. It is also worth noting that the game was released exclusively in Spanish and has no official translations.

As always, if you own an original copy of the game and speak Spanish, grab the latest daily development build, add the game’s required data files and enjoy. We also welcome bug reports. and feedback from players to help improve compatibility and preserve this quirky piece of adventure gaming history.

The Cartoon Carnival opens its gates!

Come one, come all! Hanna-Barbera's Cartoon Carnival is now ready for public testing in ScummVM.

Originally developed and released for Philips CD-i in 1993 by Funhouse Design and then ported to Windows and Macintosh in 1995, Cartoon Carnival brings together a cast of familiar Hanna-Barbera characters in a collection of short arcade-style minigames. Scooby-Doo, Fred Flintstone, George Jetson, Top Cat and friends each get their turn in different challenges, ranging from mazes and pattern matching games to balloon catching and trivia quizzes.

Rather than a traditional adventure game, this is a colorful piece of mid-90’s multimedia entertainment, full of animation, voices and music. It is also one of those licensed titles that can easily slip through the cracks, so we are especially happy to help keep it playable on modern systems.

At this time, only the Windows and Macintosh versions are supported. The original CD-i version is not supported.

To test the game, you will need the latest daily development build and either an original copy of the Windows or Macintosh release, or its demo (which we were only able to source in Italian). Please check the wiki page for the required data files.

As usual, please report any issues to our bug tracker, including the version you tested, your platform, and any useful screenshots or save files.

Step right up, pick a game, and help us make sure this carnival runs properly!

Week 4

Hey there, welcome back to the blog. The inventory is fully implemented and hence, we’re now able to drag the rung and open the fire exit, hurray.

Other than this, there’s no way to get out of this scene(except if you use the debugger console 😉 ).

Also if you notice, I have revived the original cursor of Beneath a Steel Sky here too.

Ever since I started the development on iBASS, I had turned the engine to 32 bits for both BASS and iBASS. Since the original beneath a steel sky featured an 8 bit colour depth, sev suggested to revert it back to make BASS run in 8 bpp mode which I successfully did. Now, BASS is running in 8 bpp and iBASS in 32 bpp.

There was one more crash I encountered at this phase of the game-See the cursor at the right edge of the window? That is the exit. But when I clicked on it, it hit this assert. To fix this, I guarded this call to never draw out of the boundary of _screen32.

I also performed some refining in the LINC terminal.

Major tasks that are still left to be done include implementing the text chooser. Here’s a snippet from the original Beneath a Steel Sky regarding the text chooser-

Currently, we cannot do this in iBASS. Some other tasks yet to be done include fixing the transparency issue in the inventory icon, render the cog icon for the control panel on the top left, etc. All this while we have no audio yet along with the intro and the outro.

Also, I had an important announcement to make. I’ll be away for the next few weeks due to some personal reasons. As agreed with my mentor, I will be unavailable from 13 June to 3 July. I’ll be catching once I return. Thanks, and talk to you all in a few weeks.

Thanks for reading 🙂

A darker shade of Grey: Noctropolis is Ready for Testing!

Tired of running a mundane bookstore and ready to dive into the gritty, neon-drenched pages of a comic book? Get your trench coat ready, because Flashpoint Productions’ 1994 dark adventure, Noctropolis is now ready for public testing!

In this interactive comic brought to life you play as Peter Grey, a lonely bookstore owner pulled into a dystopian superhero world where the sun never shines. With the legendary hero Darksheer retired, it is up to you to take on his role, solve the puzzles, and save the city from a cast of villains doing the bidding of a mysterious force.

To help us test, you will need your copy of the game data files and a recent daily development build of ScummVM.

Support has been added for both the original DOS release (including demos) and the Nightdive Studios digital re-release, which is available from GOG or Steam.

As you patrol the dangerous streets, please report any issues you find on our bug tracker.

The city needs a hero – will you answer the call? Happy crime-fighting!

Following the Warning Lines

Tuesday

This week I finally got SSH access to john.scummvm.net, the machine that runs the Director buildbot. I’d seen its output for weeks (those BUILDBOT: warning lines I kept triggering) but had no idea how it actually worked. Getting access and reading through the code was very satisfying because I finally met the machine that had been yelling at me for the past couple months.

The Buildbot Architecture

buildbot_diagram

How a build starts

The first thing that clicked was how the build is scoped. The buildbot watches the ScummVM GitHub repo via a webhook, but it doesn’t rebuild on every commit. master.py checks whether any changed files are in engines/director or graphics/macgui. If yes, it kicks off a build. If not, it ignores the commit entirely. Simple filter, makes sense.

Triggering the tests
Once the ScummVM binary is compiled and uploaded to the master, build_factory.py calls steps.Trigger(schedulerNames=["Director Tests"]). This tells the buildbot master to fire the Director Tests scheduler, which is a Triggerable scheduler defined in master.py. That scheduler then queues up all the test builders at once; one for each entry in targets.json, so they all start running in parallel on the available workers. The build step waits (waitForFinish=True) for all of them to complete before marking the overall build as done.

Running the tests and error checking

The test runners themselves are defined in targets.py. Each one downloads the binary, rsyncs game files from /storage, and runs ScummVM against a list of movies. Screenshots are saved to /home/director-buildbot/screenshots/ on every run. Error checking is done in steps.py, which watches the output for lines matching “BUILDBOT: incorrect check for line:” – that’s the log-replay mechanism from the director-tests repo, where expected output is recorded directly into the test movie file, and on each buildbot run the live output is compared against it line by line.

Reading through this, the separation of concerns became clear: master.py handles scheduling, build_factory.py handles the build pipeline, targets.py defines what gets tested, and steps.py defines how a single test run behaves. Once I had that mental map, the rest followed quickly.

The ImageDiff tool

pic of the tool from dev chat

One thing the buildbot produces but doesn’t analyze automatically is screenshots. Sev pointed me to ImageDiff, a tool built to catch visual regressions, cases where the engine produces slightly different output without triggering any log-based errors.

It’s a Flask web app that reads from the screenshots directory and shows a frame-by-frame diff between any two builds for a given target and movie. The core logic uses PIL’s ImageChops.difference to compute a pixel-level diff. If there’s any difference, the diff image is rendered alongside the source and comparison frames so you can see exactly what changed. Results are cached to disk so repeated comparisons don’t recompute.

I temporarily deployed it on john.scummvm.net on port 5002. The two targets currently generating screenshots are director (from the director-tests-* folders) and theapartment-mac, both of which showed up with their full build history. It’s a simple tool but it fills a gap that log-replay can’t, visual changes.

Bugs

This week I did not work on any game bug fixes. I only made changes to the visual debugger and its bugs.

The gus games bugs are mostly fixed already and the remaining one bug has not been very consistently reproducible. So, this week I’ll try to find out how to replicate it.

Here is a brief on the DT changes:

Windows Panel

  • Added a new Windows panel showing all currently loaded windows and all .DIR movie files in the game directory
  • Clicking a movie navigates to it via func_goto

Search

  • Added variable search mode to the search bar
  • Improved search with new modes (Handlers, Variables, Body) and a cleaner 3-column results table with keyboard focus on open

PRs:

https://github.com/scummvm/scummvm/pull/7564

https://github.com/scummvm/scummvm/pull/7553

last week’s changes: https://github.com/scummvm/scummvm/pull/7563

What I am currently working on

  • Currently I am working on some more DT changes.
  • Working on the checksum function.
    • Some Director movies have a VWCF resource with a checksum that our implementation fails to verify correctly, causing a crash when navigating to those movies in the debugger.
    • Sev suggested – before diving deep – running the mismatched movies through ProjectorRays first to see if it also miscalculates, that way we know whether the bug is in our implementation or the movies themselves. The code once completely written, will be identical to the Projector Rays checksum code.

P.S. The buildbot integration will be re-worked soon by one of our devs rvanlaar, so the current architecture might not be relevant in the future

More Bug Fixes in the DM Engine

Hi everyone! Following up on Week 1, most of this week was spent resolving PVS-Studio warnings in the DM engine. Many of these warnings were responsible for crashes or pointed to code paths that could lead to unstable behavior.

By the end of the week, I was able to reduce the warning count from 103 to 13, with the remaining ones being false positives. With the static analysis issues largely addressed, I can now focus fully on gameplay-related bugs, fixing the remaining crashes, and implementing the remaining methods.

One such crash was in DisplayMan::drawDoorButton():
doorButtonOrdinal = kDMDerivedBitmapFirstDoorButton + (doorButtonOrdinal * 2) + ((doorButton != kDMDoorButtonD3R) ? 0 : doorButton - 1)
The existing logic worked for most door button values but failed when doorButton = 0, leading to an incorrect offset and eventually a crash. I corrected the expression to ((!doorButton) ? 0 : doorButton - 1), fixing the crash and restoring the intended behavior.

Another crash occurred while picking up a coin placed on a pressure plate. The game would immediately crash when processing the resulting door animation. After tracing the issue, I found that processEventDoorAnimation() unconditionally queried creature attributes before checking whether a creature group actually existed on that square. Reordering the checks fixed the heap buffer overflow and matched the behavior of the reversed source code.

I also fixed several other crashes and memory safety issues in the DM engine, including an incorrect bitmask in ProjExpl::projectileDelete() and an out-of-bounds champion access in drawPanelFoodWaterPoisoned().

Towards the end of the week, I shifted my focus back to the GUI, where I fixed a bug in the launcher that caused the collapsed state of groups to be lost when switching between views.
The fix prevents LauncherSimple::updateListing() from saving the collapsed group state during the initial build, avoiding an unnecessary overwrite of the already saved configuration and correctly preserving the collapsed groups when switching views.

I also added support for stopping fluid scrolling animations with a click, allowing the first click to stop the ongoing animation and the next click to perform the intended selection in lists, grids, and scroll containers.

Additionally, I added configuration support for fluid scrolling, allowing it to be toggled from the options menu, as the feature did not turn out to be a pleasant experience for everyone.

That wraps up this week’s progress. Next week, I’ll continue working on the remaining gameplay issues and implementing the remaining methods. Thanks for reading, and see you in the next update!

Week 3

Hello again, we are into the 3rd week and I spent the last week working to finish the inventory and we are almost there. So, the inventory has four states depending upon how long the inventory item has been clicked for and where on the screen the click is recorded. In simpler terms-

 

And, if you remember from the first blog, we had converted the engine to 32-bit and we talked about the main game screen that was an 8-bit buffer and we merged them together before the screen output. The control panel has a separate screen and naturally, it was also an 8-bit screen. When the menu was built in an 8 bit buffer but passed directly to a 32 bit renderer, we got this-So naturally, just like with the main game screen, we let the menu to build its 8 bit layout but merge it before rendering.

Currently, I am working on the dragging part of the inventory items which I will brief about in the next blog.

Thanks for reading 🙂

WEEK 2

Welcome back! This second week was all about bringing the EGA port of Kult from “it boots but it’s unplayable” to a genuinely playable experience. I tackled memory corruption, weird rendering delays, and a few bizarre hauntings.

Let me walk you through the three most interesting mysteries I cracked this week.

Story 1: Groundhog Day in the Tunnels

Early in the game, you walk through a tunnel and have a chance to encounter an Aspirant. But during my testing, he wasn’t just there sometimes—he was hostile and attacked me every single time. It felt less like a random encounter and more like a scripted mugging.

I dug into the engine’s random number generator (RNG) and found the culprit. It turned out our randomize() function was a stub, leaving the seed permanently stuck at 0. Furthermore, the prepareAspirant logic was reusing a stale random value instead of pulling a fresh one.

To fix this, I seeded the RNG with the host’s millisecond timer (mimicking the original DOS game, which read the BIOS timer) and forced a fresh byte draw in the room prep logic:

C++

rand_seed = (byte)(g_system->getMillis());
getRand();

With the dice finally rolling correctly, the encounters became truly unpredictable again!

Story 2: The Ghost That Followed Me Through Doors

Once I got past the Aspirant, another weird bug appeared. I walked through a door into a completely new zone (“De Profundis”), where no NPCs should exist. Suddenly, the Aspirant’s face popped up on screen, along with a speech bubble asking if I wanted to trade!

This turned out to be a classic state leak. When swapping rooms, the engine correctly cleared the upcoming command queues, but it forgot two things: the command chain currently executing, and the pointer to the current NPC. If you crossed a door at the exact millisecond the Aspirant’s trade script was running, that script would survive the transition. The engine would then happily render the previous room’s NPC in the new room.

The fix was a surgical guard inside the door transition path (SCR_42_LoadZone):

C++

the_command = 0; 
script_vars[kScrPool8_CurrentPers] = pers_list;

This instantly aborts any in-flight command chain and drops the stale actor pointer before the room swap finishes. No more ghosts!

Story 3: The Door, The Scorpion, and the Greedy Opcode

In the Scorpion room, the intended solution is to use the “Pray” command on a statue to open a door. But doing so gave me corrupted, gibberish text on the screen, the speech bubble got stuck, and the door remained firmly locked.

I suspected it was a missing item at first, but the truth was a desync in the script decoder. The EGA and CGA versions of the game have slightly different opcode lengths. When you pray, the game wants to do two things: play a sound (0x68 PlaySfx) and then set a variable to open the door (setVar).

However, in our engine, the EGA opcode 0x68 was mistakenly programmed to read an extra “pad” byte. Because of this, the sound opcode literally “ate” the door-opening command! The engine lost its place in the script, spit out garbage memory as text, skipped the bubble cleanup, and never opened the door. Removing that single rogue script_ptr++ operand width fixed all symptoms instantly.

Honorable Mentions

While digging through the engine, I also patched up a few other nasty issues:

  • Exploding Sprites: A massive “Lutin” character sprite was overflowing its allocated scratch memory (scratch_mem1) and corrupting the EGA sprites_list[], causing random crashes during blit/restore operations. I bumped up the memory limits to keep the big guys safely contained.

  • Death Loops: Fixed an infinite loop tied to the “YOU FAILED THE ORDEALS” timer.

What’s Next

This week felt amazing because the EGA version is finally starting to behave like a real game. Next week, I’ll continue hunting down logic bugs and refining the remaining EGA-specific rendering quirks.

Thanks for reading — see you next week!

Week 2

We are into the second week and the last week went primarily working on the LINC inventory and spawning regular inventory items. As opposed to BASS, the inventory shows up at the left bottom of the game window, like this-

Currently, as you can see, there’s a transparency issue in the icon that I am trying to solve at this moment. Coming back to the inventory, the first item inside the inventory is joey’s circuit board just as it is in BASS. Same goes for the bar which is used to open the ‘FIRE EXIT’.You can see the inventory items in the images above.

Since the regular inventory’s behaviour is not yet implemented, we can’t currently go past this scene to hack into the LINC terminal. This week, I will be working on it and in the next blog, I’ll be briefing about it in a lot more detail.

Also, you might have noticed a very minute detail in the last blog where I posted the screenshot of the icons spawning. In that image, the icons were inverted. It didn’t catch my eyes until i drew the inventory and it was also rendered in an inverted position. Same goes with joey’s circuit board. I worked on it for almost a day just to find that in the original sources of ibass, it was reading the bitmap in the reverse. I did some research and found out that most bitmap files store pixel data upside down i.e. bottom-to-top, starting with the left bottom corner of the image, as opposed to how graphics APIs expect texture memory to be, i.e. top-to-bottom, starting from the top-left corner. I fixed the behaviour and now the icons are rendered like this-

Thanks for reading 🙂

Chasing Crashes in the DM Engine

Hi everyone! Now that the GSoC coding period has begun, I moved from working primarily on the GUI to investigating crashes and gameplay bugs in the DM engine while becoming more familiar with its internals.

The first issue I looked at was a progression blocker near the start of the game. After assembling my party in the Hall of Champions, I found, as Strangerke had pointed out, that the gate to the dungeon remained shut despite stepping on the floor sensors. The issue turned out to be an incorrect early return in the sensor effect logic, preventing the pressure plate from triggering the door-opening event.

Another crash occurred when highlighting a selection in the game’s save dialog. The cause was a simple parameter ordering mistake in a box constructor used to draw the selection highlight.

Another task involved implementing EventManager::highlightScreenBox(), enabling visual highlighting for UI elements such as the movement controls.

 

Next was a save-loading bug, where the first in-game load attempt would start a new game instead of loading the selected save. Setting the correct game mode when a valid save slot was chosen resolved the issue.

Next was a stairs transition crash when moving from level 0 to level 1. The crash is currently fixed, but Sev explained that I will need to revisit this area and make the implementation aware of endianness and struct padding to achieve a fully portable solution.

 

Next was a crash that occurred whenever certain creatures, such as mummies, entered the player’s field of view.

 

Another bug involved a teleporter that was intended to appear as a normal wall. Incorrect bitfield masks caused it to render instead as a room filled with teleporter, hiding the key that was supposed to be visible on the ground.

Before:

After:

 

Next was a crash in the game’s debugger, where entering an invalid gimme command such as gimme gold key instead of the expected gimme GOLD KEY would result in a crash.

Another set of fixes involved correcting several incorrectly ordered CLIP calls across the codebase, which were responsible for a number of crashes.

I also resolved several warnings reported by PVS-Studio, addressing a number of potential issues in the codebase.

Now that I’m gaining momentum, I hope to tackle many more bugs in the coming weeks while continuing to resolve PVS-Studio warnings and improve the overall stability of the DM engine. That’s all for this week. See you in the next update!

WEEK 1

GSoC Week 1: Mouse Bugs, Rigged Minigames, and a Frozen Snake

This first week was full of detective work. Let me walk you through the three main mysteries I tackled.

Story 1: The Mouse That Lied to You

The game’s interaction system works by letting you hover over objects to highlight them, then click to interact. Early on I noticed something frustrating: hovering over an action worked perfectly — the cursor snapped to it and highlighted it — but the moment I clicked, the game would sometimes grab a different nearby object, or worse, produce a “not a good idea” failure message as if I’d clicked on empty space.

The hover and click paths were reading coordinates from different places.During a hover, the game samples cursor_x and cursor_y continuously from the
mouse movement events, so the position is always current. But on a click, the code was still using whatever position happened to be in those variables from
the last movement event, which could be slightly stale if the mouse had moved even a pixel between events. The fix was straightforward: latch the coordinates directly at the moment the button-down event fires.

case Common::EVENT_LBUTTONDOWN:
cursor_x = event.mouse.x; // capture position at click time
cursor_y = event.mouse.y;
mouseButtons |= 1;
break;

While I was deep in the input code, I also spotted that the cursor gave no visual feedback at all when hovering over interactive spots. The original game changed the cursor color on hotspots, but it was rendering pure white
regardless. A one-line fix in the EGA cursor renderer now makes the cursor turn yellow when over a hotspot and white otherwise — a small touch, but it immediately makes the game feel more alive and responsive.

Story 2: The Cups Game Was Always Rigged (And That’s Correct)

This was my favorite discovery of the week.

The game has a classic shell game — a character shuffles three cups, and you have to guess which one hides the skull. While testing it, I kept losing .The shuffle was to fast to track the cup, so it was pure luck. It felt like a bug, but something about the pattern nagged at me.

I pulled out the script disassembler and dug into the original game bytecode at offset 0xa76, where the ball’s cup position is calculated. The logic reads:

rand_value & 7 + 7

That expression generates a number in the range 7–14. But valid cup positions in the game’s data are only 7–12 (three cup positions × two states). So whenever the random number generator yields a 6 or 7 in the lower bits, the ball gets placed at position 13 or 14 — positions that don’t correspond to any cup.

My first instinct was to clamp the value. But then I stopped and checked if this was an accident or a feature. It’s not an accident — 25% of the time the ball is literally placed off the table, making it harder to win .

I will confess, though, that I temporarily hardcoded myself a 100% win rate just to bypass him and test the rest of the game, but rest assured, the final release will keep the authentic 25% scam rate entirely intact.

Story 3: The Snake That Wouldn’t Move Its Mouth

The third bug was the most technically satisfying to crack, and my mentor Sev deserves credit for pointing me toward the solution.

In the room called “The Twins”, there’s a snake you can interact with. You click OPEN on it, the game logic updates correctly (afterwards you can click SHUT , proving the internal state changed), but the snake’s sprite on screen
stays frozen in the closed position. The animation just never fires.

My first suspicion was a backbuffer issue — the game uses a software backbuffer for rendering, and maybe the wrong flush function was being called after the state change. I poked at SCR_5F vs SCR_11 for a while without getting anywhere.

Sev told me to run the script dumper on the original bytecode and compare what the EGA build was actually calling. When I dumped the scripts, the answer jumped out immediately: the sequence that handles the snake interaction ends with opcode 0x6B —RedrawRoomStatics. This opcode tells the engine to redraw all the static elements in the room and flush the backbuffer to screen.

The problem? Our opcode dispatch table only went up to 0x6A. The bounds check at the time was a hardcoded >= 107 (which is 0x6B in decimal), so the engine
hit opcode 0x6B, saw it was out of range, and silently aborted the script mid-execution. The state update had already happened, but the screen redraw command and everything after it was never reached.

The fix turned out to be surprisingly simple! The EGA version of the game used a slightly different command number to redraw the screen compared to the older CGA version. Our engine simply didn’t recognize this new command, so it just ignored it.

All I had to do was add the missing EGA command and link it to the existing CGA logic—essentially telling the engine, ‘Hey, this new command does the exact same thing as the old one.’ Finally, I updated the engine’s internal safety limits so that if it ever encounters another unknown command, it won’t just silently skip it.

What’s Next

This week showed me how much of game archaeology this work involves. Sometimes you’re fixing your own code, sometimes you’re uncovering a 36-year-old intentional design decision, and sometimes you’re hunting a silent abort in a dispatch table.

Next week I’ll be looking at more EGA-specific rendering differences and continuing to close the gap between the CGA and EGA builds of the engine.

Thanks for reading — see you next week!

Catching momentum

Sunday

Coming back to a large codebase after a break is disorienting.

I returned this week after a 3-week gap and barely recognized the code I’d been working on. So I planned the week around small, finishable tasks. Nothing ambitious. Just things I could complete and feel good about, to rebuild momentum.

Start small

Sev assigned me my Planka board (the task tracker we use) and pointed me toward a set of Gus games: Gus Goes to Kooky Carnival, Cybertown, and a few others. I went through the tickets, broke each issue into its own card, and we had a Zoom call where he helped me set up the environment properly.

This call is worth describing because it’s a good example of what mentorship looks like here. We talked about Dumper Companion (a tool for inspecting Director movie internals), useful ScummVM debug flags, debugging workflows. The non-technical conversation ended up being just as helpful for reorientation as the technical one. If you’re new and a maintainer makes space for this kind of call, take it.

cast viewer in the visual debugger

It wasn’t loading shared cast members. I fixed that and removed some duplicate code while I was there. Small, satisfying, done. That’s the point of starting small.


Bug 1: Crashing on empty cast slots

The hard part of a fix is often defining the boundary correctly, not writing the code

In original Director, if you set a property on a cast member slot that exists but is empty, it fails silently. ScummVM was throwing an error, which crashed games that relied on that behavior.

Imagine cast slots are numbered 1 through “b”, where “b” is the last populated member. Accessing slot “a” (within bounds but empty) should fail silently. Accessing slot “c” should throw an error. The bounds are defined by the last populated cast member, not the total allocated size.

The fix seemed obvious: skip the error if the cast member isn’t found. But that would also swallow errors for genuinely out-of-bounds IDs, real bugs you’d want to catch. The problem wasn’t the error. It was defining “in-bounds” correctly.

test movies

This is also where Sev introduced me to test movies, minimal Director movies that reproduce a single buggy behavior in isolation. Instead of loading an entire game to verify a fix, you create the smallest possible movie that triggers the issue. Much faster, much cleaner.

The solution: teach ScummVM to distinguish between an empty cast slot within a valid range and a genuinely invalid cast member ID (error).


Bug 2: Sprite dragging not working

The symptom and the cause can live in completely different systems

In one of the Gus games, clicking a puzzle piece should let you drag it around the screen. In ScummVM, clicking did nothing. The piece stayed frozen.

I assumed the drag logic was broken. It wasn’t. The drag code was fine. The real issue was buried in the event pipeline, and finding it meant tracing the entire event flow from click to handler.

ground truth testing

I needed to confirm how Director 4 actually behaves, so I ran the original Director inside Basilisk II (a classic Mac emulator) to observe the real immediate sprite property. You can’t rely on documentation alone (shocker for me), some features are marked obsolete but still functional, and behavior varies between versions.

Here’s what immediate does in Director 4 (5 & 6 too):

normally, mouseDown fires on press and mouseUp fires on release. When immediate is set on a sprite, both mouseDown  and  mouseUp  fire on the press.

The drag handler works by running a repeat while the stillDown loop inside on mouseUp. If mouseUp only fires after the physical release, stillDown is already false and the loop exits instantly. Nothing moves.

The fix was in queueEvent(): when a press arrives on an immediate sprite in D4, queue both mouseDown and  mouseUp  events immediately, then suppress the physical mouseUp later to prevent double-firing.

version boundaries

sev pointed out something: does immediate behave the same in D3? Does it stop working in D5? 

Turns out immediate works in D4, D5, D6. Not tested D3 yet. Will update this after.

the director-tests repo

Instead of loading full games to test a behavior, we use the director-tests repo, a collection of minimal test movies that verify specific Director engine behaviors. You create the smallest possible movie that reproduces just the thing you care about, and it lives in the repo as a permanent regression test.


What I’m aiming for next week

  • Work through more Gus bugs. 

  • Look at build-bot issues. 

  • Go deep on one subsystem end-to-end.

  • Improve tests and stress the visual debugger.

Week 1

We are into the first week and I have a lot of work to brief about which also includes the work done before the official start of gsoc.

For ibass, I started off by adding it to the detection table. Unlike og bass, ibass doesn’t have a separate file for the dinner table entries. Hence, adding it to the skyVersions[] array seemed a bit incorrect. So, I created a separate PlainGameDescriptor for ibass.

After the detection, the next milestone was to make the game start. But the problem was, the UI icons used in ibass are 32-bit images but our sky engine was 8-bit. So, what I did to solve this mismatch was turning the engine into a 32-bit one, keeping the game screen separately at 8-bit and clapped them together before the output.

I have skipped the intro for now because it has a separate video for the intro(and for the outro too). I have saved it for later.

The next natural step was to overlay the icons. To load the bitmaps, I ported the loadAnim() function from the ibass sources and wrote an icon renderer that we then call from our screen compositor. And finally, we have the icons rendered on our screen-

Next step was to trigger the respective actions for those icons. Upon a click, it incorporates something called “_actionFlash”. When we click, the “_actionFlashTime” starts decrementing if _actionFlash is set. So, it gives a flickering effect to the icon until _actionFlashTime becomes zero. It happens like this. As you can see, when _actionFlashTime is even, the icon is cleared and vice versa. In the game, it looks like this-

Thanks for reading 🙂