Week ∞

This time’s post will be a big summary of the events up until the present point. I feel like this is necessary so we can get into cool in-depth stuff soon!

Tile Drawing

Similar to the debug images we tested last time, we first get clean up tile.cpp by getting rid of any remaining traces of their memory manager rmem. Then we adopt some debug methods and use them inside of the main run function.

void testTileRendering() {
	tileRes = resFile->newContext(MKTAG('T', 'I', 'L', 'E'), "tile resources");
	listRes = objResFile->newContext(MKTAG('L', 'I', 'S', 'T'), "list resources");
	resImports = (ResImportTable *)LoadResource(listRes, MKTAG('I', 'M', 'P', 'O'), "res imports");

	initResourceHandles();
	mainPort.setDisplayPage(&protoPage);
	initPanelSystem();
	initDisplayPort();
	initDisplay();
	initGameMaps();
	testTiles();
}

Along with the actual tile debug blitting function testTiles, I also call a lot of initialization methods. These methods are supposed to be called the game’s own initialization tower procedure, located in a file called towerfta.cpp. However, since we’re trying to blit tiles without going through the initialization, we have to call these methods manually.1

void testTiles() {
	initTileCyclingStates();
...
	setCurrentMap(0);
	PlayModeSetup();

	// draws tiles to tileDrawMap.data
	drawMetaTiles();

	uint8 *img = tileDrawMap.data;
	Point16 size = tileDrawMap.size;
	debugC(3, kDebugTiles, "img = %p, size = %d,%d", (void *)img, size.x, size.y);

	Graphics::Surface sur;
	sur.create(size.x, size.y, Graphics::PixelFormat::createFormatCLUT8());
	sur.setPixels(img);
	sur.debugPrint();
	g_system->copyRectToScreen(sur.getPixels(), sur.pitch, 0, 0, sur.w, sur.h);
...
}

The testTiles method has quite some stuff going on, but the important parts here are setCurrentMap(0) and drawMetaTiles(). drawMetaTiles takes in the current map, which we set with setCurrentMap, and then blits it into gPixelMap tileDrawMap. It is analogous to OSystem’s Graphics::Surface.

About 3 or 4 hierarchies below the drawMetaTiles call, we have a call to drawTile, which is defined in assembly. Thus, this is probably the method we need to implement.

Thankfully, SAGA engine has a method of the same name implemented in saga/isomap.cpp. After implementing it inside of SAGA2, commenting out irrelevant blocks and changing values to FTA2 standards, we get the following result:

Never did squares look so beautiful.

It blits mostly correctly. Here we see horizontal stripes throughout the image, which we later learn is the result of the tile width constant. Spoilers.

Scripts

We run the same methodology on scripts:

  1. Clean up interp.cpp
  2. Find a way to test it out.

For item 1 however, we have to be careful of these kinds of expressions too:

return strSeg + ((uint16 *)strSeg)[strNum];

The problem with this expression is that we’re trying to read a WORD (2 bytes) from a byte array that is loaded from an external resource (i.e. files). The result will change depending on whether the machine is Big Endian or Little Endian. For this reason, we use READ_LE_16 to specify 16 bits to be read in Little Endian:

return strSeg + READ_LE_INT16(strSeg + 2 * strNum);

Viva OSystem! Portable code has never been so easy.

For item 2 we do the following:

void testScripts() {
	scriptCallFrame scf;
	//for (int i = 1; i < 100; ++i)
	//	runScript(i, scf);
	runScript(1, scf);
}

We also confirm it is trying to run scripts through debug statements:

Scripts: op_enter: [SagaObject].WriteName 
Scripts: op_return

Initialization

I will mostly just skim over the procedure, but many of initialization steps deserve their own posting.

Now that we have the basic resource loading and image displaying working, it is time to the proper tower initialization I mentioned before. The entire structure is as follows

