Categories
GSoC 2026

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!

Categories
GSoC 2026

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!

Categories
GSoC 2026

Finishing Incomplete ScummVM Engines

Hello everyone! My name is Andy and I’m a 2nd-year Computer Science student. I am incredibly excited to announce that I will be participating in Google Summer of Code 2026 with ScummVM!

I am deeply drawn to ScummVM because of its core mission of digital preservation, coupled with my deep passion for retro gaming. For this summer, my project is titled “Finishing implementation of incomplete engines”. The ultimate goal is to push three nearly-finished ScummVM engines over the finish line, ensuring they are stable and ready for official ScummVM releases.

Currently, these engines face various challenges, ranging from incomplete low-level graphics and legacy code structures to unresolved gameplay bugs.

I am looking forward to diving deep into the codebase and sharing my progress, technical challenges, and bug-hunting stories right here.

Stay tuned for more updates, and let the coding begin!