Last week I started working on creating multilanguage dat file for the Teen Agent. Currently, only the English version is supported, and Polish, Czech and Russian versions needs to be added.
The task is to extract language specific data, strings from the executables, put them in the create_teenagent utility and finally generate the .dat file for the use of engine. The extraction tools for the game were already in hand, although I had to write custom python scripts to create cpp code from them. The strings that needed to be extracted are the following:
Names and descriptions of inventory items
Dialogs
Messages
Character’s thoughts
Credits
Names and descriptions of scene objects
Most of them were not difficult extract and put in the code, except for the scene objects. I started with the Russian version, and the problem was that some string could not fit in the original positions, so they had to be copied to the free space of the data segment. That includes the pointers to those objects. So this caused a little issue, and I am in process of writing it properly.
Screenshot the Russian (fan-translated) version
In parallel, I was also looking at the Polish (CD) version. This version brought more issues, which I am yet to solve. The problem is that Polish executable is larger than the English, and has two more scene objects. The sizes of texts are also generally bigger, so I would not be able to fit them into the dat file. I am thinking of putting them after the data segment and dialogs. However, that would require also changing in a couple of places in the engine code, since it relies on the data segment. This means that save system also needs to be adapted for this, as the engine is currently writing the save section part of the data segment. I’ve already tried this idea and separated the credits section the data segment, and put it separately at the end. This allowed to display the Polish text in the credits section:
Similarly, I’ve experimented with the dialogs, and they also started showing up properly:
However, there is also a Dialog stack, which is modified at runtime by the game, so I probably will need to put as a resource in different segment as well.
This week, I focused on bringing keymapper support to three engines: Sword25, Teenagent, and NGI. Each had its own quirks, but none were too complex—so I was able to make steady progress across the board.
Sword25
The Sword25 engine uses script-driven input handling. That made things straightforward—no refactoring needed.
My job here was mostly investigative: figure out what each key does and document it for the keymapper. Since input behavior was defined in scripts, I didn’t need to touch much engine-level code. The entire process was smooth and quick.
Teenagent
Teenagent was also fairly simple. It involved defining a few custom actions for specific keys and setting up mouse input. The engine doesn’t have complex input modes or conditionally processed input, so once I identified the key behaviors, wiring up the keymapper didn’t take long.
NGI
The NGI engine was the most challenging of the three this week. While it didn’t introduce any new types of problems, it did have more keys and more complexity around where input is processed.
One big issue: keys pressed during menus, modals, or intros weren’t processed through the same input flow as in-game actions. That meant keymapper actions would either:
Be ignored in certain contexts, or worse…
Be interpreted in places they shouldn’t be
To fix this, I had to carefully trace the engine’s input handling paths and add logic to enable or disable the keymapper at the right times. Most of my time on NGI was spent understanding those flows and placing the toggles precisely.
Once that was in place, implementing custom actions and mapping key behaviors was straightforward.
Wrap-Up
This week, I:
Implemented keymapper support for Sword25, Teenagent, and NGI
Got the NGI keymapper PR merged
Got the keymapper action label normalization PR merged
Next week, I’ll continue this momentum and explore new engines.
This week’s progress was somewhat better than the previous. Continuing from last week’s writing task, I was successfully writing the initial map, memory map, the key data and the cast data (‘CAS*’ data) successfully. Until then, we’re outputting the data as it is, without any modification. The goal was to be able to write movies for now even if there aren’t any modifications. This week I got to actually writing data that will be modified.
The first thing was the cast config data (‘VWCF’ chunk) data, this was pretty straightforward. I just had to save all the data that was being ignored and write it back in `saveConfig()`. Although, it involved calculating checksum for the config.
The next thing was the cast member data (‘CASt’ chunk), this was the tricky part. As I understand it, a ‘CASt’ chunk is made up of three sections, the first is the headers (which includes the metadata, like flags, cast member type and size of the rest of the two sections), there is info and data. The info has “strings” inside which contain information about the cast member like the name of the cast member, the file name (if it’s external), id of the script attached to it, font information, etc. Depending on the cast member, this number of strings varies. Hence, the size of the info also varies. Since I need to write the size of the info and data sections in the header, I have to calculate it from the loaded data. Hence, I ended up creating three methods getCastResourceSize(), getCastInfoSize(), and getCastDataSize(), this is useful since, I’ll need the ‘CASt’ resource size when I’m writing it in the memory map. Thankfully, this structure of info is consistent across all ‘CASt’ resources. So, I can create a single method in the base class and it works for all the cast members. The only section the cast members differ in is the data section for which I created a custom method in each derived class. (Note: Here the base class is CastMember and the derived classes are TextCastMember, PaletteCastMember, BitmapCastMember, etc.) The data section contains the cast member data (the bounding box dimensions, background color, flags, etc.). Now that I think about it, data and info are arbitrary names. Both contain different data about the castmember, but data still.
Majority of this weeks’ time was spent on testing chunks being written(even though there were only two (‘VWCF’ and ‘CASt’)). For debugging, I’ve also introduced a new channel (kDebugSaving). Some of the cast members are still haven’t been tested thoroughly.
These cast members can have children resources. Data like bitmap (pixel data in ‘BITD’ resource), text (‘STXT’ resource), are stored in these children resources. After the ‘CASt’ resource, our next step is writing these children resources which I think are going to be easier to write and test than the ‘CASt’ resource. Sidetracking, I tried writing the ‘CLUT’ (Palette data) and ‘STXT’ (Text data).
Even though, I was occupied the entire week, this still isn’t that much progress. It is still some time before this task is on its way to completion. I’ll need to speed up if I want to complete it within time.
Also, I realized my blogs are not as information rich as I’d like them. So, I’m planning on writing a mid-week blog to explain in detail, what I’ve done till now (shouldn’t take long!).
Welcome to this week’s blog update. This week was focused on fixing bugs related to seeding and set.dat uploading, improving filtering mechanisms, and rewriting parts of the processing logic for scan.dat.
After some manual checks with the recent set.dat updates, a few issues surfaced:
1. Identical Detection Fix
Some detection entries had identical file sets, sizes, and checksums, which led to issues during the automatic merging of set.dat. Previously, we used a megakey to prevent such duplicates, but since it included not only file data but also language and platform information, some identical versions still slipped through. To solve this, I replaced the megakey check with a more focused comparison: filename, size, and checksum only, and logging the details of the fileset that got clashed.
2. Punycode Encoding Misplacement
The filenames were being encoded using Punycode every time they were processed for database insertion. However, there was no requirement for this encoding as it should have occurred earlier — ideally at the parsing stage, either by the scanning utility that generates .dat files or on the application’s upload interface. I have removed the encoding during database updates. Though I still have to add it at the scan utility side, which I’ll do this week.
3. Path Format Normalization
Another issue was related to inconsistent file paths. Some set.dat entries used Windows-style paths (xyz\abc), while their corresponding detection entries used Unix-style (xyz/abc). Since filtering was done using simple string matching, these mismatches caused failures. I resolved this by normalizing all paths to use forward slashes (/) before storing them in the database.
4. Improving Extra File Detection with clonof and romof
While analyzing filesets, I encountered a previously unnoticed clonof field (similar to romof). These fields indicate that extra files might be listed elsewhere in the .dat file. The previous logic only looked in the resource section, but I found that:
Extra files could also exist in the game section.
The file references could chain across multiple sections (e.g., A → B → C).
To address this, I implemented an iterative lookup for extra files, ensuring all relevant files across multiple levels are properly detected.
scan.dat Processing Improvements
For scan.dat, I introduced a file update strategy that runs before full fileset matching. All files that match based on file size and checksum are updated first. This allows us to update matching files early, without relying solely on complete fileset comparisons.
Minor Fixes & UI Enhancements
Prevented reprocessing of filesets in set.dat if a key already exists in subsequent runs.
Passing the --skiplog CLI argument to set.dat processing to suppress verbose logs during fileset creation and automatic merging.
Improved filtering in the dashboard adding more fields like engineid, transcation number and fileset id, and fixing some older issues.
Introduced a new “Possible Merges” button in the filesets dashboard to manually inspect and confirm suggested merges.This feature is backed by a new database table that stores fileset matches for later manual review.
During this week of GSoC, I mostly focused on adding TTS to MADE, with some work on ADL as well. A PR has been made for MADE, though there may be more work for it in the future.
MADE
Most of my week was dedicated to adding TTS to MADE. MADE – or at least, Return to Zork – offered many new challenges. For one, rather than displaying text one piece at a time, like I’ve seen in most of the engines I’ve worked with, Return to Zork displays several pieces every frame across multiple channels. This meant that I couldn’t simply track the previously said text in a single variable to avoid speech loops, as it would be repeatedly overridden. To solve this problem, I tried several different approaches. First, I tried finding the function that actually sets most of the text, which I found in the form of sfReadMenu. This function does set the text one piece at a time, which would be good for voicing. However, the issue with it is that sfReadMenu is called several times when a new scene loads, even when the text that it’s setting isn’t visible, resulting in extraneous text being voiced. I tried to check for any flags that could identify this text as invisible, but I didn’t find anything good to use, so I scrapped this idea. I then thought of tracking several different previously said texts and their channels in an array, but this seemed unnecessarily cumbersome, especially because the channels are often changing. Eventually, I settled on adding a variable for the previously said text to the individual channels themselves, and using this variable to check if text should be voiced. This worked quite well in most cases, and after accounting for exceptions where the spoken text should be queued, it resulted in good TTS for much of the game.
MADE, however, had a few additional problems. For one, it seems to handle hovering over objects completely within the game scripts, which means that there’s no easy place to detect when the cursor hovers over buttons in Return to Zork’s save/load screen. After trying to search for possible conditions I could use, I eventually decided to recreate the click boxes for the save, load, and cancel buttons, as well as the text entry boxes for new save slots. Fortunately, none of the text for these buttons is in the form of an image, which meant I could find the IDs for each individual object or menu that displays them and use these IDs to get their text, a strategy that should work across languages.
Another issue arises with the tape recorder in Return to Zork. The text for the name and numbers on each tape recorder entry are displayed within channels, but the text that corresponds to these pieces – such as “name” or “trk” – are in sfDrawText, which results in awkward, out-of-order voicing (for example, when the track is 001 and the name is Wizard Trembyle, TTS would say “trk, name, 001, Wizard Trembyle”). Fortunately, this could easily be avoided by not voicing these pieces in sfDrawText, and instead fetching this text when voicing the channel text, resulting in cleaner voicing (such as “trk: 001, name: Wizard Trembyle”). Unfortunately, the tape recorder is interactive, which further complicates matters. Voicing each piece separately can result in issues if the user switches between entries where a number is the same, such as one entry having max track 001 and the other having max track 001, since the previously said text won’t change and thus it won’t be voiced at all. Getting around this involved voicing all pieces – name, track, and max track – whenever the name changes, as it should be unique for each entry, alongside voicing the track individually in specific situations to account for the user switching tracks. Voicing the time was another matter: if it’s voiced whenever it changes, then the TTS system will repeatedly try to voice it as it ticks upward, resulting in awkward interrupts or unnecessary lag. I eventually decided that it was best to try to voice it when the sound clip ends, so it’ll only be voiced at the moment it stops moving. These techniques involved keeping track of flags and the status of the previously said variables, since they’re modified and retrieved each frame, but it seems to have resulted in more responsive voicing of the tape recorder.
In conclusion, MADE was a little more challenging than expected, since Return to Zork has several simultaneous channels and handles much of its logic within the game scripts. Nonetheless, it was a fun experience adding TTS to it. It seems finished now, though it remains to be seen how well it will handle different games or translations, and it has a few hardcoded translations that need verification.
ADL
ADL has seemed quite easy so far. As an engine for rather simple text-based games, almost all of its text is handled by Display::printString and Display::printAsciiString. It doesn’t even require tracking the previously said text, since these methods to display text are called only once, and there are no buttons that require responsive voicing. Thus, I think I’ve almost finished TTS for it, though challenges may arise later.
Conclusion
Over week 4 of GSoC, I’ve completed more TTS implementations, with MADE finished and ADL mostly finished. MADE offered the most challenges, due to its different ways of handling text, but its lack of text in the form of images was welcome. It’s been an interesting week, and I’m excited to explore more engines. Next week, I’ll be looking to finish TTS for ADL and begin work on TTS for Parallaction.
Do you want to expose the deepest secrets of the Tremendous Corporation® and save the games industry? Are you interested in helping a young man who's out of order in an alien future? Or will you join the story of Nathan, who wants to get a second chance in life?
If you answered "yes", then get ready, as we are happy to announce full support for all games based on the SLUDGE engine. The original work began back in 2017, by one of our GSoC students, Simei Yin. Now, after many years of development, the following games are available for public testing:
Above The Waves
Cubert Badbone, P.I.
Frasse And The Peas Of Kejick
The Game That Takes Place On a Cruise Ship
The Interview
Lepton's Quest
Life Flashes By
Mandy Christmas Adventure
Nathan's Second Chance
Out of Order
Robin's Rescue
The Secret of Tremendous Corporation
Essentially, this is the list of all the SLUDGE-based games we know of.
In the third week of GSoC, I worked on finishing the WAGE engine. The main things that needed to be done for the engine were: 1) check save/load system and fix the game bugs; 2) to redump the games and add them to detection table.
The save/load system was mostly working when I started, but a there were a few missing cases that needed to be handled. 1) When trying to load a save after loosing the in the game, the console window showed the text from the previous session. The fix was to simply reload the state() and then run the “Look” command, so that we get the initial text about the scene (surroundings) where the main character in located.
Other interesting bug to solve was the crash I was getting after loosing in several games. For example, I would always crash after throwing the “frag grenade” and destroying the spacecraft in the game Bug Hunt. Since we have the sources for the engine, I first tried to check the code there. It was the same. I run the original Java sources in Eclipse, but as it turned out, the game also crashes in that scene. So that meant I had to go deeper in the code logic and find what exactly went wrong there. The problem was that when destroying the spaceship, the script would move the character to “storage scene”, an empty, temporary scene where characters are located during the startup. Since the Scene class contains Rect pointer needed for drawing and the for the storage scene it’s null, the game crashed when trying the draw the scene. Of course, we could add the checks for the NULL pointer, and fix the crash, but that would not be sufficient, since the “Game Over” dialog would not show up. The solution I came up with is to check if the player was moved to this storage scene after making the turn and set the gameOver flag: commit.
After that, I fixed the remaining bugs and the text in the About dialog, which took several iterations since I would discover new problems as run more and more games with different text sizes, fonts, etc.
Then, it was a time to do the re-dumping task. Thanks to my mentor, I already had my Basilisk II setup ready, and it was not that difficult to do the task. My workflow was as follows: I would decompress the game archives inside Basilisk, then move them to place that will be visible from the host (my OS), then use dumper-companion the make sure the file names are nicely puny-encoded. After that, I would check if there are any issues with the games in ScummVM itself, and later add them to detection tables.
This week was quite busy and eventful! I had my keymapper implementations for the Supernova and Voyeur engines successfully merged, and I also completed keymapper support for Titanic and Tetraedge. In addition, I took on an extra task to help normalize all keymapper action descriptions across the project.
Titanic: The Most Challenging Engine So Far
Out of all the engines I’ve worked on so far, Titanic has definitely been the most complex.
Implementing the keymapper took me about four days, most of which were spent trying to understand how input was being handled. Unlike other engines, Titanic processes input through a system of “messages.” Key presses are encapsulated in these messages and passed around to various components that need to handle input. This indirect approach made the input flow harder to trace.
Even though I’m still not 100% confident I fully understand every detail of the mechanism, I eventually got the keymapper working by mimicking how raw keycodes were originally processed. After two days of reading and experimenting, things finally started to click — and from that point on, it was just a matter of replacing keycodes with action-based mappings and writing the action bindings.
Tetraedge: The Easiest One Yet
In contrast to Titanic, Tetraedge was a breeze. The engine only uses three keys and mouse input, so I was able to wrap up the keymapper integration in half a day. Always nice to have a quick win!
Action Label Normalization: A Multilingual Cleanup
ScummVM’s GUI is actively translated into many languages by the community using Weblate. Weblate automatically picks up strings that are explicitly marked for translation in the code.
To reduce the translators’ workload, it’s important that identical strings are reused consistently across the codebase. Weblate treats strings with even minor differences — like different capitalization — as separate entries. Unfortunately, over the years, different contributors have used varying styles (sentence case, title case, etc.) for keymapper action descriptions.
To help streamline translations, I went through and normalized 1100+ action labels to follow sentence capitalization. The task itself wasn’t difficult, but it was quite repetitive. Fortunately, sev helped by providing a git grep and awk-based workflow that made identifying and editing the labels much faster. With that in place, I was able to get through the normalization process much more efficiently.
Wrapping Up
This week, I:
Got my Supernova and Voyeur keymapper PRs merged 🎉
Implemented full keymapper support for Titanic (the hardest one so far)
Added keymapper support to Tetraedge (the easiest one yet)
Normalized 1100+ keymapper action labels for consistent translations across the GUI
Next week, I plan to keep the momentum going and tackle keymapper support for more engines. With each engine, I’m getting a better understanding of the diverse input systems in ScummVM—and figuring out how to adapt the keymapper to each one’s quirks.
This week I switched my task to saving Director movies in ScummVM. This was my main task of focus in my proposal. @sev explained to me that some games need to save .dir movies.
A director movie is in a containerized format. Chunks of different types of files are stored under a resource header. We can load and parse the movies in ScummVM but we cannot save them back since we throw away the things we don’t know yet. However, it is not as simple as reading the data in the file and writing it back to file. Because these games may modify the data inside these chunks. Also, the loading functionality is scattered throughout the Director engine. The ProjectorRays project by @djsrv is a great resource for this task.
So, the task is to keep the unknown data for the sake of saving the movies and to figure out how I can save the chunks that will be modified. All the other chunks that won’t be touched will be written verbatim.
This week, I started by writing the “reverse functions” to the archive loading functions in the Archive class readStream(), readMemoryMap(), readKeyMap()and readCast() namely writeStream(), writeMemoryMap(), writeKeyMap() and writeCast(). The former load an Director movie archive whereas the latter saves the said archive. The rest of the chunks as described in the memory map (chunk ‘mmap’) are written as they are. For now, the movies are being written properly (except for a minor hiccup which I will check out today). Although I have done much testing on the windows, Director 5 movies I have, but @sev asked me to test this with a variety of movies before opening a pull request. Another idea is to store all the data for a specific chunk in a struct for consistency just like how ProjectorRays does. However, ProjectorRays doesn’t handle all chunks.
My initial idea to saving director movies was to move all the loading functions to the Archive class so that it can store them. It will have all the data and write it whenever necessary. However, @sev pointed out, that is a substantially flawed approach. There are far too many chunks to move to the Archive class, they don’t belong to the Archive class and it will be an unmaintainable mess. Thankfully, I didn’t spend much time on it.
Now, this week, I need to start writing Cast data (reversing loadCastData()).
I had to go to Pune again, for the publishing of our college magazine. Finally, that responsibility of mine is over. This week, I was logging in late, which moving forward, won’t be happening.
I’ve finally started to pick up the pace (although still slow). Things have started to look little brighter now…
Most of this week was spent rewriting the logic for processing set.dat files, as the previous implementation had several inconsistencies. As shown in Figure 1, the earlier logic directly compared checksums to determine matches. However, this approach only worked when the file size was smaller than the checksum size (typically md5-5000), since set.dat files only include full file checksums. This caused us to miss many opportunities to merge filesets that could otherwise be uniquely matched by filename and size alone.
Fig.1 : Previous query used in matching.
Since set.dat files only contain entries that are already present in the detection results (with the exception of some rare variants I discovered later in the week), we should typically expect a one-to-one mapping in most cases. However, some filesets in set.dat can correspond to multiple candidate entries. This happens when the name and size match, but the checksum differs—often due to file variants. This case needs to be handled with manual merge.
Previously, the logic for handling different .dat file types was tightly coupled, making it hard to understand and maintain. I started by decoupling the logic for set.dat entirely. Now, the candidates for the match for set.dat filesets are filtered by engine name, filename, and file size (if it’s not -1), excluding out the checksum. It is made sure that all the detection files(files with detection flag set to 1) follow the condition.
Initially, I was filtering out the fileset only with the highest number of matches, assuming it was correct. However, that approach isn’t reliable—sometimes the correct match might not be the largest group. So all these candidates need to go for the manual merge. Only when all checksums match across candidates can we be confident in an automatic match.
I also added logic to handle partial or full matches of candidate filesets. This can happen when a set.dat is reuploaded with changes. In such cases, all files are compared: if there’s no difference, the fileset is dropped. If differences exist, the fileset is flagged for manual merge.
Finally, I handled an issue with Mac files in set.dat. These files aren’t correctly represented there: they lack prefixes and have checksums computed for the full file rather than individual forks. So, these filesets are dropped early by checking if no candidates are found for that fileset after SQL filtering.
Other Updates
During seeding, I found some entries with the same megakey, differing only by game name or title. Sev advised treating them as a single fileset. So now, only the first such entry is added, and the rest are logged as warnings with metadata, including links to the conflicting fileset.
Other fixes this week included:
Removing support for m-type checksums entirely (Sev also removed them from the detections).
Dropping sha1 and crc checksums, which mainly came from set.dat.
Next Steps
With the seeding logic refined, the next step is to begin populating the database with individual set.dat entries and confirm everything works as expected.
After that, I’ll start working on fixing the scan.dat functionality. This feature will allow developers to manually scan their game data files and upload the relevant data to the database.
Another week of GSoC has passed, with more text-to-speech work done, as a PR has been opened adding TTS to Draci. In addition, TTS for WAGE and CruisE has been merged, and TTS for Cine has been more rigorously tested.
Draci
Much of my time this week was invested in adding TTS to Draci. In terms of text complexity, Draci was roughly below average: most of its text was displayed in a few places, which were easy to track down. There were, however, several considerations regarding the engine. For one, Draci’s Czech and Polish translations have full (or almost full) voiceovers, while the German and English translations only have subtitles. To account for these differences, I split TTS into subtitles and objects, my first time doing so, and had to refrain from voicing subtitles for versions with voiceovers. Beyond that, I had to implement making the subtitles last as long as the TTS is speaking, as the subtitles normally move so fast that the speech can’t keep up; TTS for missing voice clips for the Czech and Polish versions, which received its own option at the suggestion of my mentor; and recognition of cases where the text is only punctuation, which isn’t voiced by the TTS system, and is thus immediately skipped (the text remains on screen for as long as TTS is speaking, meaning if the text isn’t voiced, the system immediately believes that it’s time to move to the next subtitle). All of these were interesting exercises in timing and accommodating different versions.
In my opinion, one of the most interesting components of Draci was the encoding. Draci uses encodings not present in ScummVM’s CodePage enum, which meant I had to hunt them down and manually write a translation table. Fortunately, the Dragon History website states that the Czech, English, and German translations all use Kamenický encoding, with the German translation having an exception for ß. This simplified the process greatly, as it just required creating a translation table for Kamenický encoding and converting the bytes, a similar process to the fix for TeenAgent’s Russian translation. Unfortunately, the Polish encoding is only described by the same website as “some ridiculous proprietary encoding”. I initially tried observing the bytes of the in-game strings to figure out the encoding for each character, but I worried that I’d miss a few characters. Instead, I found the character sets in the original Draci source code, which showed the bytes of UTF-8 encoding for each character as characters in the Windows-1250 code page. This meant that all I had to do was translate these characters according to Windows-1250 encoding to bytes, then combine these bytes to obtain the UTF-8 encodings for the Polish characters. That was most of the work for encodings, aside from altering the encoding table for German and English to replace certain Czech characters with equivalents in those languages, as TTS for the credits struggled to pronounce those characters.
Ultimately, Draci’s encodings were interesting to explore, and I’m very glad that the Dragon History website had them listed.
MADE
At the end of the week, I started work on MADE, but more work must be done for it. It seems that Return to Zork handles text a little differently from what I’m used to, as it displays text in several simultaneous channels every frame. My usual solution of tracking the previously said text with a variable won’t work here, as it’ll be overridden by the other text. I’ve thought of a few possible solutions, including tracking several different previously said texts or finding where the text is set – which I may have already found, but I need to do more investigation – and I plan to explore them over this week.
Miscellaneous
Aside from Draci and MADE, I fixed a few problems with CruisE – thanks to my mentor for noticing that the encoding was wrong for some of the versions, among other necessary changes – and tested TTS for Cine for other versions. For Cine, I was worried that my method of checking image names for text in the form of an image would fail across different versions of Future Wars and Operation Stealth, but I was pleasantly surprised to not find any issues, at least in my testing. The only changes I ended up making were adding a few new translations, fixing some small caveats with the encoding for the German and French translations, adding line-by-line TTS for the Operation Stealth copy protection screen (which I initially had, but I removed due to fears that it wouldn’t work across versions), adding TTS for Future Wars’s grid copy protection screen, and adjusting for exceptions across versions (such as the US Amiga version of Future Wars using a different copy protection screen from all other Amiga versions). Cine is thus much more thoroughly tested across different game editions now.
Conclusion
This week involved more work on TTS, with testing for Cine, TTS implementation for Draci, and the beginnings of TTS implementation for MADE. Exploring Draci’s encodings was the most entertaining part, and I hope to bring over my more in-depth knowledge of encoding methods into future implementations. Next week, I’ll be finishing TTS for MADE and beginning TTS for ADL, alongside any changes I need to make to my open PRs for Cine and Draci.
Paris, July 24, 1914. We are excited to announce that Jordan Mechner's acclaimed real-time adventure The Last Express is now ready for testing in ScummVM!
Step into the shoes of Robert Cath, an American doctor on the run from murder charges, as he boards the legendary Orient Express for what would be its final journey before the outbreak of World War I.
What makes this adventure game truly unique is its time-based gameplay - the game takes place in real time, and characters move within the train following their own schedules, speaking in their native languages, and living their lives whether you're watching or not. But don't worry... you can always rewind time if you missed anything important!
To test this game, you'll need the original version of the game (either from the original hybrid DOS/Windows/Mac CDs or from GOG.com) and a daily development build of ScummVM. Please note that the Gold Edition available from Steam is NOT supported - only the original version is compatible at this time. The engine supports the following languages for the game: English, French, German, Italian, and Spanish. We couldn't locate the Japanese version at the moment, but please contact us if you have it!
Grab a daily build, have our testing guidelines handy, and get ready for an incredible journey through history, mystery, and one of the most innovative adventure games of the 1990s — and besides... the Orient Express is such a beautiful train, wouldn't you want to take some screenshots while you're there?
Welcome to the weekly blog.
After wrapping up work on macfiles last week, I finally moved on to testing the first two phases of the data upload workflow — starting with scummvm.dat (which contains detection entries from ScummVM used for populating the database) and set.dat (data from older collections, provides more information).
Database Changes: File Size Enhancements
Before diving into the testing, there was a change Sev asked for — to extend the database schema to store three types of file sizes instead of just one. This was necessary due to the nature of macfiles, which have:
A data fork
A resource fork
A third size: the data section of the resource fork itself
This change introduced significant modifications to db_functions.py, which contains the core logic for working with .dat files. I had to be careful to ensure nothing broke during this transition.
Punycode Encoding Logic Fixes
At the same time, I fixed the punycode logic in db_functions.py. Punycode encoding (or rather, an extended version of the standard used in URL encoding) is employed by ScummVM to convert filenames into filesystem-independent ASCII-only representations.
There were inconsistencies between punycode logic in db_functions.py and the original implementation in the Dumper Companion. I made sure both implementations now align, and I ran unit tests from the Dumper Companion to verify correctness.
Feeding the Database – scummvm.dat
With those fixes in place, I moved on to populating the database with data from scummvm.dat. While Sev was working on the C++ side to add the correct filesize tags for detections, I ran manual tests using existing data. The parsing logic worked well, though I had to add support for the new “extra size” fields.
Additionally, I fixed the megakey calculation, which is used later when uploading the scummvm.dat again with updates. This involved sorting files alphabetically before computing the key to ensure consistent results.
I also introduced a small optimization: if a file is less than 5000 bytes, we can safely assume that all checksum types (e.g., md5-full_file, md5-5000B, md5-tail-5000B, md5-oneMB, or the macfile variants like -d/-r ) will be the same. In such cases, we now automatically fill all checksum fields with the same value used in detection.
Uploading and Matching – set.dat
Finally, I worked on uploading set.dat to the database, which usually contains the follwoing – metadata (mostly irrelevant), full size checksums only and filesizes.
scummvm.dat doesn’t contain full file checksums like set.dat, so a match between files from set.dat and scummvm.dat is only possible when a file’s size is less than the detection checksum size, generally 5000 Bytes.
This transitions the status from “detection” to “partial” — we now know all files in the game, but not all checksum types.
If there is no match, we create a new entry in the database with the status dat.
Fixes :
There was an issue with the session variable @fileset_last, which was mistakenly referencing the filechecksumtable instead of the latest entry in the filesettable. This broke the logic for matching entries.
When a detection matched a file, only one checksum was previously being transferred. I fixed this to include all relevant checksums from the detection file.
Bug Fixes and Improvements
Fixed redirection logic in the logs: previously, when a matched detection entry was removed, the log URL still pointed to the deleted fileset ID. I updated this to redirect correctly to the matched fileset.
Updated the dashboard to show unmatched datentries. These were missing earlier because the SQL query used an inner JOIN with the game table, and since set.dat files don’t have game table references, they were filtered out. I replaced it with a LEFT JOIN on fileset to include them.
That’s everything I worked on this past week. I’m still a bit unsure about the set.dat matching logic, so I’ll be discussing it further with Sev to make sure everything is aligned.
My second week of GSoC is over and has been full of different activities: I wrote two news items, took screenshots for qdEngine and SLUDGE games, started working on a new engine (WAGE) and had a video meeting with my mentor.
As I wrote in my previous posts, the main work on qdEngine is done, but few things were needed before the announcement: preparing screenshots, creating wiki pages for the games and writing the announcement post itself. And I started the week this those tasks. Taking the screenshots turned out to be quite useful as I discovered the regression caused by one of commits. In particular, when playing “Wait for it! Issue 3. Song for a Hare” I found that the game went black screen and stalled after playing a video. After probing the packets with ffprobe, I found that it happened because of a packet with invalid timestamp, which I did not consider in my PR. I quickly fixed the issue, and the videos are playing normally now. After the screenshots, I created a wiki page for the newly added games and I wrote a news item to announce the full support for qdEngine.
Following the qdEngine, I did the same kind of tasks for the SLUDGE engine. Again, taking the screenshots were useful as I discovered that I did not cover one case with save/load system – autosaves. By that time, I was very comfortable with the saves, so it was an easy fix.
After the SLUDGE, I started working on WAGE engine. This engine is also very close to completion with only a few small things left to be done. However, to properly implement the leftover features and fix the bugs, I needed to compare with the original, which wasn’t that easy to set up due to intricacies of the file system of Macintosh. But that wasn’t that big of an issue, thanks to ScummVM project leader and my mentor – Eugene (sev), who kindly guided and explained every step for me in video call.
Right now I am continuing my work on WAGE. I’ve already fixed a couple of bugs, and there is only problem with the text (About dialog) left for now. After that, I plan to redump the games which is also part of the tasks for this engine.
This week, I continued working on implementing keymapper for more game engines. The highlight was getting my SLUDGE keymapper PR merged, and diving into two new engines: Supernova and Voyeur.
Supernova Engine: The Challenge of Custom Actions
The Supernova engine powers the Mission Supernova series—old-school point-and-click adventures that are surprisingly engaging (and tough!). To test my changes, I had to play through parts of the game, and I genuinely enjoyed the puzzles and storytelling, despite the lack of modern visuals. Honestly, without the walkthrough, I would’ve spent more time solving puzzles than coding the keymapper.
Unlike SLUDGE, this engine uses engine-driven input handling, which gave me the flexibility to define custom actions instead of just simulating keypresses. Why is this better? Because:
Custom actions give the engine full control over what happens when a key is pressed, while mimicking keypresses can cause issues like duplicate handling or unexpected default behavior still triggering.
The biggest challenge I faced in Supernova was how the engine handled keyboard input. It stored the raw keycode of every key press in a single variable called _key, which was then used for both gameplay actions and full-text input.
This became a problem when introducing custom actions. Unlike raw keycodes, custom actions are more abstract—they represent “what to do” rather than “what key was pressed.” But if I allowed both to be handled the same way, the engine might mistakenly store a custom action as if it were a real keypress, which would break things like text input.
The Solution
I separated the handling of keycodes and actions using two variables:
_key → stores the raw keycode (used only when text input is needed)
_action → stores the custom action (used during normal gameplay)
Then, I made sure the engine would:
Disable the keymapper when full keyboard input was required (e.g., entering text). This way, real keypresses would go through, and _key would be filled correctly.
Enable the keymapper during gameplay. In this mode, keypresses would be intercepted, converted into actions, and stored in _action.
This setup allowed me to cleanly separate when to use keycodes and when to use actions, without breaking any existing input logic. Once this system was in place, replacing the original key handling with action-based logic was pretty straightforward. All I had to do was replace hardcoded keycodes with custom actions.
Voyeur Engine: Simple, Then Smarter
Compared to Supernova, Voyeur was a breeze. The game is almost entirely mouse-driven, with very little keyboard interaction. I initially thought I could get away with two simple binds—left and right click—and be done in a few hours.
But… why stop there?
I decided to improve it by breaking the keymap into multiple context-specific keymaps, depending on where the player is in the game. For example, different menus or in-game sequences would have their own set of keybinds with clearer action labels.
The tricky part was figuring out where in the code to enable/disable each keymap. That took a whole day of reading through the engine, but once I nailed it, the final implementation was clean and intuitive.
Wrap-Up
This week, I:
Got my SLUDGE keymapper PR merged 🎉
Implemented full keymapper support for Supernova (with custom actions)
Added smarter, context-aware keymapping to Voyeur
Next week, I plan to continue this momentum and bring keymapper support to more engines. The work is getting smoother now that I’m more familiar with ScummVM’s input systems—and every engine brings a new twist to solve.
This week, I actually got to implementing the Movie castmembers in ScummVM. The idea behind this was to use the MovieCastMember as a wrapper around a Movie member. This Movie class variable will handle the updating of frame number, execution of Scripts, handling events, etc. just like how a the main movie handles it. The tricky part was that it does not have a window. The main movie has a dedicated window where it can render its Sprites. The Movie castmember needs to be rendered in a box inside this main window. For this purpose when the main window updates (renders the next frame in the main movie) it also asks the MovieCastMember for its updated sprites using a function called getSubChannels(). So, the idea was simple, before returning these sprites, we ask the Movie castmember to “step”, update its own state.
But, this simple idea lead to me facing many difficulties. First of all, I had to include a check at each place the _window member was accessed in all functions in classes Movie and Score. The class Window also tracks the the Lingo State, which was now inaccessible for our MovieCastMember. So, the Window class needed refactoring. I separated all Lingo State keeping functionality into a separate class LingoStateKeeper and migrated all the relevant functions from Window. I added a member _stateKeeper to Window and Score. There was also a question of whether MovieCastMembers can switch movies using the go to movie ... command. I created a movie as a test in the original Director to check this. @sev asked me to add it to the director-tests repository.
This task is still incomplete. I’m able to see externally linked movies in ScummVM’s Director but they are stuck. They still do not allow execution of their own scripts.
What was thought to be a two day job, ate my entire week. I didn’t have my Director basics down. I hadn’t read the Manuals that were given to me a weeks ago. I made obvious logical mistakes, I got stuck at multiple points… to the point that @sev has asked me to take a step away from this task. I was working on processing Lingo script events the last. He will continue from here. He asked me to focus on my main task of saving Director Movies. Hopefully, I’ll learn enough to come back to this task and be able to finish it.
I’m worried at this pace, whether I’ll be able to complete my project or rather make a meaningful contribution to ScummVM in a reasonable time period. I wanted to work yesterday (Sunday) but I thought it’d best to take the day off and read the Interactivity Manual (even though its a bit late now) and start working today (Monday) with a fresh mind.
This week I’ll focus on studying ProjectorRays to start writing director files. Study the different chunk types in a .dir file.
So… the first two weeks for me have been much less than perfect. I’m too focused on the greener side of the river forgetting there is deep water in between, gotta pass through that first or I’ll drown… But I’m optimistic. Even though I couldn’t keep the promise of having a good week from last week, I hope to have a comeback week starting today.
Another week of GSoC has passed, and I’m fairly satisfied with my progress. This week, I mostly focused on adding TTS to CruisE, and I’ve opened a PR for it. However, I also spent some time working on Draci, and on updating older PRs.
CruisE
I began this week with adding TTS to CruisE. Fortunately, CruisE wasn’t too difficult to work with, as it has very few cases of text in the form of an image. In addition, most of Cruise for a Corpse’s text is displayed from renderText, which made it much easier to identify where text is displayed in the code. The difficulty with Cruise for a Corpse, however, came from making it user-friendly. For example, text can’t simply be voiced from renderText, as there are cases when the text appears on screen a significant amount of time after renderText is called, or cases where the text isn’t visible, such as when the user tries to open the inventory in the copy protection screen. In a similar manner, text can’t be voiced from createMenu, because there are times when it shouldn’t be voiced (mainly when the inventory is empty). It also can’t be voiced from createTextObject, which handles much of the game’s text, because freezeCell is sometimes called on objects created with this method, which means that the text appears on screen later. Voicing text for CruisE, therefore, required addressing these exceptions accordingly, such as by storing the text from createTextObject and voicing it when the cell is unfrozen; queuing the text of the first menu item hovered over after a menu is opened instead of interrupting the current speech, so the title of the menu is always voiced; and checking for button input in Op_GetMouseButton to stop TTS when skipping through dialog or a cutscene. I believe that these changes make the TTS for CruisE more responsive and more accurate to the gameplay.
Ultimately, actually finding where text was displayed in CruisE wasn’t that difficult. The more time-consuming part was finding and accounting for the many exceptions, skips, and places in the code that freeze, show, or hide text, in order to create a user-friendly experience. It was entertaining to look through the code and ponder the best implementation, as its code was more spread out than that of Cine or WAGE.
Miscellaneous
After CruisE, I worked on various components related to my project. For one, I revisited my WAGE PR, which required a fair number of changes, including resolving some difficulties across games that I missed (in some cases, different WAGE games handle the same circumstances surprisingly differently, such as Eidisi only calling renderSubmenu when hovering over a new item, while Ray’s Maze and Another Fine Mess call it every frame, requiring a check for the last submenu item that was hovered over in these games). I also worked on fixing TTS for TeenAgent’s Russian translation, which uses a custom encoding that has to be replicated for proper voicing – fortunately, it seemed that this encoding followed a simple pattern of adding a number to the original character to obtain a Cyrillic character in UTF-8, though since I don’t speak Russian, I’m uncertain if this works in all cases.
Beyond that, I started work on TTS for Draci. So far, Draci seems even simpler than CruisE, with fewer exceptions and oddities. Good progress has been made on Draci, but there is always a chance that something unexpected will emerge.
Conclusion
CruisE was a somewhat more complex engine to add TTS to than WAGE or Cine, and it was entertaining to hunt down its different exceptions and learn how it handles its inputs and menus. A PR has been opened for it, and while I imagine that there’ll be more work for it in the future, the hardest part for it is done. Next week, I’ll be continuing work on Draci, and I look forward to completing TTS support for it.
A young physics professor named Lester conducts a particle experiment. Suddenly, something goes wrong, lightning strikes, and in a moment Lester finds himself in a strange alien world. Now he must fight for his life in this strange place. Luckily, he'll find help along the way..
The ScummVM Team is pleased to announce full support for Out of this World (aka Another World). This is particularly noteworthy, since the original version of this engine was briefly added to ScummVM over twenty years ago.
Help us test the game by grabbing a daily build. Read through our testing guidelines. Currently, only the original DOS and Windows 3.1 versions are supported, though with sounds effects only, and not the music. Also, unfortunately, the Anniversary Editions are not yet supported. Additionally, like the original, you can't save your game, but you can use the 'c' key to open an area select dialog to enter a code to resume where you left off. And please take some screenshots along the way.
Lester's fate is in your hands. You'll deal with dangerous fauna, bloodthirsty aliens, and fiendish puzzles as you flee for your life.
It's been seven months since the first few qdEngine games were supported. After further work on the engine and playtesting, we are pleased to announce support for the remaining games:
Little Longnose (Russian: Карлик Нос, Lithuanian: Nykštukas Nosis)
Pilot Brothers 3: Back Side of the Earth (Russian: Братья Пилоты. Обратная сторона Земли)
Pilot Brothers 3D. The Case of Garden Pests (Russian: Братья Пилоты 3D. Дело об Огородных вредителях)
Pilot Brothers 3D-2. Kennel Club Secrets (Russian: Братья Пилоты 3D-2. Тайны Клуба Собаководов)
Features of National Fishing (Russian: Особенности национальной рыбалки)
Mom Don't Worry (Russian: Мама, не горюй)
Dog-n-cat: In the Footsteps of Unprecedented Beasts (Russian: Агентство "КотоПес": По следам невиданных зверей)
Dog-n-cat: Island of Dr Ratiarty (Russian: Агентство "КотоПес": Остров доктора Крысарди)
To play the games, you will need a daily development build. If you encounter any issues, please submit the bug reports to our issue tracker.
We are also still looking for a Lithuanian copy of the Little Longnose game (Nykštukas Nosis). If you have a copy or know where to obtain one, it would be greatly appreciated by the team.
In the first week, I started with finishing the work on qdEngine, then began working on the SLUDGE engine.
With qdEngine, what needed to be done was to improve the performance in the game Dog-n-cat: In the Footsteps of Unprecedented Beasts. After a little investigation, it turned out that whenever the tiled animations were drawn, the uncompress() method was called. This was very costly, and the solution was to cache the already uncompressed animations.
After finishing with this task, I was assigned to finish the SLUDGE engine. The engine is almost finished, with only a couple of small things to be done. I began with implementing the support for extended saves, which turned out to be a little harder than I expected. In particular, there was a problem with the saving the files in the ScummVM format. The saves with simple names are in this format: %s.%03d, where d is slot number. At first, I didn’t know how to get the slot number from the name of a save. This was needed for loading the save files in the games. The problem was that the SLUDGE game interpreter is based on stack machine and the only thing which is pushed to stack when loadGame function is called is the name (description) of the save.
This problem took quite a while to solve, but after many unsuccessful attempts and tries, I finally came up with the solution. I realized that almost every game when loading/saving the game shows the save file list:This meant that I could create a lookup table that maps the save name to slot number of the save file and use it for saving/loading procedures. And indeed, it worked out.
There was also a little interesting task was to testing transition modes of the engine. For that, I needed to create a demo with SLUDGE development kit, run it on original interpreter and on ScummVM, compare, and see if they are different. The transition modes appeared to be the same, so there was no additional work needed from my side.
After that, I quickly implemented return-to-launcher feature, fixed a bug in thumbnail loading and wrote a code to simulate the right click of a mouse. Lastly, I made it so message box with a specific message set by a game pops up when exiting the game:
Message box in Cubert Badbone, P.I.Message box in Verbcoin 2.
Okay, Week 1 over! Hussshh! Not how I imagined it would go.
I started the week by “trying” to work on my unfinished QTVR task. I made very minor improvements, but they were not my main goal. Basically, the QTVR decoder in ScummVM uses a two step approach to projecting a cylindrical panorama in ScummVM. First Step: Cylindrical projection correction, Second Step: Perspective projection correction. I tried to implement a warp mode 1 in it by skipping step 2. But apparently that’s not the correct solution. This introduces a wobble to the image when tilting up and down. I tried working on it, but couldn’t do it. @sev suggested I abandon it because I have to focus on my main task of saving Director files.
This week I worked on familiarizing myself with the Director 5. I started reading the Director 2 Interactivity Manual. My main task for this week was to understand how Director Movies can be embedded into other Director Movies as cast members. Currently this functionality is missing from ScummVM Director. We cannot load external Movies into the current movie. It was my task to implement this functionality. This is used extensively in startrek, a game we’re hoping to support in ScummVM, which has a main (Director) movie that loads other (Director) movies which store different things (e.g. the floor plans of different rooms on the USS Enterprise NCC-1701-D spacecraft). But so far I’ve been struggling to carry out the task.
To get acquainted with Director and to understand what the task is, @sev asked me to create a simple movie in Director 5. I had to embed an external movie into that just like startrek does using a lingo script. Although, making the movie was simple enough, I failed to see the point behind the exercise. Instead of embedding an external movie, I instead imported all the cast members of my external movie into my current movie and basically mimicked the external movie in my current movie (to not do so was the entire point). After a helping hand from @sev, I was able to make the movie. I will be using this movie as a unit test to check whether the movie cast member loader (which I will start implementing starting next week) works or not.
@sev wanted me to do another thing which was to create a loader for Score that works for all of these cast members: scores, filmloops (very similar to scores but very little or no external control using scripts) and movies (which have their own score and casts and scripts unlike filmloops). But my progress was slow, I am getting stuck often; which lead @sev to tell me that I should immediately ask for help if I feel stuck. I have very little time to make this happen and I can’t afford to spend much time on just getting the basics done. Other students are well on their way to complete their respective tasks.
So to expedite this process @sev and I had a video call where he explained how the Director works, how its components work, how it is implemented currently in ScummVM, how are they loaded, how all of this is tied to my main task of saving movies in ScummVM, and what is it exactly that I need to do. This helped a lot. I now have a clear picture of what needs to be done. From next week I’ll start by implementing a common loader for Score, filmloop and movies in ScummVM’s Director.
I hope I’ll have better news and a better blog next week!
This week marked the beginning of my journey with ScummVM for Google Summer of Code, and I kicked things off by working on the SLUDGE engine.
For those unfamiliar, SLUDGE is an engine that enables developers to create and publish their own point-and-click adventure games. These games are highly script-driven, especially in how they handle input. Keyboard input, in particular, is passed directly to the script, and from there, it’s entirely up to the game developers to define how those inputs are interpreted and used in their game logic.
My main goal for the week was to implement ScummVM’s keymapper support in the SLUDGE engine. However, this came with its own set of challenges.
Each SLUDGE-based game defines its own key bindings for different actions. That meant I had to manually identify the default controls for over 10 supported games. This process was quite time-consuming, as there was no centralized documentation. In some cases, I found the information in the game’s source code, in others through manuals, and occasionally I had to just play through the game and note what each key did.
While this part of the task was tedious, it was relatively straightforward. The real challenge came when I had to figure out when to disable the keymapper.
Why? Because some games allow players to enter custom save file names, which means they need unrestricted keyboard input. If the keymapper remained active during such input modes, it would interfere with typing. Unlike many other engines, SLUDGE doesn’t offer a built-in pause menu or modal state, so each developer handled menus differently.
After some digging through the engine source and various game scripts, I discovered two important functions: `freeze()` and `unfreeze()`. These were used every time a menu was shown or hidden. While not all menus required full keyboard input (most only needed mouse interaction), this discovery gave me a reliable enough hook. I could safely disable the keymapper when a menu was active and re-enable it when gameplay resumed.
With that, I successfully implemented keymapper support for the SLUDGE engine in ScummVM!
Welcome to this week’s blog. Most of the time this week was spent fixing the portability of the Mac files. So the plan was to test the working of the mac files on both Python and C++ side. On checking the C++ side halfway through, we realised, that some code was broken and was giving incorrect results. So, Sev decided to take a look at it himself while I started working on the same task on the Python side.
On the Python side, the code had three main issues:
Not all Mac file variants were being covered.
Fig. 1 : 7 Mac file variants ( Image taken from macresman.cpp -> MacResManager::open() )
Instead of using the data section of the resource fork, the entire resource fork was being used for the checksum calculations, which was different from what the C++ side was doing.
Fig. 2 : The data section of the resource fork had to be separately extracted
There was no file filtering, which caused problems when Mac files were present – specifically, AppleDouble and raw resource fork files, which had their forks spread over multiple files. Instead of showing a single file entry with all the checksums, extra entries were incorrectly displayed as non-Mac files.
Fig. 3 : First file entry should not be a part of this game entry.
I corrected all these issues. For filtering, I added 7 different categories for each file – NON_MAC, MAC_BINARY, APPLE_DOUBLE_RSRC, APPLE_DOUBLE_MACOSX, APPLE_DOUBLE_DOT_, RAW_RSRC and ACTUAL_FORK_MAC.
Fig. 4 shows consistent output for the all the mac file variants. Next task is to create proper test suites for its verification and check the workflow with the C++ side.
Fig. 4 : Checksum calculation of all 7 macfile variants on python side
The first week of GSoC is over, and I’m fairly happy with how it went. My TeenAgent PR was merged, and I’ve opened two new PRs: one adding TTS to WAGE and one adding TTS to Cine. They’re still under review, with Cine requiring more testing and translation verifications, and I imagine that there will be more work to be done with them, but the hardest parts – getting familiar with the engines and adding TTS to most of their text – are over.
WAGE
I started this week with WAGE, which was a relatively simple engine. There was an abandoned PR adding TTS to this engine that I picked up, but while it provided a base for me to work with, it was buggy and unfinished, meaning there was still a significant amount of work to be done. As for the engine itself, WAGE’s games were almost entirely text-based, with few graphical components. For me, this had its benefits and disadvantages. On the positive side, I didn’t have to worry much about hovering over objects, and finding where text was output to the screen wasn’t too difficult: nearly all of the text was in the form of the action log, which was modified by only a small number of functions. On the negative side, the greater quantity of text meant that there was more to look at and be aware of.
For the most part, adding TTS to WAGE was simple. It was a matter of adding a toggle, identifying the small handful of methods that displayed text, and feeding it through the TTS manager. There was no need to clean up any of the text. I soon encountered a caveat, however, with how WAGE handles its command menu. Rather than being embedded into the engine, WAGE uses the MacWindowManager class to manage its menu, including submenus and dialogs. My first approach was to identify and process this text inside WAGE’s Gui::processEvent method by retrieving the menu item that the mouse is over from the MacWindowManager and voicing its text. This worked fine initially, until I tried to voice the buttons: once a MacDialog is open, it pauses the loop inside of WageEngine::run, which is what runs Gui::processEvent. Without Gui::processEvent running, I couldn’t check for the mouse hovering over a button from it. At this point, I realized that it would be difficult to keep everything exclusively in WAGE itself, so I added TTS code to MacWindowManager, which worked much better. I did end up restoring some of my original code to Gui::processEvent for menu items, since MacMenu didn’t seem to have a trigger for hovering over a menu item (only for clicking one, and I wanted to voice the menu item as soon as the user hovers over it). The end result was TTS working as expected for the command menu.
Ultimately, WAGE wasn’t particularly difficult, but the fact that it used MacWindowManager for much of its GUI was an initial challenge. With a little extra code, however, it functioned fine.
Cine
After WAGE, I worked on adding TTS to Cine. Cine’s games, Future Wars and Operation Stealth, have much less text than WAGE games, and much of it is displayed through a few methods. Voicing the majority of the engine’s text was as simple as feeding the text into a small handful of methods, mainly drawMessage, drawMenu, and drawCommand. From there, all that was necessary was making sure it all behaved in a user-friendly way, like voicing the “USE” and “INVENTORY” commands when using the F3 and F4 keys and vocalizing inventory items.
Unfortunately, Cine came with a rather large problem: it has a lot of text in the form of images. Just about any text in Future Wars and Operation Stealth that isn’t directly related to gameplay (i.e. commands and menus) is an image. This includes credits, some cutscene text in Operation Stealth, and everything in the copy protection screens. Two problems resulted from this issue. For one, much of Cine’s work is handled by global and object scripts built into the files, which meant that there was no easy location to find where these images are displayed. As a result, I had to go to the methods that render general images and catch the exact conditions (PRC name, object index and frame, background name, and so on) under which they display. For another, I needed to know the text of these images in all supported languages and versions, so I could hardcode it. This meant a mixture between looking through videos on YouTube and asking the community (thanks to my mentor, criezy, for providing the copy protection images in French, and eientei for providing the German, Spanish, and Italian copy protection fail texts for Operation Stealth). It also meant keeping track of exceptions: Future Wars has two different copy protection screens, one for DOS and one for Amiga and Atari ST; Operation Stealth’s opening credits has a credit for the IBM version only for the DOS version, but its end credits has this credit in all versions; and Future Wars has a different opening title screen for the French version (“Les Voyageurs du Temps: La Menace” as opposed to “Future Wars: Time Travellers” for Amiga and Atari or “Future Wars: Adventures in Time” for DOS). Accounting for these exceptions meant more checks and more text to include, but it has been done.
In the end, getting the copy protection screen and credits to be voiced was the bulk of the work for Cine. Finding the different translations, deciphering the best place to voice them, and adding a new method to recognize hovering over buttons in the copy protection screens was time-consuming, but entertaining.
Conclusion
WAGE and Cine weren’t too difficult to add TTS to, and it was fun to implement it. The most time-consuming was working with Cine’s copy protection screens. There were some difficulties, but through enough investigation, they’ve been resolved. However, as of this blog post, a fair number of translations in Cine need to be verified, and other versions of the game have to be tested.
Next week, I’ll be focusing on adding TTS to the cruisE engine. I’m looking forward to exploring it.
Hi everyone! I’m Prime, a third-year computer science student and lifelong gamer. This summer, I’ll be working with ScummVM as part of Google Summer of Code 2025.
My project, “Add Keymapper to More Games,” focuses on integrating ScummVM’s keymapper system into more of its supported game engines. This will allow players to customize controls across a wide range of classic games — making gameplay smoother and more accessible on different input devices like keyboards and gamepads.
Over the summer, I plan to add keymapper support to over 20 engines. For each engine, I’ll analyze its input system, replace hardcoded key handling with keymapper logic, and thoroughly test the changes to ensure everything works as expected.
A huge thank you to my mentors, the ScummVM community, and the GSoC organizers. I’m looking forward to contributing to the preservation and improvement of these classic games!
In this post I would like to go over some of the highlights of the changes I made to qdEngine.
Adding support for RGBA8888 format
At first, there wasn’t any intent to add a support a 32bpp color format. The engine was using RGB565, and it was pretty much fine with most of the games. However, when I began playtesting Pilot Brothers 3D, and got to a specific place in the game, I quickly noticed that something was off:
The location in the game where I noticed the bug in shadows.
When looking more closely, you can see that the shadows under actor’s bodies are dark green:
dark green shadows
This problem took quite some time to solve. I was checking every line of the drawing functions, comparing with the original source code, but was unsuccessful. Then, I tried to change things, in particular, tried to remove alpha_blend_565() inside the methods, and realized that it was responsible for the shadow color. I looked again at the the original, it saw that it was RGB888. But we were using RGB565, which of course has less bits and a thus created noise for the shadows. Since there is an extra bit for green channel, the shadow was a little greenish. Thus, I proceeded with adding the support for this color format, and indeed got the shadows fixed:
fixed shadows
Although it took about 2 days at the time of fixing that, I now much understand to engine’s drawing functions and learning few things about color formats. Here is the PR for the fix: https://github.com/scummvm/scummvm/pull/6552.
Checking different game (engine) versions and learning Ghidra
One of the aspects of working on QdEngine was comparing the code across different engine versions. The engine developing over time, which meant that because of the changes made to the engine, things such as pathfinding, inventory selection and collision system could work different across different the games. We had sources for multiple versions of the engine, so once the sources are compared and the change is found, a typical fix could be like this:
here the date (number against which g_engine->_gameVersion) indicates the engine version where the a certain change was introduced.
When the sources were unavailable, we had to rely on reverse engineering. For example, we didn’t have the sources for the engines of two games. To find the exact cutoff date, I would write the exact the method and how it was changed to Sev, and he would reverse and tell me the date. Thanks for his help, we were able to fix a decent amount of bugs and make the games completable to the end.
In beginning of May, I started with playtesting Brother Pilots 3D-2 game and could not even run it. This was because one of the tags in the script specific to this game (engine) version was missing. At the time, Sev couldn’t help me, as his laptop was in repairment. So I thought to myself, maybe I could try fixing it myself. I downloaded Ghidra and uploaded three games (including Brother Pilots 3D-2) with consecutive engine versions. Then I found the place where the the tags registered:
Decompiled code of qdscr_XML_Parser() function
After that I took decompiled code of this function, pasted to my editor and diffed between the games:
Comparison of decompiled code of “Features of National Fishing” and “Pilot Brothers 3D-2. Kennel Club Secrets”
In the image above, I compare the code for Brother Pilots 3D-2 and the game that came before it. As can be seen from the image, rotation_angle_per_quant tag is added, so the I adjusted in the code the date and was able to run the game. This was a small change, but quite useful for me, as I learned a little about reverse engineering and Ghidra.
Fixing file loading and adding support for advanced minigames games
This was supposed to be my of the main tasks for this GSoC. I needed to change how the interface for “advanced” minigames worked. Not that they are called advanced, but they are complex in comparison to the ones in other games, and added more things to previous interface.
To understand the problem better, I first tried to run the games, and see where I crash, or where things are supposed to not work. I took the first game, Dog-n-cat: In the Footsteps of Unprecedented Beasts and tried to run it and noticed that I can’t get past the logo image. What’s more important, the memory usage panel in Visual Studio was showing 3.8 Gigabytes:
That was quickly fixed by adding back early end-of-stream check from the original sources:
But something was still missing. I was still getting this warning: MinigameManager::init(): Game could not be initialized. This meant that state (file) loading was unsuccessful and returned false. The code for reading from file looked something like this:
while (!file->eos()) {
index.read(*file);
if (file->eos()) {
delete file;
return false;
}
.
.
.
At this stage, the method could only return false only from code branch I just introduced. So it was reaching the end of stream every time. I did not realize ScummVM’s ReadStream::eos() would return true only when reading beyond file size. In the original, however, their eof() would return true once file size bytes are read, and of course the code in this section relied on that. But once I figured out and adding small fixes, the minigames started showing up. We first though that more complex approach and rewiring would be needed, however, it was much simpler. After that, unstubbing the constructors the each minigame was quite easy and I was able to finish the work early.
In this post, I would like to share a story of solving one of the recent bugs I had in qdEngine.
First, let me tell you about the bug. The problem was with the minigame in the game called “Dog-n-cat: In the Footsteps of Unprecedented Beasts”. In the minigame, players swap and move around triangle pieces to assemble the picture of an animal. The problem was that when swapping triangles, the triangles would move to swapped positions, instead of folding/unfolding in place. And the animation itself had artefacts. This is how it looked before the fix:
and how it looks now, after the fix:
I first thought the problem was with the code logic of this minigame. Even though we already had the sources of the minigames, as it turned out, some of the parts of code were not present there. This led me to think that maybe some crucial piece of code responsible for the rotation was absent. I told this to my mentor – Eugene Sandulenko (aka sev), and he provided me with the decompiled code of the dll for the minigame. But, it turned out to be the same. Then, he went with reversing the several games which were developed in later versions of the engine to see if important differences were introduced. Again, differences that could help were not found.
We started to lose hope on this, so I started to think about the problem from beginning and try a different approach. Sev adviced to look again at what exactly was responsible for rotation and tranformation of the triangle. And I looked very closely at qdScreenTransform::change(), the method that changes object’s angle and scale when transformation happens. In our case, it was called when triangle is transformed during swapping. I noticed that the angle was always 0.0 (wasn’t changing), but the scale in y-axis was decreasing. What’s important is that I was testing it by swapping the triangles vertically. And then, it really came to my mind, that no rotation should not even be happening in this case. Turned out, to simulate the effect of folding, the game was only scaling down the y-component of the triangle object. This meant that I only needed to check the drawing methods with scale parameter. And indeed, after comparing with the original source code, the issue was in the wrong conditional operator:
This bug a took quite some time to solve, because of wrong a wrong assumption I had. However, with the help of my mentor and an insight that came to me eventually, the problem was successfully solved.
Hello! I’m Ellen, a second-year undergraduate computer science student. Over the summer, I’ll be adding text-to-speech to several ScummVM engines to enhance their accessibility and assist language learners. I’ve already worked on adding TTS to two engines (Drascula and TeenAgent), and I hope to continue the process for other engines. I’m excited to work on this project!
My name is Alikhan and I am one of the GSoC students of this year. The task in my proposal was finishing the implementation of QdEngine. However, I’ve been working on the engine since March, and the task to implement interface for minigames appeared to be easier than was thought initially. Thus, there is only one task left for me to finish, so I plan to work on other engines after that.
In these blog posts I’ll be sharing my progress of each week, post about problems I faced, how I dealt with them and things a learnt along the way.
Hi, I’m Shivang Nagta, a pre-final year Computer Science undergraduate. I’ll be sharing my weekly blogs here, with updates on my GSoC project — “System for Checking Game Files Integrity.”
My mentors for this project are Sev and Rvanlaar, and I’m really grateful to have them guiding me. This project has been part of the last two GSoC years, so a lot of work has already been done. Here’s the current status:
Work done by the previous developers : 1. Server Side –
The server has been written in Flask. There’s a dashboard for proper visualization. The database schema and logic for feeding/updating the database have been implemented.
2. Client Side / ScummVM App :
There’s a Check Integrity button in the ScummVM application, which hits the server endpoint for validation with the checksum data of the game files.
Work done by me previously : 1. Client Side / ScummVM App :
Fixed the freezing issue in the Check Integrity dialog box. It was caused by the MD5 calculation of large files, which blocked synchronous screen updates. I solved it by implementing a callback system.
Engines like GLK and Scumm don’t use the Advanced Detector, so I worked on implementing a custom system to dump their detection entries. Some verification is still needed, as the current logic of these engines introduces complications in the implementation of the custom dumping systems.
2. Server Side :
I worked on two particular tasks: Punycode names and the different Mac files portability. Both tasks require final verification and testing. I’ve already mentioned them in the last section of the blog.
Work plan for Official Coding Phase:
1. Testing all the workflows on the server side :
Initial seeding by scummvm.dat (checksum data from the detection entries)
Uploading set.dat (checksum data from some old collections)
Uploading scan.dat (checksum uploaded by developers by scanning the local files using a command line utlility provided on the server)
user.dat from api (checksum coming from the client by the Check Integrity feature added on the ScummVM application)
Reupload scummvm.dat / set.dat
2. Moderation features :
Review the user submitted fileset
Have a list of unmatched fileset
Manual merge with search feature for a particular fileset ID followed by a merge screen
Remove filesets / undo changes, on a new upload (roll back feature)
Easy searching and filters of filesets by different field
3. Some fixes :
Different types of Mac files (like Appledouble, Macbinary, and Rsrc) have forks represented differently for the same game data. The checksums of Resource forks and Dataforks need to be extracted separately to create correct entries.
Often, filenames from one OS are not supported on another. To tackle this, Sev built a method on top of the classic Punycode encoding method (used for URL encoding), but it needs proper integration and testing in this project.
Tomorrow marks the beginning of the offical coding phase. Thank you for reading.