TowerLayer tower[fullyInitialized] = {
	{ nothingInitialized,        &initTowerBase,        &termTowerBase },
	{ errHandlersInitialized,    &initErrorManagers,    &termErrorManagers },
	{ delayedErrInitialized,     &initDelayedErrors,    &termDelayedErrors },
	{ activeErrInitialized,      &initActiveErrors,     &termActiveErrors },
	{ configTestInitialized,     &initSystemConfig,     &termTowerBase },
	{ memoryInitialized,         &initMemPool,          &termMemPool },
	{ introInitialized,          &initPlayIntro,        &termPlayOutro },
	{ timerInitialized,          &initSystemTimer,      &termSystemTimer },
	{ resourcesInitialized,      &initResourceFiles,    &termResourceFiles },
	{ serversInitialized,        &initResourceServers,  &termResourceServers },
	{ pathFinderInitialized,     &initPathFinders,      &termPathFinders },
	{ scriptsInitialized,        &initSAGAInterpreter,  &termSAGAInterpreter },
	{ audStartInitialized,       &initAudioChannels,    &termAudioChannels },
	{ tileResInitialized,        &initResourceHandles,  &termResourceHandles },
	{ palettesInitialized,       &initPalettes,         &termPalettes },
	{ mainWindowInitialized,     &initDisplayPort,      &termDisplayPort },
	{ panelsInitialized,         &initPanelSystem,      &termPanelSystem },
	{ mainWindowOpenInitialized, &initMainWindow,       &termMainWindow },
	{ guiMessInitialized,        &initGUIMessagers,     &termGUIMessagers },
	{ mouseImageInitialized,     &initMousePointer,     &termMousePointer },
	{ displayInitialized,        &initDisplay,          &termDisplay },
	{ mapsInitialized,           &initGameMaps,         &termGameMaps },
	{ patrolsInitialized,        &initRouteData,        &termRouteData },
	{ spritesInitialized,        &initActorSprites,     &termActorSprites },
	{ weaponsInitialized,        &initWeaponData,       &termWeaponData },
	{ magicInitialized,          &initSpellData,        &termSpellData },
	{ objectSoundFXInitialized,  &initObjectSoundFX,    &termObjectSoundFX },
	{ prototypesInitialized,     &initObjectPrototypes, &termObjectPrototypes },
	{ gameStateInitialized,      &initDynamicGameData,  &termDynamicGameData },
	{ gameModeInitialized,       &initGameMode,         &termGameMode },
	{ gameDisplayEnabled,        &initTop,              &termTop },
	{ procResEnabled,            &initProcessResources, &termProcessResources }
};

While TowerLayer is a struct defined the following way:

struct TowerLayer {
	int 			ord;
	pPROGRAM_INITIALIZER	init;
	pPROGRAM_TERMINATOR	term;
	configProblem		onFail;
};

pPROGRAM_INITIALIZER and pPROGRAM_TERMINATOR are simply typedefs for function pointers. We don’t need to care about configProblem right now.

You may be able to guess how the initialization works now. We go through each step in the tower table, calling the appropriate initialization functions (and vice-versa when terminating). So our plan of attack is to simply connect to the game’s main loop, reach the tower initialization process and fix the errors that pop up (and boy, will many errors pop up).

These errors are mostly memory management ones related to rmem (such as the use of RHANDLE). Some others arise from the difference in architecture (such as different pointer sizes) or from writing into “packed” structs. Yet more errors result from their custom timer system.

rmem related errors are easy to fix, but resource loading-related ones require direct writing into each resource struct, like this:

static void readGameObject(hResContext *con, ResourceGameObject &obj) {
	obj.protoIndex = con->readS16LE();
	obj.location.u = con->readS16LE();
	obj.location.v = con->readS16LE();
	obj.location.z = con->readS16LE();
	obj.nameIndex = con->readU16LE();
	obj.parentID = con->readU16LE();
	obj.script = con->readU16LE();
	obj.objectFlags = con->readU16LE();
	obj.hitPoints = con->readByte();
	obj.misc = con->readU16LE();
}

We end up scraping this design later on though, opting instead to load resources in each struct/class’s constructor or member functions.

Finally, the timer related errors caused headache for some time because it caused the game to stop progressing without us knowing. This particular example doesn’t happen at this moment in time, but I’m gonna explain it here in order to not break the flow. I was running around in circles trying to figure out just why was the screen not updating*, until I noticed there was a check like this in a routine that updates the screen:

