Go Fixes and House Jam

Last week I fixed some longstanding issues with the Director engine’s implementation of the ‘go’ command!

‘go’ is one of the most commonly used Lingo commands, used to change which frame a movie is on or to load an entirely different movie. While it seems straightforward on the surface, it has some subtle quirks that can affect movie execution in major ways.

Most of the time, ‘go’ is used in a script that looks like this:

on exitFrame
  go to frame 5
end

It’s pretty clear what we need to do here. When we exit the current frame, we just need go to frame 5. Previously, we did that something like this:

  • When a ‘go’ command is executed, queue the requested frame or movie.
  • When script execution has finished, the Lingo interpreter returns control to the score.
  • If a frame has been queued, the score loads the requested frame. If a movie has been queued, the score does nothing and returns control to the window.
  • If a movie has been queued, the window loads the requested movie.

Now what if we make the script a bit more complicated?

on exitFrame
  go to frame 5
  doSomethingElse()
end

With our previous implementation, the entire script, including doSomethingElse() would execute before the frame/movie switched. This isn’t how the original does it, though. It should actually work like this:

  • When a ‘go’ command is executed, pause execution of the current script.
  • Immediately load the new movie/frame.
  • Process any startMovie/enterFrame events for the new movie/frame.
  • Unpause execution of the script containing the ‘go’ command.

With this execution order, doSomethingElse() shouldn’t execute until after the new frame/movie is loaded. I implemented the correct implementation order, and then I immediately ran into another longstanding problem.

A movie’s scripts can sometimes outlast the movie that originally contained them, as in the example I gave above. But scripts could also contain pointers to data owned by the movies, most notably the name table. Once the movie was destroyed, these would become dangling pointers, and dereferencing them would cause the engine to segfault. I solved this by copying any data which a script needs, including any names, over to the script itself, eliminating all references from the script to the movie.

An additional, new problem popped up. Previously, after a script was executed, the call stack would be entirely empty, and we could safely share one stack among several movies running in parallel in different windows. They would just take turns executing their scripts, leaving the stack nice and tidy for the next movie. Now, though, a movie’s script execution could be temporarily paused, leaving data on the stack. We can no longer share one call stack among multiple windows, so I gave each each window its own call stack.

Also, I’ve debugged one of Meet MediaBand’s interactive music videos, House Jam, and it’s now running quite well! There are some rendering issues that need to be sorted out, but that doesn’t make it any less fun. Here’s a small sample of it, but it’s much better to interact with the music video yourself. 😉

The Beginning of The End 2 (Progress Report)

Good morning/afternoon/evening. With saves implemented and most warnings dealt with we’re one step closer to the final stage. The game is should be mostly playable at the moment, but the only way to verify that is to actually test it.

Debug Commands

In order to facilitate our endeavors, it is useful to create lots of debug commands (I’ve explained how to implement them in the last post).

Here is the full list of commands added so far:

// Input: <None>. Kills the center actor
bool cmdKillProtag(int argc, const char **argv);

// Input: <Actor ID>. Kills an actor
bool cmdKill(int argc, const char **argv);

// Input: <Object ID>. Prints an object's name
bool cmdObjName(int argc, const char **argv);

// Input: <Name Index>. Prints an ObjectID corresponding to the name index.
bool cmdObjNameIndexToID(int argc, const char **argv);

// Input: <Object Name>. Prints a list of objects containing the string in their name
bool cmdSearchObj(int argc, const char **argv);

// Input: <Object ID>. Adds the object to the center actor's inventory.
bool cmdAddObj(int argc, const char **argv);

// Input: <1/0>. Sets godmode
bool cmdGodmode(int argc, const char **argv);

// Input: <1/0>. Sets whether the position coordinates show
bool cmdPosition(int argc, const char **argv);

// Input: <1/0>. Sets whether an item's stats show when holding it
bool cmdStats(int argc, const char **argv);

// Input: <1/0>. Sets whether you can teleport by clicking on the screen
bool cmdTeleportOnClick(int argc, const char **argv);

// Input: <u> <v> <z>. Teleports the center character to the given position
bool cmdTeleport(int argc, const char **argv);

// Input: <Actor ID>. Teleports the character to the npc
bool cmdTeleportToNPC(int argc, const char **argv);

