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:
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:
- Clean up
interp.cpp
- 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
By clicking on the UI and fixing stuff that crashed, we eventually got other parts of the UI showing up too.
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:
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 not 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.
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.
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.
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