Following the participation of ScummVM to the Google Summer of code this summer we were invited to send two mentors to the Mentors Summit in Munich in October. I was selected to go with Arnaud (Strangerke).
While there I decided to make a short presentation in the Lightning Talk sessions. In those sessions we only have 3 minutes to present something, which is a fun exercise in conciseness.
Arnaud recorded this presentation and you can see it here:
And the slides are visible below:
Criezy's Blog
Sunday, 10 November 2019
Tuesday, 21 November 2017
So, when is the supernova going to explode?
With all the progress made on the supernova engine in ScummVM since the last post, I decided it was time to post an update. And in case you can't wait to know the answer to the question asked above, watch the video at the end of this post. But it would be a shame not to read first about the amazing work done lately ;)
Now what would help the most to get the translation completed is for native English speakers to play the first half of the game and suggest improvements on our translations web site.
We also already have a lot of suggestions for the second game, but it is a bit early for me to review those as I would like to focus on completing the engine for the first game.
As a reminder, the game is available for free on the original developer web site and the source code for the scummvm engine is on GitHub. To play the game you will also need to get the supernova.dat file that contains the game text (both German and English). I have uploaded a version on my dropbox, but you can also compile the devtools/create_supernova tool yourself (available from the same repository as the game engine) and execute it to generate the data file.
The graphics is an interesting aspect, so I will now give a bit more details about that point. It will also give me a good opportunity to add some much needed illustrations to this post. In the supernova engine, images contain multiple layers. The first layer always contain an image for the full screen (minus the inventory area, except for a few images that really are fullscreen) and then additional layers can be displayed or not for example to make animations, or depending on the context (door open or closed, object taken on not by player). Each room has an associated image and an array of boolean that indicates which layers are visible. When we render the room we thus iterates on all the layers and render the visible ones.
During animations though, we only render a layer and all those above it or "unrender them". To unrender a layer, we simply render the room starting from the first layer, for the area previously occupied by the now hidden layer.
The first graphical glitch found was due to setting the layer visibility flag in the wrong place when showing a layer, which caused the flag to be set not only for this layer, but also for all the layers above it. And we ended up with layers drawn in cases where they should not be.
Another minor issue with similar circumstances is that there was a logic bug that caused a single layer to be drawn in some places without redrawing layers above it.
I also introduced graphic glitches myself when rewriting the code to reduce memory usage. Initially the code had been implemented by using a surface of the full screen size for each layer. But most layers are a lot smaller and I rewrote the code to only use a surface only as big as needed for each layer. I got the surface pitch wrong however in the code bitting the background layer to screen when "unrendering" a layer. That resulted in some interesting animations...
Then more recently I stumbled upon a more serious issue that required rewriting a big chunk of the way images were rendered. The issue was that the wrong image was being rendered, and with the way the engine had initially been implemented in ScummVM there was no way to tell the rendering code to use the correct image. This caused the random crash mentioned above (when trying to render image -1), and when the crash did not occur (which happened more often than you might think), an interesting cutscene mixing layers from two images.
Translations
Thanks to the many suggestions from JenniBee, and some help from Joefish who reviewed all the translations for which I had a doubt, the first pass of the translation for the first Mission Supernova game is now completed. And I even had time to do a second pass for the first half of the game while testing the engine, and so improve the translation with the help of the context.Now what would help the most to get the translation completed is for native English speakers to play the first half of the game and suggest improvements on our translations web site.
We also already have a lot of suggestions for the second game, but it is a bit early for me to review those as I would like to focus on completing the engine for the first game.
As a reminder, the game is available for free on the original developer web site and the source code for the scummvm engine is on GitHub. To play the game you will also need to get the supernova.dat file that contains the game text (both German and English). I have uploaded a version on my dropbox, but you can also compile the devtools/create_supernova tool yourself (available from the same repository as the game engine) and execute it to generate the data file.
Engine
With Strangerke we made a lot of progress on the engine, getting to a point where more than half of the game is fully playable without any major issues, and this includes the supernova explosion! To get to this point, we needed to:- Implement the dialog system. In the early game there is no dialog (you will understand why when you play the game), so the dialog system had not been implemented yet. But it was required to progress beyond the first scene where the protagonist meets a fellow creature capable of speech.
- Implement the event callback mechanism. There are a several events in the game which are on a timer, the supernova explosion being the first of those. I am not a fan of timed event, but it kind of make sense in the context of the game. And we are not going to change the game anyway ;)
- Fix the graphics pipeline. There was some minor graphic issues, so major ones, and some catastrophic ones (causing a random crash due to out of bound access when trying to access the image at index -1 in the image array).
- Fix a palette brightness glitch that caused some rooms to be entirely black. This made it quite hard to progress.
The graphics is an interesting aspect, so I will now give a bit more details about that point. It will also give me a good opportunity to add some much needed illustrations to this post. In the supernova engine, images contain multiple layers. The first layer always contain an image for the full screen (minus the inventory area, except for a few images that really are fullscreen) and then additional layers can be displayed or not for example to make animations, or depending on the context (door open or closed, object taken on not by player). Each room has an associated image and an array of boolean that indicates which layers are visible. When we render the room we thus iterates on all the layers and render the visible ones.
During animations though, we only render a layer and all those above it or "unrender them". To unrender a layer, we simply render the room starting from the first layer, for the area previously occupied by the now hidden layer.
The first graphical glitch found was due to setting the layer visibility flag in the wrong place when showing a layer, which caused the flag to be set not only for this layer, but also for all the layers above it. And we ended up with layers drawn in cases where they should not be.
What is this brown thing in the toilets? |
Cleaned version. |
Another minor issue with similar circumstances is that there was a logic bug that caused a single layer to be drawn in some places without redrawing layers above it.
I also introduced graphic glitches myself when rewriting the code to reduce memory usage. Initially the code had been implemented by using a surface of the full screen size for each layer. But most layers are a lot smaller and I rewrote the code to only use a surface only as big as needed for each layer. I got the surface pitch wrong however in the code bitting the background layer to screen when "unrendering" a layer. That resulted in some interesting animations...
I know he is ugly, but was that a reason to censor his face? |
Also, two more hours before what? |
Flight cutscene, before and after fix |
Rewriting the graphics code to fix this last bug gave me the opportunity to also easily change the way the graphics data are loaded. The initial engine implementation in ScummVM was loading all the graphics data when starting the game. Now they are loaded on demand, which should reduce considerably the memory usage (and speed up a bit engine startup on slow platforms).
Conclusion
And now, finally, enjoy the supernova explosion!
But I won't tell you when it occurs so that you have to watch the full video of me playing the game (but at least I recorded it with the English text and not the original German one, and I slightly edited it to make it a couple of minutes shorter).
Monday, 2 October 2017
Supernova - translation to English
As you may be aware I was a mentor for the ScummVM organisation for the Google Summer of Code this year. I was supervising the work by Joefish on the Mission Supernova game. The engine has not yet been merged into the main repository, but work is continuing in Joefish's GitHub fork.
At the end of the GSoC coding period the first chapter of the game was completable. However the game is only available in German. One of our aim is not only to make it possible to play this game again on all platforms supported by ScummVM, but also to play it in English. And this is what I have been focussing on in the past week.
I have actually been working on the translation for a while with the help of Joefish. We are using the ScummVM translations website to handle the translation. With previous games when working on a translation we would use more rudimentary tools, such as a shared Google Doc spreadsheet. Thus using our translations website to translate a game is an experiment, but so far I have found it a positive one. And I expect it will also allow participation from the community to complete or improve the translation (this has not been the case yet as we did not advertise this translation until now).
What I was focussing on this week is writing a tool to generate an engine data file, as we do with other engines. Until a few days ago all the text was hardcoded in the engine. But the idea is to have the text in this engine data file instead and to have the engine load all the strings when it starts. This has two main advantages:
The rest of the file is a collection of blocks, with each block having a 12 byte header and a variable size data section:
byte 1-4 marker, for example 'TEXT' for the game text
byte 5-8 language code, for example 'de' for German
byte 9-12 block data size in byte
byte 13-N block data
This file structure means that we can easily add other types of data in the future if needed, such as the font data (that is currently hardcoded in the engine) or the mouse cursors (also currently hardcoded in the engine). The marker tells us what type of data we will find in a block. Currently the markers used are 'TEXT' (game strings), 'IMG1' and 'IMG2' (for newspaper bitmaps).
With this file format we can also easily add translations to more languages. Currently we have data for German and English.
(where 'xx' is a language code)
For each file found a corresponding data block will be added in the engine data file.
The strings-xx.po file is typically generated and updated by our translations web site and contains the translation in a given language for each German string. The German strings are defined in a specific order, and when generating the TEXT block for another language the tool iterates on the German strings, for each string looks for a translation in the po file for this language, and if found write this translation and otherwise write the original German string. This means that untranslated text will appear in German when playing.
The ppm files are bitmap images in portable bitmap format for the two newspaper article images in the game. They can for example be generated with gimp. Other than the header, the ppm format happens to store the data in exactly the same format as the game does for the original bitmaps in German. So the tool simply reads the header to do some sanity checks on the format and image size and write the rest of the file to the engine data file.
So adding a new language simply means adding its code to the language array in the tool source code, and dropping some pbm and po files in the directory where we execute the tool.
I didn't paste any source code in this post, but you can see the source code for the tool here: https://github.com/Joefish/scummvm/tree/supernova/devtools/create_supernova
The progress on the translation to English is similar, as 279 strings are currently translated, although some of those are for strings that have not yet been moved to the engine data file, and can thus not yet been seen translated in the game.
At the end of the GSoC coding period the first chapter of the game was completable. However the game is only available in German. One of our aim is not only to make it possible to play this game again on all platforms supported by ScummVM, but also to play it in English. And this is what I have been focussing on in the past week.
I have actually been working on the translation for a while with the help of Joefish. We are using the ScummVM translations website to handle the translation. With previous games when working on a translation we would use more rudimentary tools, such as a shared Google Doc spreadsheet. Thus using our translations website to translate a game is an experiment, but so far I have found it a positive one. And I expect it will also allow participation from the community to complete or improve the translation (this has not been the case yet as we did not advertise this translation until now).
What I was focussing on this week is writing a tool to generate an engine data file, as we do with other engines. Until a few days ago all the text was hardcoded in the engine. But the idea is to have the text in this engine data file instead and to have the engine load all the strings when it starts. This has two main advantages:
- The strings being into a separate file instead of being in the engine, the ScummVM executable ends up being smaller and using less memory when the engine is not running (for example when playing another game).
- We can store the strings in several languages in this engine data file and the engine will load the strings for the language selected by the user when the game was added to ScummVM. This means we can provide translations for the game.
Format of the engine data file
The data file starts with a 3 byte identifier ('MSN') that can be used to identify the file, and a 1 byte version number so that we can change the format in the future without breaking everything.The rest of the file is a collection of blocks, with each block having a 12 byte header and a variable size data section:
byte 1-4 marker, for example 'TEXT' for the game text
byte 5-8 language code, for example 'de' for German
byte 9-12 block data size in byte
byte 13-N block data
This file structure means that we can easily add other types of data in the future if needed, such as the font data (that is currently hardcoded in the engine) or the mouse cursors (also currently hardcoded in the engine). The marker tells us what type of data we will find in a block. Currently the markers used are 'TEXT' (game strings), 'IMG1' and 'IMG2' (for newspaper bitmaps).
With this file format we can also easily add translations to more languages. Currently we have data for German and English.
Generating the engine data file
The German text is hardcoded in the tool that generates the engine data file. All other data however are provided with supporting files. The tool has a list of languages for which it will look for those supporting files. Currently for each language we can have 3 files:- strings-xx.po
- img1-xx.pbm
- img2-xx.pbm
(where 'xx' is a language code)
For each file found a corresponding data block will be added in the engine data file.
The strings-xx.po file is typically generated and updated by our translations web site and contains the translation in a given language for each German string. The German strings are defined in a specific order, and when generating the TEXT block for another language the tool iterates on the German strings, for each string looks for a translation in the po file for this language, and if found write this translation and otherwise write the original German string. This means that untranslated text will appear in German when playing.
The ppm files are bitmap images in portable bitmap format for the two newspaper article images in the game. They can for example be generated with gimp. Other than the header, the ppm format happens to store the data in exactly the same format as the game does for the original bitmaps in German. So the tool simply reads the header to do some sanity checks on the format and image size and write the rest of the file to the engine data file.
So adding a new language simply means adding its code to the language array in the tool source code, and dropping some pbm and po files in the directory where we execute the tool.
I didn't paste any source code in this post, but you can see the source code for the tool here: https://github.com/Joefish/scummvm/tree/supernova/devtools/create_supernova
Status
The game contains 655 strings. Currently 281 of those have been moved to the engine data file, and this includes the name and description for all the objects. A lot of strings are still hardcoded in the game engine though, such as all the dialogs, and they will be moved to the engine data file as well eventually.The progress on the translation to English is similar, as 279 strings are currently translated, although some of those are for strings that have not yet been moved to the engine data file, and can thus not yet been seen translated in the game.
Monday, 29 May 2017
Mission Supernova - A look at the music
Earlier this month we announced two projects for this year Google Summer of Code to add support for the Sludge engine and for the Mission Supernova games in ScummVM. I am a co-mentor for the Mission Supernova project (the other mentor being Strangerke). We were lucky enough to be provided with the original source code for Mission Supernova (for which we have to thank the rights owner). With the coding period for GSoC starting officially tomorrow we spent the last month looking at this original source code. Interestingly, in addition to the source code for the game we were also given the source code for some tools. One of those converts a MOD music file to a game data file. And I though it would be interesting as a side project to reimplement it so that it works on modern computers, and to then extend it to perform the reverse conversion from game file to the original MOD file (which we don't have).
I thus spent a few days working on this in the past two weeks.
We have been asked not to share the original source code (and anyway you would have to be a bit of a masochist if you want to see C code from over 20 years ago). But I will show a small extract to give you an idea of the work involved. The original source code is in C and for DOS.
The source code for the mod conversion tool is very compact and starts with these two functions:
For anyone familiar with supporting both big endian and little endian platforms, what they do is obvious. They swap bytes to convert between big endian and little endian representations.
If you are wondering what big and little endians are, go read my first post in this blog on adding support for the mac version of Broken Sword (you can also read the third post in that series about fixing speech for some mac versions of Broken Sword).
Conversion between big endian and little endian should come as no surprise. The MOD format was originally developed for Amiga, which are (or at least were at the time) big endians computers. Looking at the specifications of the MOD format shows that it is indeed using the big endian convention. On the other hand the DOS operating system was working on little endian computers. Using a little endian format for the music in Mission Supernova thus made sense to avoid having to swap bytes during runtime. Every little helped at the time to get good performances...
Another thing visible in the code above and that should come as no surprise (at least for developers dealing with old platforms) is that an unsigned int is coded on two bytes (and not on 4 bytes as you would expect nowadays) and a long int uses 4 bytes.
The last point we can note is that functions and variables have German names. Fortunately for me I did study German at school and could understand most of the code straightaway without having to ask Google translate (or my German sister in law) for help.
The first step of my work was to rewrite the code of that tools so that it works on modern computers, whether they are using big endian or little endian conventions, and can be understandable by others.
I thus spent a few days working on this in the past two weeks.
We have been asked not to share the original source code (and anyway you would have to be a bit of a masochist if you want to see C code from over 20 years ago). But I will show a small extract to give you an idea of the work involved. The original source code is in C and for DOS.
The source code for the mod conversion tool is very compact and starts with these two functions:
For anyone familiar with supporting both big endian and little endian platforms, what they do is obvious. They swap bytes to convert between big endian and little endian representations.
If you are wondering what big and little endians are, go read my first post in this blog on adding support for the mac version of Broken Sword (you can also read the third post in that series about fixing speech for some mac versions of Broken Sword).
Conversion between big endian and little endian should come as no surprise. The MOD format was originally developed for Amiga, which are (or at least were at the time) big endians computers. Looking at the specifications of the MOD format shows that it is indeed using the big endian convention. On the other hand the DOS operating system was working on little endian computers. Using a little endian format for the music in Mission Supernova thus made sense to avoid having to swap bytes during runtime. Every little helped at the time to get good performances...
Another thing visible in the code above and that should come as no surprise (at least for developers dealing with old platforms) is that an unsigned int is coded on two bytes (and not on 4 bytes as you would expect nowadays) and a long int uses 4 bytes.
The last point we can note is that functions and variables have German names. Fortunately for me I did study German at school and could understand most of the code straightaway without having to ask Google translate (or my German sister in law) for help.
The first step of my work was to rewrite the code of that tools so that it works on modern computers, whether they are using big endian or little endian conventions, and can be understandable by others.
- I replaced the byte swap function from the original source code with code we already have in ScummVM that handles byte swapping depending on the platform on which the code is run (so that for example reading a MOD file would only swap bytes when the code is run on a little endian computer).
- I replaced data types such as unsigned and long with types provided by ScummVM such as uint16 and int32.
- I rewrote the code to use ScummVM Common::File API instead of the low level DOS file access code.
- I translated variable and function names to English.
- I objectified the code a bit adding a ModReader class.
At this point, without the original MOD file, I had no way to know if the code I wrote was correct. Writing this code however helped me understand the differences between the MOD format and the format used by the Mission Supernova game.
The two formats are very similar, but besides the different endianness, there are a few other differences. Actually the format for the two parts of Mission Supernova is slightly different.
Here is a description of the MOD file header:
And one of the Mission Supernova part 1 data file header:
For the Mission Supernova part 2, there are only 15 instruments stored and not 22.
Note how some information is missing in the Mission Supernova data file. That means that we have to guess what that information should be when converting that data file back to a MOD file. Fortunately none of that missing information is really important. For example for the song name I just decided to use the name of the MOD file that was hardcoded in the original source code.
Some other information is just formatted in a different way, such as the Mission Supernova instruments data having a loop start and loop end instead of a loop start and loop length.
Also the Mission Supernova data file stores explicitly the number of patterns and the offsets of the samples data. Those have to be computed from other informations in the MOD format.
Note how some information is missing in the Mission Supernova data file. That means that we have to guess what that information should be when converting that data file back to a MOD file. Fortunately none of that missing information is really important. For example for the song name I just decided to use the name of the MOD file that was hardcoded in the original source code.
Some other information is just formatted in a different way, such as the Mission Supernova instruments data having a loop start and loop end instead of a loop start and loop length.
Also the Mission Supernova data file stores explicitly the number of patterns and the offsets of the samples data. Those have to be computed from other informations in the MOD format.
The other difference not seen above between the two formats is in the pattern data. Both are using 32 bit values, but they are not coded in exactly the same way. For details on the differences just look at the source code and comments in the rewritten tool.
This knowledge of the MSN music data file might be useful when we have to work on supporting the music in the game engine reimplementation. For now I used it to write some code to do the conversion the other way around: from the game data file to a MOD file.
This allowed me to check that the code is correct:
- By checking that the converted MOD file I am getting is played correctly in a player supporting that format.
- By doing a round trip conversion: converting from MSN data file to MOD and then back to MSN data and checking that I get back the original file.
My first round trip test actually resulted in the original and converted MSN data file having a one byte difference (every bytes were identical except one). The offset of that bytes indicated it was the second byte of the order list length value, coded on two bytes in the Mission Supernova format. And then I realised that I was using a char variable (that uses one byte) since in the MOD format the order list length is coded on one byte. Writing that variable on two bytes meant the second byte was garbage.
The final source code is available at https://github.com/criezy/scummvm-tools/tree/supernova/engines/supernova. At some point I might merge it in the main ScummVM repository.
Implementing this reverse conversion also allowed me to listen to the music without waiting for the games to be supported in ScummVM. And to let you enjoyed that music as well, here are recordings for the music of the first and second parts of Mission Supernova converted to MOD and played back in an OpenSource ProTracker clone.
Implementing this reverse conversion also allowed me to listen to the music without waiting for the games to be supported in ScummVM. And to let you enjoyed that music as well, here are recordings for the music of the first and second parts of Mission Supernova converted to MOD and played back in an OpenSource ProTracker clone.
Mission Supernova part 1 music
Mission Supernova part 2 music
Friday, 25 March 2016
NSDockTilePlugIn for ScummVM on OS X
As you may already be aware, this year the ScummVM project is participating to the Google Summer of Code. One of the rules for students who want to participate with us is that they need to submit a simple patch against the ScummVM source code before they are accepted. Usually we direct prospective students to our bug tracker for ideas on what they could implement. But now most of the bugs that are still open are not trivial to fix. So I was looking at the source code hunting for simple things to do when I found a TODO comment I left two years ago when implementing the TaskbarManager API on OS X.
(if you don't see the source code below visit the blog as it may not be visible in RSS feeds)
Here we can note that I am using CFPreferences to read the list of recent games and not NSUserDefaults. Why is that? Do I need to remind you that this code is in a plug-in and not in ScummVM? That means we need to access the preferences of another application. Admittedly we could have used NSUsersDefault addSuiteNamed: to achieve this, but remember, we are implementing a plug-in and not an application. The plug-in is loaded by the SystemUIServer and using NSUserDefaults addSuiteNamed: would have changed the global preferences domain list for the SystemUIServer and not only for the plug-in.
The second point we can note is that the code above is using something called StartGameMenuItem. As you have probably guessed this is a custom class that derives from NSMenuItem. Indeed for each menu item I needed to store somewhere the game ID so that when this menu item is activated it can start the corresponding game. So I decided to inherit from the NSMenuItem class and store the game ID in the derived class. And while I was at it I also added the method to start a new game in that derived class. So here is what this class looks like:
What is the TaskbarManager API?
The TaskbarManager API allows interacting with the ScummVM application icon in the taskbar (or in the case of OS X in the dock). We have implementations of this API for several systems, but the only one that is complete is the implementation for Windows. In details the API allows to:- Display an overlay icon when playing a game. If you have in your extra path png files named after the game IDs, when starting a game the corresponding png image is overlaid on the ScummVM icon in the dock. You can for example get icons from http://www.megamonkey.org/icons/ and below is example of this feature in action.
- Display progress. This is for example used in the mass add feature to indicate the number of directories scanned in respect with the total number of directories to scan.
- Display a count. This is also used in the mass add feature to indicate how many games are being added.
- Notify of an error.
- Provide a list of recently played games.
The last two were not implemented on OS X and the TODO comment was related to the last point. The idea was that we could provide a list of recently played games in the ScummVM dock menu and thus provide a shortcut to start a game quickly. I decided to take another look at this feature and wrote some bits of code to check that the idea I had hinted at in the TODO would indeed work. I then waited a few days in case a student wanted to implement this as part of his application to the GSoC. But today I finished implementing this feature, cleaned the code and pushed this to the ScummVM repository.
One aspect to consider here is that we want to customise the menu on the ScummVM icon in the dock when ScummVM is not running. That way we can propose a list of recent items in the menu and start ScummVM directly with a game. On OS X we can provide this feature with a plug-in that implements the NSDockTilePlugIn protocol. If an application bundle contains such a plug-in, the OS loads that plug-in when the application is added to the Dock. So there are actually two separate things to implement:
- Obviously we need to implement the plug-in.
- But we also need to implement code in ScummVM to update the list of recent games when starting a game.
Saving the list of recent games
The TaskbarManager is part of the ScummVM application and when starting a new game the addRecent method is called. So what I did here was simply to save the list or recent games in a place where the aforementioned plug-in can find it. I decided to use the NSUserDefaults class to do this, which means the list is saved in the user preferences (to be precise in the ~/Library/Preferences/org.scummvm.scummvm.plist file).
(if you don't see the source code below visit the blog as it may not be visible in RSS feeds)
That code is a bit too simple though. There are two main issues with it: the list can grow indefinitely and the same game can appear multiple times in the list. So let's improve the code that updates the array of games.
And that's it. We have this part fully implemented. After playing a few games the ~/Library/Preferences/org.scummvm.scummvm.plist file should look like this:
Implementing the NSDockTilePlugIn
If you took a look at the NSDockTilePlugIn protocol documentation you will have seen that it requires implementing a setDockTile: method, and optionally we can implement a dockMenu method. We actually have nothing to do in the first one, so let's skip it and look directly at the second method.Here we can note that I am using CFPreferences to read the list of recent games and not NSUserDefaults. Why is that? Do I need to remind you that this code is in a plug-in and not in ScummVM? That means we need to access the preferences of another application. Admittedly we could have used NSUsersDefault addSuiteNamed: to achieve this, but remember, we are implementing a plug-in and not an application. The plug-in is loaded by the SystemUIServer and using NSUserDefaults addSuiteNamed: would have changed the global preferences domain list for the SystemUIServer and not only for the plug-in.
The second point we can note is that the code above is using something called StartGameMenuItem. As you have probably guessed this is a custom class that derives from NSMenuItem. Indeed for each menu item I needed to store somewhere the game ID so that when this menu item is activated it can start the corresponding game. So I decided to inherit from the NSMenuItem class and store the game ID in the derived class. And while I was at it I also added the method to start a new game in that derived class. So here is what this class looks like:
To Conclude
Now if you add ScummVM to the dock, and after playing at least one game, you should see the list of games you played recently in the the dock menu like in the picture below. This provides a quick way to start one of those games.
And here is what it looks like in action:
And here is what it looks like in action:
This is in my opinion the most useful of the features provided by the TaskbarManager API, so I am happy to see it finally implemented (I would have done it sooner if I had not forgotten about it :P).
Edit: Our buildbot uses an older SDK that does not support the NSDockTilePlugIn protocol. So nightly builds from our web site will not contain this new feature. You will need to compile your own version or wait for ScummVM 1.9.0.
Edit: Our buildbot uses an older SDK that does not support the NSDockTilePlugIn protocol. So nightly builds from our web site will not contain this new feature. You will need to compile your own version or wait for ScummVM 1.9.0.
Sunday, 30 November 2014
Broken Speech
Yes, another Broken Sword post. In previous posts I wrote about my initial work to add support for the mac version of the Broken Sword game in ScummVM and some more work I did to fix graphical glitches with this version. At that point the game was working fine for me. But soon, we got a report on the forum that the speech was not working. Obviously it was working for me, I would have noticed if it wasn't. So what the heck?!
Bug reports are good. They stop me getting bored. And they show that other users have the mac version of Broken Sword and benefit from my work, which is also gratifying. So let's look at that issue. And to do so, first let's rewind to my first post. I wrote that I assumed the files with the same name (including the extension) as the files of the Windows version where in the same format, and in particular used the same endianness. And the files with a different extension were big endian in the mac version and little endian in the Windows version. That proved mostly correct (a few resources had been left as little endian data in files otherwise converted to big endian).
Except that wasn't correct. So why did it work? Because I had been lucky. When I initially worked on supporting that game it looked like my guess was correct, and I therefore made quick progress as I was not distracted by some strange behaviour. But...
Let's start the story from the beginning:
In the Windows version the speech is stored in a file named speech.clu. There are actually two such files, one on each CD, and they store the speech as 16 bits compressed mono wave data. And as you can expect the data is little endian.
In the Mac version, the files have the same name (speech.clu). So in my initial implementation I assumed the speech data was little endian in the mac version as well. And it worked... with the version I have (the French version). Obviously it didn't work with the version of the user reporting the bug (the English version) since the user reported hearing static noise instead of speech.
The two files (Windows and Mac versions, both English) have the same size:
But opening them in an hex editor shows differences:
Do the differences remind you of something?
If not go back and read again the first two posts in this Broken Sword mac support series.
Before looking at the differences, I will give a short explanation of the speech file format.
The speech files are a collection of sound resources. Each resource contains the wave data for spoken sentence and is organised as follow:
4 bytes: 'data' (i.e. hexadecimal values 64 61 74 61)
4 bytes: number of samples
n bytes: wave data (16 bits mono)
So quite simple, but maybe no as simple as you might think. If you are thinking that n is the number of samples multiplied by 2 (since each sample takes 2 bytes) you are wrong. Because the data is compressed. This is not really important for now so I will keep the description of the compression for later.
What is important here is that we can see that the first 4 bytes after 'data' are identical (in the image above hexadecimal values 8E E6 01 00 - which, since we know it is little endian, means 0x0001E68E = 124558 samples). But the values that follow are obviously a series of 2 bytes values for which the bytes have been swapped.
At this points, here is a small reminder in case you are not following me and did not go back to my previous posts: big endian and little endian are conventions used to interpret the bytes making up a data word (more at http://en.wikipedia.org/wiki/Endianness). For example 42 in hexa is 2A, or when using two bytes 002A. When using the big endian convention, this would be stored as 00 2A. But when using the little endian convention this would be stored as 2A 00.
So this should be obvious to you now that both the Windows and the Mac version store the number of samples of the sound resource as little endian values, but in the mac version the data samples themselves are stored using big endian convention. Why mix endianness in the same file? Why do this for the mac English version but not the mac French version? Don't ask me, I have no idea.
Since some mac version use little endian and others use big endian, we need to know which one it is. Does it depend on the language, i.e. all French versions use little endian and all English versions use big endian? Maybe. But I don't trust statistics on a set of two samples. And what of the German versions?
Therefore we decided to use a heuristic to find out if the mac version the player has uses big endian data or little endian data. The heuristic works by computing the average difference between two consecutive samples (using absolute values). Using the assumption that a sound wave is smoother than picking values at random, the lower average difference is considered to be the correct endianness.
If we take the 13 samples from the example image above, assuming big endian for the mac version gives us the following curve:
The average difference value from one sample to the next is 425.17.
If we assume little endian data the curve is:
And the average difference value is 9344.75.
So in this case the heuristic tells us the data is big endian, which it is. Of course in the actual source code we use more than 13 samples to get a statistically valid heuristic value.
The original patch that adds the heuristic code can be found in the patch tracker: https://sourceforge.net/p/scummvm/patches/956/
But the story does not ends here. A new bug report very similar to the original one (i.e. speech sounds like static noise) was reported a few months ago. It was quite obvious that the heuristic did not work for that user and the wrong endianness was used. Why is that? It turns out there were several issues with the original heuristic code.
And that is where explaining how the speech data compression works will help to understand what was wrong. The data for one sound resource is broken in blocks, each one starting with a number of samples followed by the sample values. When you have consecutive samples with the same value it uses a negative size and the value is stored only once.
So for example the following sequence:
0 0 0 0 0 1 2 3 4 5
would be stored as (where the brackets indicate the blocks):
[ -5 0] [5 1 2 3 4 5]
All those numbers (number of samples and sample value) are stored on 2 bytes.
Here is the original source code (if you don't see the source code visit the blog as it may not be visible in RSS feeds).
I will not show the uncompressSpeech() code (yet). The only thing you need to know is that it uses the value of _bigEndianSpeech as either big endian or little endian data. So what the code above does is get the data assuming little endian data and then compute the heuristic value for the samples it gets and for the same samples to which a byte swap is applied, which should be the value we would get assuming big endian data. Right?
Wrong! This heuristic forgets something: the data is compressed, and when uncompressing it always assume little endian when reading the number of samples for each block. But if the data are big endian this number would be different and the blocks would have different sizes, and because the block boundaries would be wrong it would cause number of samples to be interpreted as sound samples and some sound samples to be interpreted as number of samples.
For example let's look at the resource of 10 sample with the compressed data -5 0 5 1 2 3 4 5.
Assuming the data is stored in big endian, in hexadecimal values with two bytes per value this give us: 80 05 00 00 00 05 00 01 00 02 00 03 00 04 00 05
If we read this with the heuristic above, because the number of sample is always read assuming little endian data we get 80 05 = 1408 samples instead of -5 for the first block. So the code will get the following samples: 0 5 1 2 3 4 5 followed by 4 garbage values read beyond the end of the resource instead of getting 0 0 0 0 0 1 2 3 4 5. So if the data are stored using big endian, the heuristic values gets biased. Almost always it still gets a lower score as reading it as little endian though.
The solution here is to call uncompressSpeech() twice, once assuming little endian and once assuming big endian.
But there is still an issue with this heuristic.
When reading with the wrong endianness, since we may read the wrong length, it may for example be a big negative number. Because we are using a relatively small finite number of samples, statistically we could end up with a small heuristic value because it has a lot of consecutive samples with the same value. For this reason I made an additional change to skip consecutive samples with the same value when computing the average difference.
Bug reports are good. They stop me getting bored. And they show that other users have the mac version of Broken Sword and benefit from my work, which is also gratifying. So let's look at that issue. And to do so, first let's rewind to my first post. I wrote that I assumed the files with the same name (including the extension) as the files of the Windows version where in the same format, and in particular used the same endianness. And the files with a different extension were big endian in the mac version and little endian in the Windows version. That proved mostly correct (a few resources had been left as little endian data in files otherwise converted to big endian).
Except that wasn't correct. So why did it work? Because I had been lucky. When I initially worked on supporting that game it looked like my guess was correct, and I therefore made quick progress as I was not distracted by some strange behaviour. But...
Let's start the story from the beginning:
In the Windows version the speech is stored in a file named speech.clu. There are actually two such files, one on each CD, and they store the speech as 16 bits compressed mono wave data. And as you can expect the data is little endian.
In the Mac version, the files have the same name (speech.clu). So in my initial implementation I assumed the speech data was little endian in the mac version as well. And it worked... with the version I have (the French version). Obviously it didn't work with the version of the user reporting the bug (the English version) since the user reported hearing static noise instead of speech.
The two files (Windows and Mac versions, both English) have the same size:
But opening them in an hex editor shows differences:
If not go back and read again the first two posts in this Broken Sword mac support series.
Before looking at the differences, I will give a short explanation of the speech file format.
The speech files are a collection of sound resources. Each resource contains the wave data for spoken sentence and is organised as follow:
4 bytes: 'data' (i.e. hexadecimal values 64 61 74 61)
4 bytes: number of samples
n bytes: wave data (16 bits mono)
So quite simple, but maybe no as simple as you might think. If you are thinking that n is the number of samples multiplied by 2 (since each sample takes 2 bytes) you are wrong. Because the data is compressed. This is not really important for now so I will keep the description of the compression for later.
What is important here is that we can see that the first 4 bytes after 'data' are identical (in the image above hexadecimal values 8E E6 01 00 - which, since we know it is little endian, means 0x0001E68E = 124558 samples). But the values that follow are obviously a series of 2 bytes values for which the bytes have been swapped.
At this points, here is a small reminder in case you are not following me and did not go back to my previous posts: big endian and little endian are conventions used to interpret the bytes making up a data word (more at http://en.wikipedia.org/wiki/Endianness). For example 42 in hexa is 2A, or when using two bytes 002A. When using the big endian convention, this would be stored as 00 2A. But when using the little endian convention this would be stored as 2A 00.
So this should be obvious to you now that both the Windows and the Mac version store the number of samples of the sound resource as little endian values, but in the mac version the data samples themselves are stored using big endian convention. Why mix endianness in the same file? Why do this for the mac English version but not the mac French version? Don't ask me, I have no idea.
Since some mac version use little endian and others use big endian, we need to know which one it is. Does it depend on the language, i.e. all French versions use little endian and all English versions use big endian? Maybe. But I don't trust statistics on a set of two samples. And what of the German versions?
Therefore we decided to use a heuristic to find out if the mac version the player has uses big endian data or little endian data. The heuristic works by computing the average difference between two consecutive samples (using absolute values). Using the assumption that a sound wave is smoother than picking values at random, the lower average difference is considered to be the correct endianness.
If we take the 13 samples from the example image above, assuming big endian for the mac version gives us the following curve:
The average difference value from one sample to the next is 425.17.
If we assume little endian data the curve is:
And the average difference value is 9344.75.
So in this case the heuristic tells us the data is big endian, which it is. Of course in the actual source code we use more than 13 samples to get a statistically valid heuristic value.
The original patch that adds the heuristic code can be found in the patch tracker: https://sourceforge.net/p/scummvm/patches/956/
But the story does not ends here. A new bug report very similar to the original one (i.e. speech sounds like static noise) was reported a few months ago. It was quite obvious that the heuristic did not work for that user and the wrong endianness was used. Why is that? It turns out there were several issues with the original heuristic code.
And that is where explaining how the speech data compression works will help to understand what was wrong. The data for one sound resource is broken in blocks, each one starting with a number of samples followed by the sample values. When you have consecutive samples with the same value it uses a negative size and the value is stored only once.
So for example the following sequence:
0 0 0 0 0 1 2 3 4 5
would be stored as (where the brackets indicate the blocks):
[ -5 0] [5 1 2 3 4 5]
All those numbers (number of samples and sample value) are stored on 2 bytes.
Here is the original source code (if you don't see the source code visit the blog as it may not be visible in RSS feeds).
I will not show the uncompressSpeech() code (yet). The only thing you need to know is that it uses the value of _bigEndianSpeech as either big endian or little endian data. So what the code above does is get the data assuming little endian data and then compute the heuristic value for the samples it gets and for the same samples to which a byte swap is applied, which should be the value we would get assuming big endian data. Right?
Wrong! This heuristic forgets something: the data is compressed, and when uncompressing it always assume little endian when reading the number of samples for each block. But if the data are big endian this number would be different and the blocks would have different sizes, and because the block boundaries would be wrong it would cause number of samples to be interpreted as sound samples and some sound samples to be interpreted as number of samples.
For example let's look at the resource of 10 sample with the compressed data -5 0 5 1 2 3 4 5.
Assuming the data is stored in big endian, in hexadecimal values with two bytes per value this give us: 80 05 00 00 00 05 00 01 00 02 00 03 00 04 00 05
If we read this with the heuristic above, because the number of sample is always read assuming little endian data we get 80 05 = 1408 samples instead of -5 for the first block. So the code will get the following samples: 0 5 1 2 3 4 5 followed by 4 garbage values read beyond the end of the resource instead of getting 0 0 0 0 0 1 2 3 4 5. So if the data are stored using big endian, the heuristic values gets biased. Almost always it still gets a lower score as reading it as little endian though.
The solution here is to call uncompressSpeech() twice, once assuming little endian and once assuming big endian.
But there is still an issue with this heuristic.
When reading with the wrong endianness, since we may read the wrong length, it may for example be a big negative number. Because we are using a relatively small finite number of samples, statistically we could end up with a small heuristic value because it has a lot of consecutive samples with the same value. For this reason I made an additional change to skip consecutive samples with the same value when computing the average difference.
After that commit the value for the heuristic with the wrong endianness is consistently about 21000, i.e. 1/3rd of 16 bits integer range (65 535 / 3 = 21845). As noted by wjp: the average absolute difference between two random numbers drawn independently from a uniform distribution between 0 and N is indeed N/3. So this is quite reassuring.
So everything was correct after this change? No, that would be too easy. We want complex puzzles spanning several rooms, not some kind of hidden objects game. And the user reporting the bugs confirmed the bug was still present after that change. So let's look at a different room, or rather a different function.
Here is the code from uncompressSpeech():
Note something relevant? No? Look closer. You see it now? Yes, this function always give us the sound data in little endian format, whatever the endianness in which it is stored, and more importantly whatever the endianness of the computer on which the code is run.
And if you look at the heuristic code above, it assumes it gets data in the native endianness (i.e. the endianness of the computer on which the code is run). So when running on a computer using big endian convention the heuristic was wrong. Let's look again at our previous example and how it was interpret on a big endian computer:
Read with the correct endianess: 0 1 2 3 4 5 (duplicate values have been removed)
Was interpreted as: 0 256 512 768 1024 1280
Read with the incorrect endianess: 0 1280 256 512 768 1024 1280
Was interpreted as 0 5 1 2 3 4
So in the heuristic code, on a big endian computer we need to swap the bytes of the two set of data to get the correct value. This brings us to the final code, in which the heuristic computation was also moved to a separate function to avoid code duplication (since it is done twice):
The user reporting the bug confirmed he was using ScummVM on a big endian computer (a G4 mac) and that the speech was correct after that final change.
Sunday, 28 September 2014
How it continued
In my very first post on this blog I wrote how I came to be involved with the ScummVM project by adding support to the mac version of Broken Sword 1. I wrote I had been lucky, and you will have to wait a bit longer to know why (yes I know, I am milking this one, but I promise I will explain it soon). I expertly avoided however to reveal that I had also been lazy. When I submitted the initial patch I knew the support was not perfect. I already mentioned it lacked support for AIFF music (and I will take this opportunity to correct myself: apparently the support was added by eriktorbjorn, at least according to the history on github, and not by sev as I mistakenly wrote in my first post). But more importantly there were graphical glitches. Yes! GRAPHICAL GLITCHES! Oh, the horror! And I can't even claim I had not noticed them. That would mean admitting I was blind (or at least color blind).
The first one is visible every time you visit Nico in her apartment, which is quite often (just a shame you can't use that big bed). Notice anything wrong (no, not the bed)?
Just in case the image above appears normal to you, here is what it should have looked like:
The second glitch is even bigger, although maybe not as obvious. I had not played the game for a few years myself when I added support for it in ScummVM, and while something was bugging me during my tests I was not sure what it was initially.
And here is what is should have looked like:
So what is wrong? This scene in Syria has a background parallax layer, on top of which the foreground is drawn, with transparency where we should see the background. And you have probably noticed by now that the background was not visible in the mac version.
The game sometimes uses parallax layers to give a sense of depth to the scene. When the characters move on screen, the foreground and background will move at different speed. See wikipedia if you have never heard of a parallax before.
This is the only scene in the game that has a background parallax layer. And as such it has a special logic for the draw code. Other scenes may have a foreground parallax layer however, as is visible in the video below.
The two glitches are caused by two different bugs. But they are somewhat related. The game uses 256 colors with a different palette for each scene. That means each scene defines a list of 256 colors, and then the image data is defined using the indexes (stored on 1 byte) in that list instead of using directly the colors.
The palette for each scene is actually defined in two separate resources: one that defines the palette for the scene itself and contains 184 colors (indexes 0 to 183), and one for the sprites that contains 72 colors (indexes 184 to 255). The first color (at index 0) is actually reserved for the top bar (inventory) and bottom bar (dialog options) area when the bars are hidden. It is forced to black in all the scenes, whatever the color defined in the data file. This is also the color used for the door in Nico's room. And it is used for the transparent part of the foreground image in the bull's head hill scene. And this is the index used for the transparency in the sprite data as well.
You have probably guessed it by now: the mac version does not use color index 0 for the door in Nico's room and for the transparency in the bull's head hill scene. After a bit of debugging it turned out it is actually using color index 255 (i.e. the last color of the palette instead of the first one). In Nico's apartment that color happens to be red, and in the Bull's Head Hill scene it happens to be some sort of brownish dark green. Once I knew what the problem was, it was fixed with a simple patch.
Other than that the Mac version is identical to the Windows version. It still uses the first 184 colors of the palette for the background and the last 72 colors for the sprites. And it still uses color index 0 for the top and bottom bars area and the transparency in the sprite data. So I have no idea why they made that change for the two cases described above.
Here is the code to get the palette when loading a new room. As explained above it is called twice, once for the first 184 colors and a second time for the remaining 72 colors. We force color 0 to be black. Lines 6 to 9 corresponds to the fix for the mac version, in which we also force color 255 to be black.
And here is the beginning of the draw code. As I wrote above the Bull's Head Hill, which is screen 54, has a special handling. We first draw the background parallax and then draw the screen on top, skipping pixels with color 0 (which here means transparent). On line 21 we have the fix for the mac version, for which we also skip pixels with color 255.
And that is all for today. In the next post I will speak of the speech data, and I will explain why I was lucky in my initial implementation.
The first one is visible every time you visit Nico in her apartment, which is quite often (just a shame you can't use that big bed). Notice anything wrong (no, not the bed)?
George, don't leave! Have you seen what is waiting for you out there? A corridor painter in red! Stuff of nightmare! And the psychopath who painted that might still be lurking in a corner! |
Just in case the image above appears normal to you, here is what it should have looked like:
No light in the corridor? I guess they forgot to pay the electricity bill. |
The second glitch is even bigger, although maybe not as obvious. I had not played the game for a few years myself when I added support for it in ScummVM, and while something was bugging me during my tests I was not sure what it was initially.
Bull's Head Hill, Syria, on a murky day. The sky, the color of a swamp, was empty of any birds. And I was about to jump into the void. |
A Sunny day in Syria. Maybe I will live after all. Not that it will stop me jumping though. |
The game sometimes uses parallax layers to give a sense of depth to the scene. When the characters move on screen, the foreground and background will move at different speed. See wikipedia if you have never heard of a parallax before.
This is the only scene in the game that has a background parallax layer. And as such it has a special logic for the draw code. Other scenes may have a foreground parallax layer however, as is visible in the video below.
The two glitches are caused by two different bugs. But they are somewhat related. The game uses 256 colors with a different palette for each scene. That means each scene defines a list of 256 colors, and then the image data is defined using the indexes (stored on 1 byte) in that list instead of using directly the colors.
The palette for each scene is actually defined in two separate resources: one that defines the palette for the scene itself and contains 184 colors (indexes 0 to 183), and one for the sprites that contains 72 colors (indexes 184 to 255). The first color (at index 0) is actually reserved for the top bar (inventory) and bottom bar (dialog options) area when the bars are hidden. It is forced to black in all the scenes, whatever the color defined in the data file. This is also the color used for the door in Nico's room. And it is used for the transparent part of the foreground image in the bull's head hill scene. And this is the index used for the transparency in the sprite data as well.
You have probably guessed it by now: the mac version does not use color index 0 for the door in Nico's room and for the transparency in the bull's head hill scene. After a bit of debugging it turned out it is actually using color index 255 (i.e. the last color of the palette instead of the first one). In Nico's apartment that color happens to be red, and in the Bull's Head Hill scene it happens to be some sort of brownish dark green. Once I knew what the problem was, it was fixed with a simple patch.
Other than that the Mac version is identical to the Windows version. It still uses the first 184 colors of the palette for the background and the last 72 colors for the sprites. And it still uses color index 0 for the top and bottom bars area and the transparency in the sprite data. So I have no idea why they made that change for the two cases described above.
Here is the code to get the palette when loading a new room. As explained above it is called twice, once for the first 184 colors and a second time for the remaining 72 colors. We force color 0 to be black. Lines 6 to 9 corresponds to the fix for the mac version, in which we also force color 255 to be black.
And here is the beginning of the draw code. As I wrote above the Bull's Head Hill, which is screen 54, has a special handling. We first draw the background parallax and then draw the screen on top, skipping pixels with color 0 (which here means transparent). On line 21 we have the fix for the mac version, for which we also skip pixels with color 255.
And that is all for today. In the next post I will speak of the speech data, and I will explain why I was lucky in my initial implementation.
Subscribe to:
Posts (Atom)