wiki:WikiStart

Version 21 (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. Can be followed by an ASCII encoded number identifying the event that will be triggered.
\1 - At the start of a message, do not erase the previous ly printed message, instead append this one at the end
[NSome text] - Hyperlinks. N is the event number that will be triggered (ASCII encoded)
{%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.
@ - Gender marker, replaced with an e if the player said she is female, nothing otherwise
$ - Plural marker, replaced with an s if the last referenced counter in the message is not equal to 1

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, to be matched with a 0D command.

Ok button

The \o sequence can be on its own at the end of the string, or suffixed with an ASCII encoded number. The number can be matched by a 0D command in the COND files to trigger the appropriate action.

North/South/East/West/Up/Down

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).

These buttons are special: they are defined in the game executable, not in datafiles. So, do they trigger a 0D command like the others? Which parameter do they pass? Or do they use dedicated commands?

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 implement the game logic.

CONDSAL is the scripts for each room. I found 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? It contains the initialization script that is run at the start of the game and a few other global things.

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. The content of each script is a sequence of opcodes. Most but not all of them start with the 0D opcode that checks if a specific button was clicked. If this is not the case, the script is not run and the next one is tried. There can be multiple scripts matching a specific button event, usually with extra conditions.

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);
}

Opcodes

00: Return

"Return" or "End of line". If this is not present at the end of a line, the execution may continue with the next one?

01 xx: CheckFlag

Check if flag number xx is set. This is used for internal flags tracking the game state.

03 xx: HasItem

Check if the player is carrying item xx.

09 xx yyyy: IfEqual

Compare counter xx with value yyyy and continue executing the line if they are equal

0A xx yyyy: IfLess

Compare counter with value and continue if it is less than the value

0D xx yy: Check event

A lot of the conditions start with 0D. This matches with an event from a button click.

For example, if a room has a button number 1, 0D 01 00 will match that, and the corresponding action will be triggered.

Every button click is matched with conditions in the current room as well as the ones in CONDGEN and/or CONDULTI.

Actions involving objects use the second parameter, for example 0D 68 05 corresponds to action 68 done on object 05.

0E xx: Check global event

Check global events, for example event 00 is the start of the game.

7E: Or

Used to combine multiple conditions (0D, 09, 0A, ...)

7F: And?

Probably And, also used to combine several conditions.

80 xx: ?

Currently unknown

81 xx: Set flag

Set flag xx

82 xx: Clear flag

Clear flag xx

83 xx: Get item

Add item number x to the player's inventory

85 xx: Remove item

Remove item from the inventory.

87 xx yyyy: Set counter

Set counter xx to value yyyy

89 xx yyyy: Add to counter

Add value yyyy to counter xx

8A xx yyyy: Subtract from counter

Subtract value yyyy from counter xx

8F xx: Go to room

Go to room passed as a parameter (single byte)

90 xx: Go to screen

Go to a screen (in the same room) passed as a parameter.

91 xx: Show global screen

Show one of the global screens (from the unique room in MSGGEN).

93 xx: WithScreen

Combined with a "Go to room", allows to set the target screen in the new room

95 xx yy: Initialize flag

A0 xx yyyy

Compare counter xx with value yyyy. This triggers an evaluation of the scripts with matching of the corresponding IfLess/IfEqual operations

FA: stop interpreter

Do not test the remaining script lines

FC: Game over

Trigger the "game over" and stop interpreting. This will ask the user if they want to restart from the last saving point or from the start of the adventure.

FD: Continue

Continue execution with the next script line.

FE xx: Goto

Call script number xx in the current room

FF: Winning the game

The game is won if you manage to execute this opcode!

INIT file

Not fully decoded yet.

I do not understand the start of the file. Could it be opcodes to init the game?

06 00 01 00 03 00 02 00 49 00 13 00 00 00 03 00 00 00 05 00 0D 00 (this could be the initial room to start the game in) 46 00 0B 00 00 00 07 00 00 00

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];
Note: See TracWiki for help on using the wiki.