wiki:WikiStart

Version 10 (modified by pulkomandy, 11 months ago) ( diff )

--

Readingame

My notes about rewriting an engine for the Lectures Enjeu game.

This is a "choose your adventure" game (in French) designed for teaching kids how to learn. It used to be sold commercially but the editor does not sell it anymore. The version I got has a message encouraging free redistribution replacing the license file (both in the documentation and in the app itself).

Some of the adventures were initially published as books by schoolchildren, and adapted into electronic versions here.

I remember playing this at school as a kid, and recently I decided to take a closer look at how it works. Maybe I will get as far as rewriting an engine for it so the games can be played on modern systems?

General look

This is a DOS application. A quick look at the ASCII strings in the executable tells us that it was compiled with Borland Turbo C++, and the user interface looks like it is based on Turbo Vision.

The engine loads each game from a separate directory with a set of .DAT files. Hopefully these contain the complete game logic.

Game files

Each game scenario is made of INIT.DAT, MSG.DAT, MSGGEN.DAT, CONDGALE.DAT, CONDSAL.DAT, CONDULTI.DAT.

All files are generally composed of chunks starting with a 16-bit size (self-including) followed by some data. A 16-bit 0 serves as a terminator chunk. The cunks do not appear to have an explicit type marker.

Message files

These two files contain the various messages that can be displayed during the adventure.

The messages are split by "room" chunks.

The first 16-bit word indicate the "room" chunk size

The single-page messages chunks are made of a size, followed by a NULL terminated string.

The complex messages chunks are made of a size, followed by more sub-chunks. Each sub-chunk is itself a size + a null terminated string.

The strings can contain various escape sequences:

%p - Replaced by the player's first name
%b - To insert an inline button (followed by a button ID)
\o - Insert an "Ok" button centered on the last line.
[NSome text] - Hyperlinks
{%f3|%m2~%m3} - Conditionals, if variable f3 is 0, show m2, else show m3
{%f6|%m6~\o1} - The contents of the condition doesn't have to be a message
{%c1|%m1|%m2|%m3~%m4} - Show message 1 if c1 is 0, message 2 if it is 1, etc.

Variables

This tells us a few things:

  • %m variables refer to submessages in a complex message
  • %f variables refer to boolean flags (such as owning or not owning an object)
  • %c variables refer to counters (such as health points)

