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!