// Input: <Actor ID> <u> <v> <z>. Teleports the npc to the given coordinate
bool cmdTeleportNPC(int argc, const char **argv);

// Input: <Actor ID>. Teleports the npc to the center actor
bool cmdTeleportNPCHere(int argc, const char **argv);

// Input: <None>. Saves the current location in a variable
bool cmdSaveLoc(int argc, const char **argv);

// Input: <None>. Teleports the center actor to the saved location
bool cmdLoadLoc(int argc, const char **argv);

// Input: <Place ID>. Teleports to the given place
bool cmdGotoPlace(int argc, const char **argv);

// Input: <None>. Lists all of the place names along with their IDs
bool cmdListPlaces(int argc, const char **argv);

// Input: <Map Scale Multiplier>. Dumps the map into a png.
bool cmdDumpMap(int argc, const char **argv);

Method

I’ve used the playlist by Sinatar as a basis of what a normal gameplay would look like (also helps me keep track of the normal course of the story). With that open, I proceed along the gameplay with ScummVM and if there’s something specific I wish to check, I open DOSBox and compare the two. Having backwards compatible saves here helps a lot.

One of the first things I wished to check was whether I could reach an ending. I did some research and took notice that I needed three Golden Apples to open the passage to the underworld.

Opening the passage to the Underworld.

Once there we can teleport the final boss, Sariloth to us with teleport_npc_here 32977.

The final fight?

Defeating Sariloth on DOSBox caused one of the endings to play, but here on ScummVM it only made the New Game dialog pop up.

Comparing the source code for playing the endings we can see that some code got lost in the reformatting.