//  If it's time to do a new frame.
if (frameAlarm.check()
	&&  tileLockFlag == 0) {

We hadn’t implemented timers yet, so this check never passed. After that, sev (my mentor) rewrote the timers so that they made use of OSystem’s TimerManager.

After fixing some more initialization steps, enabling the cursor and some other stuff, we eventually were into the event loop looking at this:

Drawing UI

We knew some things were being drawn on the screen. We also knew we could draw images properly due to our previous test run. So why were we getting garbage?

Well, spoilers ahead.

1. The main surface on which the UI is drawn on was not initialized

2. Our blitting functions were being ignored by a display check that returned false due to Windows DD not being present.

After fixing both of those issues, things were properly drawing

Would you look at the progress.

By clicking on the UI and fixing stuff that crashed, we eventually got other parts of the UI showing up too.

Tasty.

I went to sleep and left it to sev to implement the rest of the sprite related routines. He also fixed the tile width constant to the proper value.

Drawing Platforms

So our next step was to figure out what’s going on with the game map. Cutting to the chase, something called DList was the problem. I haven’t mentioned it much, but removing it completely is also one of our goals.
In FTA2, platform (map) fetching makes use of a cache design, and after (haphazardly) getting rid of it I was greeted with this sight.

This sure seems like a lot of progress, but I later learned that the window was not updating properly when we fixed the problem mentioned above*.

After that was fixed, we started seeing the map flickering:

https://media.discordapp.net/attachments/843917188004053012/855029634269446184/fta2.gif

I’m gonna spoil once again, but this was the result of not properly porting the cacheing in the platform fetching.

Headache-inducing Scripts

This problem took a particularly long time to figure out. It deserves its own post, so I’m going to explain the details just yet.

Basically, we were crashing at a script execution. We thought of many possibilities, such as the wrong script being called. However sev checked against the original and confirmed that we were in the correct script call. The script call in question was this one:

int16 scriptSelectNearbySite(int16 *args) {
	MONOLOG(SelectNearbySite);
	TilePoint       tp;

	tp = selectNearbySite(args[3],
	                      TilePoint(args[0], args[1], args[2]),
	                      args[4],
	                      args[5],
	                      args[6]);

	if (tp == Nowhere) return 0;
	scriptCallFrame     &scf = thisThread->threadArgs;

	scf.coords = tp;
	return true;
}

Particularly, we crashed because the first argument, args[3], didn’t represent a valid WorldID.

Cutting to the chase, the problem was that their scripts access memory according to a 32-bit layout. In my 64-bit machine, the structs are different. In particular, pointer sizes are different. This difference caused the script to grab the wrong address.

After noticing that (and feeling very smart about it), we fixed it by implementing a packed struct object containing all data in the expected address.

Motion At Last

And with that, we were able to walk around.

You may see the tiles not loaded in the left side. The reason for that is because we are drawing at the wrong offset. After correcting for that and removing rmem from speech.cpp, we are able to see something resembling gameplay.

We also had our fair share of weird bugs.

This is… requiem.

The above was the result of, again, improper platform fetching. More specifically, I had removed some lines of code and forgotten about them. Oops.

We (aka sev) fixed the buckets on the inventory by extracting the method codes from the disassembly. After removing some more rmem calls, we were able to learn skills, open our sack, open the map and much more.

Was this a game all along?

One problem though, is that actors were either not spawning or not moving. The later was fixed by setting proper flags in assignment creation.2 The former was fixed by fixing a typo. Cheers.

if (sect) {
	if (sect->isActivated())
		sect->activate(); // should be simply activate()
}

Tasks3 not being cleared was also another problem. That one was fixed by comparing the code with the original and putting delete expressions where they were missing. Code refactoring is dangerous sometimes.

Present Day, Present Time

The most recent (and also yet unsolved) bug is the one that relates to NPC Kwarrel.

FTA2: Halls of the Talking Corpses

He dies while talking to you. I wonder if that could be considered murder.

We have a few leads, such as multiple speech tasks being created. It probably relates to timers, but we have no clue about what exactly is wrong yet.


Alright, it was a bit long, but that’s mostly it. Quite a few details are missing, but we’ll fill them up as we go!


1: I spent a lot of time to get the correct procedure here.

2, 3: Assignments? Assignment creation? Tasks? We’ll talk about those later.


Leave a Reply

Your email address will not be published. Required fields are marked *