where N is an identifier for the link (it's not a room number, since it can trigger other actions, picking items, moving to subrooms, ...)

Ok button

The \o sequence can be on its own at the end of the string, or suffixed with an ASCII encoded number. In that case the number seems to indicate which substring to show next.

TODO: in some games you can move north/south/east/west and up/down, how is thins handled? This is not used in Le Mystère des Lunettes which I have studied now (because it is simpler).

MSGGEN.DAT

MSGGEN.DAT is similar, but contains a single "room" with global ("general") messages that can be triggered from any room (such as global buttons or using objects).

Rehex script

Here is a rehex script to parse these files:

LittleEndian();

// Simple string chunks are defined by a size (self-including) and a
// string (NULL terminated)
struct lenstring
{
	uint16_t len;
	local uint16_t realLen = len - 2;
	char string[realLen];
};

// Room chunks have a size and are filled by string chunks
struct ROOM
{
	local uint16_t start = FTell();

	uint16_t sizeInBytes;

	struct lenstring substrings[0];
	while (FTell() - start < sizeInBytes)
	{
		ArrayExtend(substrings);
	}
};

struct ROOM rooms[0];

while(!FEof())
{
	ArrayExtend(rooms);
}

"COND" files

These probably implement the game logic.

CONDSAL is the scripts for each room. I guess this because it has the same number of toplevel chunks as MSG.DAT, and also because "SAL" can be for "salles" (French for "rooms").

CONDGALE may be "conditions générales" and associated with MSGGEN.DAT?

I don't know what CONDULTI is for yet, but maybe it is for global things and associated with the INIT file?

Each of the COND script contains chunks that contain sub-chunks, in a similar format to the messages files. But the content of the chunks is not decoded yet.

The rehex script, for now:

struct unknownchunk {
	uint16_t size;
	local uint16_t realLen = size - 2;
	if (size > 0) {
		char data[realLen];
	}
};

struct recursechunk {
	local uint16_t start = FTell();
	uint16_t sizeInBytes;

	struct unknownchunk innerchunks[0];
	while (FTell() - start < sizeInBytes)
	{
		ArrayExtend(innerchunks);
	}
};

struct recursechunk chunks[0];

while (!FEof()) {
	ArrayExtend(chunks);
}

Easy access to the conditions in the game

CONDGALE

  • Room 0, Chunk 0: 0E 00 8F 0E 95 01 01 95 02 01 87 01 0A 00 93 01 00
  • Room 0, Chunk 1: 09 01 00 00 97 91 09 97 FC

CONDULTI

  • Room 0, Chunk 0: 0D 64 00 96 00
  • Room 0, Chunk 1: 0D D0 00 80 FF 01
  • Room 0, Chunk 2: 0D D1 00 91 01 00
  • Room 0, Chunk 3: 0D 68 05 85 05 00 89 01 02 00 FD
  • Room 0, Chunk 4: 0D 68 05 7F 0A 01 0B 00 87 01 0A 00 FD
  • Room 0, Chunk 5: 0D 68 05 91 07 00
  • Room 0, Chumk 6: 0D 69 02 91 08 00

From this we can already see a pattern emerge:

0D is followed by an action ID. For example, 68 is Manger, 69 is Lire, 64 is OK. The next byte would be the object to which the action is associated, with 00 being no object (global buttons D0 for Inventaire and D1 for Santé) and 01 and above being indices in the objects table defined in INIT objects chunk (05 is Pain au chocolat, 02 is Plan, so this all checks out).

We can also see that 91 seems to be followed by a 16-bit value (1, 7 and 8 in these examples)?

INIT file

Not fully decoded yet.

I do not understand the start of the file.

The second part is made of chunks which all start with an ID. They don't have a size, instead they have some kind of termination pattern which is apparently different for each chunk.

0001 Widgets

This chunk defines custom buttons and possibly other elements of the GUI.

For the buttons, the layout is, with each value on 16 bits:

  • X position
  • Y position
  • Color (using EGA palette)
  • 0000 0001 0007
  • Button ID or triggered action
  • Button label (null terminated, $ can be used to highlight the shortcut character)
  • Shortcut (also null terminated)

There are probably other widgets but I don't understand the exact format yet.

The chunk is terminated by two NULL words (XPos = YPos = 0).

0002 Actions

These are the action buttons that can be associated to objects.

Each action has an identifier byte, and a label and shortcut (same format as the global buttons).

The section is terminated by a NULL byte.

0003 Objects

Not fully decoded yet.

Each object has two null-terminated strings: a name and a description. Following them are three NULL bytes and an action ID, identifying which action can be used on the object. I suppose there could be more than one action per object, to be checked with a more complex game.

There are some more bytes before and after that. The section is terminated by a NULL byte.

I am not sure where each object start, do they all start with a 1 byte? If so, what are the two extra bytes before the first object?

0004 Inventory descriptions

Descriptions as NULL terminated strings for the "objects" and "inventory" screens.

This section is terminated by a $ (0x24) byte.

0005 Action results

Some messages for various actions as NULL terminated strings.

This section is terminated by a $ (0x24) byte.

0006

This section is terminated by a $ (0x24) byte.

0007

Terminated by a NULL word

0008

Terminated by two NULL words

0009

Not terminated?

Rehex script

Not really usable yet, hardcoded to the game I'm testing with for the most part. I can improve it once I figure out more of how this works.

LittleEndian();

FSeek(0x52);

struct NullTerminatedString {
	local int64_t start = FTell();
	local int64_t end = start;

	while (ReadU8(end) != 0) {
		end++;
	}

	local int64_t size = end - start + 1;
	char string[size];
};

struct TextAndShortcut {
	struct NullTerminatedString label;
	char shortcut[2];
};

struct GlobalButton {
	uint16_t xpos;
	uint16_t ypos;
	enum <uint16_t> {
		BLACK;
		BLUE;
		GREEN;
		CYAN;
		RED;
		MAGENTA;
		YELLOW;
		GRAY;
	} bgcolor;
	uint16_t unknown[3];
	uint16_t buttonID;
	struct TextAndShortcut label;
};

struct GlobalButton globalButtons[2];

uint16_t unknown[3];

struct Action {
	uint8_t id;
	struct TextAndShortcut label;
};
struct Action actions[6];

FSeek(0xD3);

struct Object {
	uint16_t unknowprefix[4];
	struct NullTerminatedString objectName;
	struct NullTerminatedString objectDescription;
	uint8_t unknownsuffix[3];
	uint8_t linkedAction; // ID of the matching action in the actions list
	uint8_t unknown2[2];
};

struct Object objects[9];

Open questions

  • How to determine which room to start the game in?
  • How to determine the action for hyperlinks?
  • How to associate an object with an action?
  • How to initialize the flags and counters?
Note: See TracWiki for help on using the wiki.