Latching on to the shoulders of giants

Latching on to the shoulders of giants

A good first step when poking around old games is to see if anybody has had the same idea. Many games, especially the classic Sierra and Lucasarts adventures, have been completely de-compiled, famously ScummVM, in particular does a fantastic job and have absorbed many small projects into its ecosystem as well.

BaKGL seems to have made some progress towards remaking the game and still sees a bit of activity (last update was 10 months ago). In many ways, this person seems to be interested in exactly the same thing as me; learning how the game actually works underneath. However, at this stage I wanted to just see if I could learn anything from the assets. BaKGL is based on XBaK, which did a lot of work describing how assets are stored.

The key files in Betrayal at Krondor are:

KRONDOR.CFG
INSTALL.SCR
STD.DRV
RESOURCE.CFG
VMCODE.OVL
DRIVE.CFG
SNDBLAST.DRV
GENMIDI.DRV
INSTALL.HLP
SX.OVL
KRONDOR.EXE
ADL.DRV
INSTALL.EXE
INSTALL.TXT
FRP.SX
MT32.DRV
STARTUP.GAM
KRONDOR.RMF
KRONDOR.001

A lot of these we can guess the purpose of; for example, KRONDOR.EXE is the main executable and GENMIDI.DRV is the MIDI driver.

By its size of alone (12 MB) it seems likely that KRONDOR.001 must be the main resource file. This would be a good place to start.

Searching for this in Ghidra, however, comes up with... nothing.

A few mentions of file names with krondor in them, but not KRONDOR.001. Do however not krondor.rmf. These are all values stored on the stack, suggestion the code will reference them directly.

However some of the other values might be worth exploring.

KRONDOR.CFG is a small file, likely holding the user's config settings from the user (for example, which sound card to use).

The non-file names are string references: You do not have enough memory to run Betrayal... is obviously an error message that appears if the user does not have sufficient memory to run the game. This could be a potential entry point into the game when we get to that stage, but it's not what we’re after right now.

Loading Betrayal at Krondor... please wait. is the string displayed when the game starts up (while waiting for the intro to load). Another good entry point into the game.

krondor.rmf refers to a 14KB file in the game directory. Potentially interesting. Ghidra was able to find where it was being referenced fairly easily.

Ghidra's decompilation of the function that references krondor.rmf. Note that various names given to some of the variables and functions were done on the back of analysis and some guesswork with the assistance of Gemini, but feels pretty accurate at this point.

This seemed promising. When looking at XBaK it was named ResourceIndex. From ResourceIndex.h

struct ResourceIndexData
{
    unsigned int hashkey;
    std::streamoff offset;
    unsigned int size;
};

class ResourceIndex
{
private:
    std::string resourceFilename;
    unsigned int numResources;
    std::map <const std::string, ResourceIndexData> resIdxMap;
    std::map<const std::string, ResourceIndexData>::iterator resIdxIterator;
public:
    ResourceIndex();
    virtual ~ResourceIndex();
    void Load ( const std::string &filename );
    void Save ( const std::string &filename );
    std::string GetResourceFilename() const;
    unsigned int GetNumResources() const;
    bool Find ( const std::string &name, ResourceIndexData &data );
    bool GetFirst ( std::string& name, ResourceIndexData &data );
    bool GetNext ( std::string& name, ResourceIndexData &data );
};

Based on this and the rest of the code from XBaK, we can begin to see a picture of the structure of this file:

Header:

Offset Size Description
0 4 bytes Version (u32 LE, must be 1)
4 2 bytes Unknown (u16 LE)
6 13 bytes Resource filename (null-terminated string)
19 2 bytes Number of resources (u16 LE)

Resource Index (repeats for each resource):

Offset Size Description
0 4 bytes Hash key (u32 LE)
4 4 bytes Offset into .001 file (u32 LE)

With this information, we can use Claude to write a simple tool to display the contents of KRONDOR.RMF:

$ cargo run --bin rmf-explorer gamedata/ | head -n 50
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rmf-explorer gamedata/`
=== KRONDOR.RMF Explorer ===

RMF size: 14165 bytes (0.01 MB)

Resource File: KRONDOR.001
Number of Resources: 1768

      Hash     Offset
---------------------
0x64425934          0
0x64525934        113
0x65425934        328
0x65525934        600
0x66425934        713
0x66525934        867
0x67425934        980
0x67525934       1093
0x68425934       1209
0x68525934       1322
0x69425934       1435
0x69525934       1548
0x6a425934       1661
0x6a525934       1774
0x6b425934       1887
0x6b525934       2000
0x6672ce81       2113
0x39ab894a       2459
0xe967cc58       3260
0x1637ffd1       4061
0x62f07c31       4862
0x65907cb6       5663
0x65a07cb6       6464
0x6ad07cb7       7265
0xa1407cb8       8066
0x65f07cb9       8867
0x47c07cba       9668
0x61407cba      10469
0x78c07cbb      11270
0xd7a07cc0      12071
0xaca07cc7      12872
0x86c07cc8      13673
0x94607cc8      14474
0x47cfaa01      15275
0x37b272dd      15465
0x478fab01      19441
0x479fab01      24751
0x47afab01      34384
0x47bfab01      40687
0x47cfab01      53421
0x38b272dd      57444

So far, so good. We've also discovered where KRONDOR.001 is referenced from. Next time, we'll start linking the RMF data to KRONDOR.001.