tromode.cpp:
void setLostroMode(void) {
	allPlayerActorsDead = false;
	if (GameMode::newmodeFlag)
		GameMode::update();

	if (!abortFlag) {
...
original/tromode.cpp
void setLostroMode( void )
{
	waitForVideoFile( "INTRO.SMK");
	allPlayerActorsDead = FALSE;
	if (GameMode::newmodeFlag)
		GameMode::update();

	if (!abortFlag)
	{
...

abortFlag is set to false inside of waitForVideoFile, so we fix that by adding a abortFlag = false; at the top of setLostroMode (this code is candidate for some more code reformatting, though).

We got one of the bad endings… Spoiler warning?

Bugs

Other than that, by following the walkthrough I took notice of some lesser bugs as well. Here is a list of the bugs I’ve annotated so far:

BugSteps to reproduceExplanation
Speech is too quiet (compared to music/sound effects)
Music does not loop when it finishesIt loops periodically on the original
Music does not disappear completely when volume is set to zeroSet the music to zeroYou can hear it faintly even when the slider is fully on the left
Music does not restartAfter setting the music to zero, raise it once againIt should restart on the original
Music Volume not reflected to what is on the config when the game startsSet the volume to 0, restart gameThe volume will be at 0 in the options but music will be blasting at full volume
Arrow sprite not changing when reading scrollOpen the scroll in the Padavis InnThe arrow sprite should change to an up arrow when hovering on the upper part and vice-versa for the lower part on the original
Close parchment button not workingOpen the parchments on the notices board in Padavis InnThe close button does not work (although clicking outside the parchment closes it)
Speech not finishing before dialog to buy beer startsPavilion Bar – buy beer with RiddenbutterOn the original, the buy dialog should appear after the speech ends
Crash on reading Kaidar’s bookcmd: add_obj 274 and read book in chestThe book is readable on the original
Crash on saving/loading when spells are presentGo to the Underworld and save when the Wraiths are casting spells, then load the save
Crash on Credits (options menu)Click on the credits button
Fade-in not working when loading a world with WorldID > 0 from the launcherLoad a save from the Underworld from the launcherThe screen flickers and the fade-in does work properly

From now on the development will consist mostly of playtesting and solving bugs. After that I suspect refactoring, solving warnings on other platforms and creating documentation may become of more importance. Thankfully I only have two weeks of school left, so in the worst case scenario I still have plenty of time after that. Let’s hope this project ends in success!

Polishing Week

Polishing Week

This week, what i mostly done is fixing the minor details in director.

I’ve implement playing the sub-looping sounds which will bring us the perfect sound playing for warlock-win and warlock-mac.

And another very exciting thing is Jaderlund is willing to help us test the warlock in director. He had find some really good problems which guide me to implement a better support for D2/3. And i believe with his help, we can support D2/3 this summer.

I’ve played l-zone for sometime, and looks like that game is quite ok for now. I will give a whole test this week to see whether it’s performing well. And if so, i think we are very closing to completing D2/3.

I’ve finished the different font style supporting. e.g. italic, shadow.

And also fix the gray item and checked item in menu.

check this pic

20210726212218

I’ve fixed some implementation of lingo commands. e.g. puppetSounds. And that give us more correct behaviour for sound in direcotr.

I’ve also fixed the implementation of invertChannel. previous our coordinate calculating is wrong.

check this

20210726212931

director in scummvm can even behave better than the one in basilisk now 🙂

I’ve fixed render the text shadow, which also gives us the correct behaviour in majestic.

Today i’ve implement some lingo commands and fixed the font kerning offset.

Those works are small but really need someone to fix. So i would like to be the guy to polish the director and fix those minor errors. 🙂

Looking forward to the supporting of D2/3. See you next week.

How to add TTS to the intro of Griffon

Note

It’s just an overview.

Find the Text

In Griffon’s case, the text is in the code

So just search for “Ever since I was a child”, and there it is, in cutscenes.cpp

const char *story[48] = {
	"The Griffon Legend",
	"http://syn9.thehideoutgames.com/",
	"",
	"Programming/Graphics: Daniel Kennedy",
	"Music/Sound effects: David Turner",
	"",
	"Porting to GCW-Zero: Dmitry Smagin",
	"",
	"",
	"Story",
	"Ever since I was a child",
	"I remember being told the",
	"Legend of the Griffon Knights,",

Pass to Text To Speech Manager

Then, we would like to pass the text to text to speech manager

To call for TTS manager, first, include this in the header

#include "common/text-to-speech.h"

Then initialize TTS manager like this

Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();

Before passing the text to it, we check to see if it is null pointer, to avoid a crash

if (ttsMan != nullptr) 
	ttsMan->say("The Griffon Legend");

We also would like to check if TTS has been enabled already

(This works given that there are proper adjustments to detection.cpp about “tts_enabed”)

#include "common/config-manager.h"

if (ttsMan != nullptr && ConfMan.getBool("tts_enabled"))	
	ttsMan->say("The Griffon Legend");

Obviously we would like the whole story to have TTS, so we loop through

for (int i = 0; i < ARRAYSIZE(story); i++) {
 	// Do the TTS thing
}

Staying in pace

We would like to have the text shown on the screen be in pace with the TTS speaker.

Therefore, we would like to use the same for loop that is used for showing the text on the screen

See this in the same file, under void GriffonEngine::intro(), there is a do {

for (int i = 0; i < ARRAYSIZE(story); i++) {           // go through the story array

 	int yy = y + i * 10;                               // calculate position

	if (yy > -8 && yy < 240) {
		int x = 160 - strlen(story[i]) * 4;            // when time comes
		drawString(_videoBuffer, story[i], x, yy, 4);  // draw text to the screen
	}

  	if (yy < 10 && i == ARRAYSIZE(story) - 1) {        // last paragraph has left the screen
		return;                                        // proceed to game
	}

Because there are multiple lines appearing on the screen at once, we need a way to keep track of what has been narrated, and what hasn’t yet


int nextparagraph = 0;               //this is new

for (int i = 0; i < ARRAYSIZE(story); i++) {

    int yy = y + i * 10;                           

    // if (i == nextparagraph)     
        // do Text To Speech
        // update nextparagraph to reflect progress

    if (yy > -8 && yy < 240) {
	int x = 160 - strlen(story[i]) * 4;
 	drawString(_videoBuffer, story[i], x, yy, 4);  
    }

    if (yy < 10 && i == ARRAYSIZE(story) - 1) {        
	return;
    }

There are also two more issues to be addressed:

  1. It’s better to glue sentences that belong to the same paragraph together. The narrating sounds more natural
  2. Queue new paragraphs behind the current paragraph that is being spoken

We ended up doing this

int textToSpeech(int nextparagraph, const char *storyVariable[], int arraysize) {
 	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
 	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && storyVariable[nextparagraph][0] != 0) {
 		Common::String paragraph;
 		while (nextparagraph < arraysize && storyVariable[nextparagraph][0] != ' ') {
 			if (!paragraph.empty())
 				paragraph += " ";
 			paragraph += storyVariable[nextparagraph++];
 		}
 		while (nextparagraph < arraysize && storyVariable[nextparagraph][0] == ' ') {
 			nextparagraph += 1;
 		}
 		ttsMan->say(paragraph, Common::TextToSpeechManager::QUEUE_NO_REPEAT);
 	}
 	return nextparagraph;
 }

And calling it with this

if (i == nextparagraph)
 	nextparagraph = textToSpeech(nextparagraph, story, ARRAYSIZE(story));

Enter game after TTS is finished

Check if ttsMan is speaking (unless it’s nullptr) before return

if (ttsMan == nullptr || ttsMan->isSpeaking() == false)
 	return;



Stop TTS if intro is interrupted

If we press escape, the intro ends and so should the narrating

else if (_event.customType == kGriffonMenu) {
 	if (ttsMan != nullptr)
 		ttsMan->stop();
	return;

And That’s about it!

Full changes can be found here.

More grouping methods (Week 7 progress)

Task 1: Sorting and formatting group headers

Pretty much the title. I implemented sorting by title, and the functionality for adding prefix and suffixes for group headers. For the fold icons, lephilousophe, another ScummVM dev, wrote the code for drawing horizontal triangles, which when merged will work nicely with the list.

Here is the list rendering with the patch

Task 2: More grouping methods, and a way to switch between them

The groups can be selected from a dropdown on the top
Currently available grouping methods

I went ahead and implemented few simple groups, which rely on nothing more than the scummvm.ini configuration, later down the line, we will need to download and pass the ScummVM database of games to the launcher so that we can make more interesting groups like “By Publisher”, or “By Series”, etc., as well as provide better headers instead of abbreviations. e.g. instead of “queen”, it would be better to have “Flight of the Amazon Queen” in the above screenshot.

The full database may be found on this Google Sheet : ScummVM Data

UTF-8 Lingo and Meet MediaBand

Last week I finished up adding support for multiple text encodings to the Director engine. Now, any text contained in a Director file is automatically converted to UTF-8, greatly easing the pain of dealing with games in different languages and for different platforms. For the most part, this was rather straightforward, but there were a few places where it got interesting.

As it turns out, Director for Windows doesn’t store text in any standard encoding, but rather something partway between Mac OS Roman and Windows-1252. Any character which is present in both Windows-1252 and Mac OS Roman is stored using the Mac OS Roman code point. Any character which is present in Windows-1252 but not in Mac OS Roman is assigned an arbitrary, unique code point, and the end result is an encoding which has all the Windows-1252 characters but with different code points.

Not only is this encoding non-standard, but it isn’t even fixed! Game developers could customize the character mappings on a per-file basis in the font cross-platform map (FXmp). I haven’t seen any games where developers bothered to customize this, and I have a hard time believing that anyone cared enough about this to do so. Still, to be safe, we have to parse these mappings, then use them to convert all the text in Windows movies from the custom encoding back to Windows-1252.

The only other place where things got a bit interesting is the Lingo commands charToNum and numToChar. We can’t just use the Unicode code points for these functions. Instead, we have to map the Unicode code points to the code points of the native encoding for whatever platform we’re pretending to be—right now either Mac OS Roman, Windows-1252, or Shift JIS.

After fully Unicode-ifying the engine, I started fixing up Meet MediaBand, an interactive music video CD-ROM. With just a few fixes to sound looping logic, the play command, and the updateStage command, the beginning now works rather well!

It’s still pretty broken past the point I showed in the video, but I’ll keep fixing bugs in Meet MediaBand this week. I really like this thing, enough that I bought three copies of it, and I’m excited to get it working. 🙂

The Beginning of the End (Progress Report)

Although this week had relatively weak progress due to me getting a vaccine shot, we still progressed in various fronts.

Saves

Let’s pick up from where we left off last time. Work on implementing the rest of the save system continued. We decided to try something new to count the number of bytes of each chunk – but first, a little refresher.

The basic structure of a SAGA2 save chunk.

Each chunk starts with an 8-byte header: the 4-byte chunk name, followed by a uint32 determining the size of the chunk data. Originally the saving program determined the necessary size through a series of sizeof() operations. However, due to alignment issues and platform differences this approach is not portable. I mentioned changing everything to constants, but that in and of itself is a dangerous process, because all of the bytes would have to be counted mostly by hand, and a mistake would be fatally easy to make.

Instead, what sev proposed to do was to to write the chunk data to an intermediate buffer and stream that to the OutSaveFile along with the chunk size. This way nothing has to be counted manually.

In order to achieve that, we used Common::MemoryWriteStreamDynamic and defined some macros:

#define CHUNK_BEGIN Common::MemoryWriteStreamDynamic *out = new Common::MemoryWriteStreamDynamic(DisposeAfterUse::YES)

#define CHUNK_END outS->writeUint32LE(out->size()); \
	outS->write(out->getData(), out->size()); \
	delete out

So when saving, we can simply do the following:

void saveGlobals(Common::OutSaveFile *outS) {
	outS->write("GLOB", 4);
	CHUNK_BEGIN;
	out->writeUint32LE(objectIndex);
	out->writeUint32LE(actorIndex);
	out->writeUint16LE(brotherBandingEnabled);
	out->writeUint16LE(centerActorIndicatorEnabled);
	out->writeUint16LE(interruptableMotionsPaused);
	out->writeUint16LE(objectStatesPaused);
	out->writeUint16LE(actorStatesPaused);
	out->writeUint16LE(actorTasksPaused);
	out->writeUint16LE(combatBehaviorEnabled);
	out->writeUint16LE(backgroundSimulationPaused);
	CHUNK_END;
}

In this example the macro doesn’t seem to do much good since the number of bytes here is easily countable, but it is very helpful in bigger saving steps, and it is also less fallible than a human counting.

Another development is that I learned booleans are saved as Uint16 in the original – go figure. After fixing that our saves ran on the original and vice-versa, hooray!1

And then finally after we finished work on getting the save/loading done, we implemented support for ScummVM’s extended savefile format, with the ability to see playtime and screenshots from the launcher. The process consisted of implementing Saga2Engine::saveGameState and using appendExtendedSave to write the extended save data2 (and then reading it back with readSavegameHeader).

Global Constructors

This week we also finished the rest of the global constructors. Not much to report, other than that some global constructors are quite sneaky.

static const StaticTilePoint NullTile = {(int16)minint16, (int16)minint16, (int16)minint16};

static auxAudioTheme aats[AUXTHEMES] = {
	{false, {NullTile, 0}, 0},
	{false, {NullTile, 0}, 0}
};

The above doesn’t generate errors when NullTile is defined within the same compilation unit, but it gives out a warning if it’s defined somewhere else. Go figure.

StaticWindow autoMapDecorations[numAutoMapPanels] = {
	{*(autoMapPanelRects[0]), NULL, autoMapTopPanelResID},
	{*(autoMapPanelRects[1]), NULL, autoMapMidPanelResID},
	{*(autoMapPanelRects[2]), NULL, autoMapBotPanelResID}
};

Dereferencing here also gives out a global constructor warning. I thought it was some kind of copy constructor thing going on, but I have no idea. To fix that I simply got rid of autoMapPanelRects and used the rects directly:

StaticWindow autoMapDecorations[numAutoMapPanels] = {
	{autoMapTopPanelRect, NULL, autoMapTopPanelResID},
	{autoMapMidPanelRect, NULL, autoMapMidPanelResID},
	{autoMapBotPanelRect, NULL, autoMapBotPanelResID}
};

To fix the other constructors I did the same steps as previously: either define it within Saga2Engine or relevant classes or transform it into a pointer and initialize it somewhere inside the file if it is static (or both).

Debug Console

This is an interesting one. ScummVM has support for a debug console, if you didn’t know. An example is implemented within Quux Engine, but I based myself off of SAGA Engine.

To implement it, you create a new file console.h and there create a class that inherits from GUI::Debugger:

class Console : public GUI::Debugger {
public:
	Console(Saga2Engine *vm);
	~Console() override;

private:
	Saga2Engine *_vm;

	bool cmdKillProtag(int argc, const char **argv);
...
	bool cmdGotoPlace(int argc, const char **argv);
};

And then you initialize it within Engine::run():

_console = new Console(this);
setDebugger(_console);

And then within console.cpp you implement the debug console commands:

Console::Console(Saga2Engine *vm) : GUI::Debugger() {
	_vm = vm;

	registerCmd("kill_protag", WRAP_METHOD(Console, cmdKillProtag));
...
	registerCmd("goto_place", WRAP_METHOD(Console, cmdGotoPlace));
}

Console::~Console() {
}

bool Console::cmdKillProtag(int argc, const char **argv) {
	debugPrintf("Killing protagonist\n");

	Actor *protag = (Actor *)GameObject::objectAddress(ActorBaseID);
	protag->getStats()->vitality = 0;

	return true;
}
...

It is really quite the interesting tool to use and develop commands for. Not only that, we plan on using it to playtest and determine easily if the game is completable. We’re developing. Ain’t got time for playing here!

Command usefulness factor? It is easily verifiable.

[1] Remember me talking about enums? Weird thing is I didn’t have to change their size in order for it to work. Oh well.

[2] We actually had to change appendExtendedSave and add the method appendExtendedSaveToStream in order for it to work with WriteStreams.

With 5 Weeks Left

Over the week, not much has happened in terms of the sentiment of this project, so I think I’ll start with something easy.



Last week I came in touch with the developer that’s the expert of SCI

Here’s a *summary* of our funny dialogue

“Hi, I’m working on giving TTS to SCI”
: I’ve been wondering for some time (before you started) whether if it’s even possible to make SCI work with TTS. “sounds hard!” i thought, “i’ll believe it when i see it!”

“For all the engines I could’ve picked. This is so lucky.”
: haha same here, “of all the hobbies…”

OMG


I’m just kidding. I’m really happy I looked into the engine and got TTS to work with LB2 & DrBrain both game and demo already (I think…).

Working on SCI actually made me a better person. It’s also the same sentiment when working on the project in general.

Sometimes I don’t know if I can find some variable or make some feature come true , but there isn’t a decision to do or not to do.

It’s a decision already made. Which makes it pretty easy to do something that I would skip under normal conditions.

Regardless of what comes out, or the fact that I am dreading that it’s not going to work, I guess I’ll have to stop running away, and try a little longer.

p.s. Although this has nothing to do about GSoC: it have been about 2 months after Taiwan’s outbreak happened (A week before this blog started). I can’t believe within just 2 months, my whole family had got there queue, vaccinated. That was so fast. Let’s not compare Taiwan’s government’s productivity with mine!!!

Spaceship Warlock

Spaceship Warlock

Last week, i’m not able to do much works, but i’ve already gone back home now. So i’m ready to boost my self this week 🙂

I wrote a blog a few days ago, which recodes the process of how i implement and fix the external sound for director.

Check this:

BEGIN

This is a post which records the whole process of implementing loading external sounds file for Director.

Actually, i don’t know whether is could be called reverse-engineering. But this task really makes me “Breaks the Shell”

Let me describe it at the very begining part.

At first, i was wandering around in stambul to test whether the new implementation of fplay xobjs work well. And just after i pressed the cab tag, the sound of flying cab didn’t occurs. Instead, i got a lot ot debug output in terminal said: trying to play non-sound cast member.

So i opened Basilisk to check the original movie. Surprisingly, the original movie really uses the sound channel to play sound, but it didn’t use the sound cast member. The describtion on score shows that it was using sample sound.

I came back to scummvm and tried to print the sound channel info for all of those frames. It turns out that even we have ID which may represent the sound, but sound type isn’t match. So i opened the D2 manual to check the sound part. After reading the manual, i found that were sample sound data. And i’ve then check the sound menu in Basilisk, fortunately, the sound we are playing in sound menu is F-xx. F represent 15 which is exactly the sound type we have read. So then i start to looking the missing external sound files.

I remembered that we have the unhandled STR resource, so i tried to dump that STR resouce. Also, very fortunately, i’ve got a string which is related to the sound name that i need. Then i re-read the manual and found it said the sound file should be put at the same directory with movie. So i checked the movie directory, and found the file which has the same name as we got in STR resource.

Then the logic becomes very clear now, STR contains the file name, and we just need to read sound from that file.

I opened that sound file, and scummvm told me there is many resources contains STR and CSND. That’s it, the CSND resource must contains what i want.

I tried to google how to read the CSND resource, and also the refered the “Inside Macintosh: Sound”. And i’ve found a anwer which said the CSND resource is compressed snd resource with lzss algorithm and delta-encoding.

So i looked at their code and learned lzss algorithm. I’ve tried to implement lzss decompress for scummvm. But saidly, no matter what implementation i tried, i always can’t read the correct data.

After a lot of times of confirming, i think that the way i was reading data was wrong. There is only one csnd file that contains the sound i need. And i need 3 sound resource eventually. Thus, i think there should be a flag or something to indicate the location of those sound resources.

After comparing with other csnd resources, i found the snd resource and offset was saved at the begining of that csnd resource. So i read those offsets and try to deal with those data streams separatly. Due to the lack experience of decompiling work. I initially thought those offsets were relative to the begining of file. After a long time of doubting myself, i then compared two different external sound file. And i found those files are kind of same at the begining, where is exactly the place i thought which saves the sound data i need. Then i realize that the offset is not relative to the begining of file, but the begining of csnd resource.

Then, i finally locate the correct data area which contains the sound i need. The last step would be resolving those data.

At the begining, i thought the data was compressed by lzss, so i try to decompressed the data. After another a long time of implementing and testing, i failed. Then i refered the book which i mentioned earlier. I’ve tried to match hex code of the sample rate data.

20210714213308

But i didn’t found any sample rate data in that file. So i was convinced that the data was somehow compressed.

So i tried a lot of method to analyze and find the compressing method and try to decompress it.

check this 🙂

20210714213515

Then i re-read the D2 manual again, and i found a important info. Those sample data was fixed, there are only 4 options. And that inspired me that the reason i didn’t read sample rate data was because that was fixed in director.

And the strange unknown flag that i read earlier must indicate that sample rate.

Then i tried to open the same movie which didn’t use the external sound but sound cast member. I’ve compared the data which i think it was compressed with the sample data of sound cast. it turns out that those data was directly the sample data which no compression.(I should do this early, to compare the original one with this one, locate the data i need would be much easier for this task)

Then all of the problem was solved. And finally after 18 hours working on this one, i managed to implement reading external sound files.

Almost done with warlock now.

END

Ok, back there.

After implemented playing the external sound, mostly, i was doing the fixing work to make warlock more completeable.

Including amend fplay, which will use a queue to playing the sounds.

Also i’ve implemented some sound-related functions. E.g. soundEnabled, soundLevel. After that, the warlock can modify sound level now, which means i’m more closer to the complete version.

djsrv told me we should not overwrite the properities of cast member. So i amend our code for setting cast in sprites. A not that happy thing is after i do that, the logic in setClean where is the place we are replacing sprites and widgets becomes more complex. That means i need to organize the logic and refactor that part some day. But before doing that, i need more experiences in cast i won’t cause regression.

Today, i’ve fix the way we are using snd resource, i’ve implement the loopstart and loopend part, which bring us the correct behaviour of sounds for D2/3.

So i think i can say, that warlock-mac is fixed now. Maybe there are still small cases which has some problem that i havn’t noticed. If you find that please tell me. 🙂

Today i’m also doing some fixing work for macfonts, which seems has wrong kern offset. And after that, this week i will start working on fixing l-zone. And i think since warlock-mac is fixed now. that game won’t have much problem left.

Waiting for my good news.

Immersive slowness or why I added artificial loading times for Myst to ScummVM

Ever since I discovered the Myst series back in 2005, I’m in love with it. To me, the Myst series feels like an immersive trip to another world – it is truly something different compared to your average point-and-click adventure game. Needless to say that especially the first entries in the series – the original Myst and its successor Riven – are truly remarkable games.

In my opinion, the immersion these games provide is partially created due to technical limitations. The original Myst was released in 1993 on this incredible new format called ‘CD-ROM’, allowing for a whopping 650 Megabytes of storage.

At that time, CD-ROM drives were slow – like double-speed slow. This means access times of 80 to 200ms as well as a blazing transfer speed of 300KB/s under ideal conditions. Since even the largest hard drives had a capacity of around 1000 Megabytes, installing the game wasn’t an option. Furthermore, RAM constraints made any caching of the datafiles impossible.

This is where immersion kicks in

Myst uses some pretty clever compression techniques and an optimized file layout on the disc. Since each time you move through the game, your computer needs to load the next image. On the next move, it needs to load yet another image. The game basically forces you to slowly walk through the worlds of Myst.

I’m not sure if this was even part of the original design concept. However, it really feels that your are not supposed to rush through the game. Everything tells you to slowly explore while making sure not to miss any hints you need to solve the puzzles.

While many adventure games are limiting playback speed on their own (e.g. due to character movement), this does not apply to Myst. When you play the game on original hardware, then the loading times seem perfectly fine since you expect them.

As soon as you eliminate the loading times at all, e.g. by playing the games with ScummVM, then you might notice something is off. Now, you are not forced to slowly explore, but you can simply run through the game. Even though I’m obviously not forced to take advantage of the instant loading, the feeling that I can do this is affecting the experience in a negative way for me.

The solution

Looking for a way to improve the experience, I recently implemented loading time simulation to ScummVM. After activating this feature, it adds a delay between the different screen transitions in order to simulate the loading times of a real CD-ROM drive.

In order to show you the difference, I prepared a small video. The first half of the video shows the previous without any loading times behaviour. Obviously, I’m really exaggerating here. It should be pretty obvious that this is clearly not how the game should be played. I enable the new loading time simulation at around the 50 second mark.

While the implementation was pretty straight-forward, it was not exactly easy to get the timing right. The main issue is that I don’t have really period correct hardware at the moment. The oldest system I own is a Pentium III with 500 MHz, featuring a 16x Philips CD-RW drive and a 48x “generic” drive. Fortunately, this generic drive is compatible with Jörg Fiebelkorn’s CD-Bremse. This tool allows you to throttle your CD-ROM drives by enforcing lower reading speeds. Unfortunately, I wasn’t able to go down to double-speed since the drive wouldn’t go slower than 4x.

This meant I couldn’t rely on measurements alone. Instead, I read through many, many datasheets of various CD-ROM drives. I also tried to emulate slower drives with 86Box since they allow setting transfer speeds as well. Even though I didn’t look at the code, I have the feeling that 86Box only emulates transfer speeds, but not access time.

Limitations and what’s coming up next

All in all, the current delays are pretty arbitrary and might be subject to change in future versions. I tried to be as accurate as possible, but I simply can’t guarantee I got them absolutely right. It is also noteworthy that applied delays are the same on the original Myst and Myst: Masterpiece Edition. Due to the lack of samples, I wasn’t able to verify this, but I’d expect Myst: Masterpiece Edition to have longer loading times on the same hardware due to increased file size compared to the original edition.

One option would be to make the delay a configurable option, allowing for a wider variety of ‘drives’. For now, the new option is disabled by default. Eventuelly, we’ll probably consider making this the new default. However, this will most likely not happen until the next release is out since I want to gather feedback from our users first.

Closing words

Currently, the loading time simulation is available for Myst and Myst: Masterpiece Edition. The same concept could be applied to Riven as well. There are two things to consider:

  • Timing: Internally, Riven uses some counters and timers to keep track of – well – time-based events. I need to dig deeper into the codebase to make sure that adding artificial delays between scene transitions won’t mess with the timing. This might even involve multiple playthroughs with varying conditions.
  • The installation itself: At least the version distributed on five CDs I own provides three options during the installation: Minimal, standard and full installation. Depending on which type you choose, loading times will obviously vary as well.

If you want to give this new feature a try, you need to download a current daily build from the ScummVM website and grab your copy of Myst or Myst: Masterpiece Edition. In you don’t own the game yet: Myst: Masterpiece Edition is available at GOG.com as digital download*.

After adding the game to ScummVM, you find the new feature in the in-game settings by hitting the ‘F5’ key.

What do you think? Any feedback is very welcome, especially when you have the chance to play the game on original hardware.

*: This link is a special affiliate link. If you visit GOG.com via this link, the ScummVM project gets a commission for any purchases you make. For you, the price is exactly the same while you are still supporting the project. Thank you ❤!