Fancy logo
Yadex 1.5.2 (2002-01-30)

Hacker's guide




Blah

Foreword

This documents is aimed at people who want to hack Yadex. It is very incomplete, partly due to lack of time, partly because as some subsystems are going to be rewritten, I'd rather not spend too much time documenting them. But if you're interested in a particular area of Yadex's bowels that does not appear here, don't hesitate to let me know.

I apologize for the poor quality of Yadex's code but it seemed to me it was better to release something imperfect now than something clean two years from now. If you want to improve it, be my guest.

Introduction

Yadex is written in a mixture of C and C++. The Unix version interfaces with X through Xlib directly ; it uses no toolkit. The DOS version uses BGI (Borland Graphics Interface), a rather low-level API to set the video mode, draw lines, text, etc.

Original platform

The Unix version has been developped with GCC 2.7.2, EGCS 1.0.3, EGCS 1.1.1, libc5, glibc2 and XFree 3.3 on a PC K6/200 with Linux 2.0.29, 2.0.30 and 2.0.34.

The DOS version has been developped with Borland C++ 4.0 on a PC 486 DX4/75 with MS-DOS 6.22.

Yadex should be compilable on all reasonable Unix-and-X11 platforms provided that

To compile on platforms where $c short or $c long don't have the needed size, just change the definitions of $c u16, $c i16, $c u32 and $c i32 in $c yadex.h.

Historic background

Yadex descends from DEU 5.21.

DEU 5.21 was written by Raphaël Quinet, Brendon Wyber and others and released on 1994-05-21. As you probably already know, DEU was a real-mode DOS program, in C, compiled with Borland C++ 4.0 (I think) and using BGI for its output.

In the mists of time (that is probably 1996), I began to hack DEU for my own use. In 1997, other people began to use my hack and I gave it a name : "Yade" (which meant Yet Another Doom Editor). It was still a real-mode DOS program.

In june 1998, tired of rebooting to DOS every time I wanted to do some Doom level editing, I started porting Yade to Linux. As there already was a Unix program called "Yade" (Yet Another Diagram Editor), I changed the name of my baby to "Yadex". At the same time, I began to use C++ in places so that's why Yadex is such an ugly mixture of languages.

Development cycle

Compiling, installing, testing

OK. So I want to hack Yadex ; what do I do now ? The obvious development cycle is

  1. modify the files in src/,
  2. type make,
  3. type su -c 'make install',
  4. run Yadex and test.

However, there are a few things to know that can make you more efficient.

To compile with debugging information, you don't have to modifiy the makefile ; just use the appropriate target (dyadex). The resulting executable is put in dobj/0/ instead of obj/0/.

You don't have to install to test : you can test in place with the special targets test and dtest (test is for the regular executable, dtest is for the debugging executable and is my favourite target by far). When testing through make, you can't pass arguments to Yadex directly : you have to pass them through the A macro. For example :

  $ make dtest A='-g heretic foo.wad'

You can also run your hack through the debugger with the dg (GDB) and dd (DDD) targets. With those targets, you can't use the A= convention : you have to type "set args blah blah blah" at the debugger prompt.

  $ make dg
  (gdb) set args -g heretic foo.wad
  (gdb) run

Even if you want to install your hack, you may want to keep the original Yadex around, for reference or for a good laugh. To do that, edit the VERSION file before compiling your hack. If your hack bears a different version number, it will not overwrite the original version. You don't have to change the version number much : you can just change "3.8.0" into "3.8.0a" or "3.8.0.0" for example.

The programming environment

Memory allocation

You're not supposed to use $c malloc() and $c free() but $c GetMemory(), $c FreeMemory(), $c GetFarMemory() and $c FreeFarMemory() instead. Why ?

$c GetMemory() and friends manage more things for you. They include an anti-fragmentation system, they try to swap things out when memory is tight (this is an only an issue for the 16-bit DOS version) and if they fail, they call $c fatal_error() so you don't need to check their return value.

The reason for $c GetFarMemory() is that, for the 16-bit DOS version, it can allocate more than 64 kB ($c GetMemory() cannot). I must say that I don't use $c GetFarMemory() a lot myself because I don't like the idea of having to use two different memory allocation routines depending on the size I expect to allocate. I modified $c GetMemory() so that it accepts an unsigned long but checks that the passed value fits in $c size_t. In other words, if you call $c GetMemory() with a size of 65,536 the 16-bit DOS version will trigger a fatal error immediately instead of silently allocating 1 byte and letting you search afterwards why the program behaves strangely. A better fix would be to make $c GetMemory() call $c GetFarMemory() when the block is too large for $c malloc(). Any volunteers ?

Memory allocated with $c GetMemory() is guaranteed to be freeable with $c free(). On the other hand, memory allocated with $c GetFarMemory() must be freeed with $c FreeFarMemory().

Endianness

The 16-bit and 32-bit integers in a wad file are always little-endian, whatever the platform.

On the other hand, Yadex keeps all its in-core integer in the platform's native endianness, i.e. in little-endian format on little-endian machines and in big-endian format on big-endian machines.

The wad endianness <-> native endianness conversion is done automagically by $c wad_read_i16() and $c wad_read_i32().

To maintain compatibility with big-endian platforms, all I/O of multibyte integers should be done with those functions.

The directory

Principle

Like Doom, Yadex accesses lumps through an indirection layer that called the directory. The directory is basically a list of all the lumps that exist in the iwad and/or at least one of the pwads with, for each lump, the information necessary to read it. Each directory entry has 4 fields ; the name of the lump, its offset in the wad, its length, and an indirect pointer to the file descriptor for the wad that contains it.

As you might expect, if the same lump is present in more than one wad, it has only one directory entry, pointing to the last occurence.

When you need to load a lump by name, you call FindMasterDir(). It returns you a pointer on its entry in the directory, which in turn contains everything you need to read it.

Managing the directory

The directory is kept in memory at all times. It is only modified when you initially load the iwad and when you load or unload a pwad. And of course when you delete the directory. Simple, isn't it ?

Ha-ha, gotcha, this is not how the actual API works. The actual set of operations is somewhat different in that it does not include unloading a pwad. Instead, there is a function to close all unused pwads, that is pwads that are not referenced by a single entry in the directory. I suppose that the reason why it was done this way is that unloading a pwad is a bit more complicated, since it involves finding the previous "provider" of all lumps that the wad to unload used to "provide". I guess the only way to do that would be rebuilding the directory from scratch. This "feature" is discussed in the Comments section below. Anyway, here is the API.

OpenMainWad()
Create the directory and fill it with the contents of the directory of the iwad.
OpenPatchWad()
Add a pwad to the list of the open wads and to the directory.
CloseUnusedWadFiles()
Close all wads that are not used in the directory anymore.
CloseWadFiles()
Close all open wads and delete the list of open wads and the directory.

Implementation

It has not been said explicitly so far but Yadex maintains a list of all open wads (iwads and pwads). It is a linked list of the WadFileInfo structure. The global variable WadeFileList is a pointer to the first element of the list, which is always the iwad. The type WadPtr is an alias for "struct WadFileInfo *" (yes, I know, it's confusing to have two names for the same thing).
struct WadFileInfo
   {
   WadPtr next;                 // Next file in linked list
   char *filename;              // Name of the wad file
   FILE *fd;                    // C file stream information
   char type[4];                // Type of wad file ("IWAD" or "PWAD")
   i32  dirsize;                // Directory size of wad
   i32  dirstart;               // Offset to start of directory
   DirPtr directory;            // Array of directory information
   };
typedef struct WadFileInfo *WadPtr;
extern WadPtr WadFileList;      // List of wad files

The directory itself is a linked list of the MasterDirectory structure. The global variable MasterDir points to the first element of the directory. The type MDirPtr is an alias for "struct MasterDirectory *".

struct Directory
   {
   i32  start;                  // Offset to start of data
   i32  size;                   // Byte size of data
   char name[WAD_NAME];         // Name of data block
   };
struct MasterDirectory
   {
   MDirPtr next;                // Next in list
   WadPtr wadfile;              // File of origin
   struct Directory dir;        // Directory data (name, offset, size)
   };
typedef struct MasterDirectory *MDirPtr;
extern MDirPtr MasterDir;       // The master directory

Sample code

To be written : searching for a directory entry. The same thing, in an incremental fashion.

Comments

The decision to use a directory is arguable. It is convenient for the programmer when he/she is looking for the effective instance of a lump, which is the case most of the time. But it also prevents the user from editing a resource (notably, a level), if it has been overridden in another wad. It's not a big deal but I don't like it.

The fact that the directory managing operations don't include removing a pwad from the directory means that there is no way for the user to "unload" a pwad. The "read" command has no inverse. The only way to do it is to restart Yadex.

Another somewhat non obvious design decision is that, in most places where the directory is updated, CloseUnusedWadFiles() is called too. This means that you can't load two pwads that have exactly the same lumps. As the second pwad is loaded, the first one is automatically (and silently) unloaded. Not a big deal either but, as a user, I don't like the programs I use to behave like that. I'll illustrate my point with the following scenario, which assumes the "unload pwad" function exists :

  1. Load pwad A (MAP01)
  2. Load pwad B (MAP01)
  3. Unload pwad B
  4. Edit MAP01
At this point, as you have backtracked on your action of loading B, you would expect to see the MAP01 from A, wouldn't you ? Instead you get the MAP01 from the iwad, because A was unloaded as you loaded B. From a user point of view, such a behaviour is confusing and therefore to be avoided.

I don't like the directory management API very much because it's unexpectedly asymmetric and therefore neither intuitive nor orthogonal. The way Yadex plays games with the directory is really disgusting and confusing to me.

I should look into replacing the iwad "on the fly", so that the user is able to change the game parameter dynamically, without restarting Yadex. In fact, the ultimate goal is to remove the game parameter completely or, more precisely, to make it local to a Level object, automatically adjusting and dynamically modifiable by the user.

The wad data

TBD

The level data

Structure

The data for a level is stored in 10 variables that are declared in levels.h and defined in levels.cc. Here they are :
int   NumThings;         /* number of things */
TPtr  Things;            /* things data */
int   NumLineDefs;       /* number of linedefs */
LDPtr LineDefs;          /* linedefs data */
int   NumSideDefs;       /* number of sidedefs */
SDPtr SideDefs;          /* sidedefs data */
int   NumVertices;       /* number of vertices */
VPtr  Vertices;          /* vertices data */
int   NumSectors;        /* number of sectors */
SPtr  Sectors;           /* sectors data */

Scope and lifetime

Since those variables (and other critical ones) are unfortunately static, it's not possible to open editing windows on several different levels simultaneously. This should be fixed in the future by making the level data a class and turning those variables into members of that class.

I think that the level data class should be separate from the editing window class because it might be useful to open several editing windows on the same level. Separate class should also make the design of the read level and write level routines cleaner and simpler.

Maintenance

It's of paramount importance for the stability and reliability of Yadex that the level data be maintained in a consistent state at all times. In particular,

Loading

The SEGS, SSECTORS, NODES, BLOCKMAP and REJECT lumps are ignored. The other lumps are read into the level data variables with a special case for VERTEXES ; vertices that are not used by any linedef are ignored (such vertices are believed to come from the nodes builder and therefore be irrelevant to level editing). The linedefs vertices references are updated if necessary.

Since the endianness of the wad files is fixed (little endian) and thus not necessarily identical to the endianness of the CPU, reading 2- and 4-byte integers from the file is done through special endianness-independant routines.

Saving

If $c MadeMapChanges is false, the SEGS, SSECTORS, NODES, BLOCKMAP, REJECT and VERTEXES lumps are copied from the original file. Else, they are output with a length of zero bytes, except the VERTEXES lump that is created from the the level data ($c NumVertices and $c Vertices).

Since the endianness of the wad files is fixed (little endian) and thus not necessarily identical to the endianness of the CPU, writing 2- and 4-byte integers to the file is done through special endianness-independant routines.

Editing windows, or the lack of it

Too many global variables...

See "_edit.h".

The editor loop

All the time the user spends editing a level is spent within a certain function, the editor loop, a.k.a. $c EditorLoop() in $c editloop.c. It's essential for you to understand it if you want to get how Yadex works right.

The $c EditorLoop() is an endless loop (okay, not really endless) which, for each iteration, first, refreshes the display, second, waits for an event, third, processes that event. I could have put things in a different order but I liked the idea of displaying something before waiting for user input.

Because the event input and the graphical output are complet argsuments is aimed at people who want to hack Yadex. It is very incomplete, partly due to lack of time, partly because as some subsystems are going to be rewritten, I'd rather not spend too much time documenting them. But if you're interested in a particular area of Yadex's bowels that does not appear here, don't hesitate to let me know.

I apologize for the poor quality of Yadex's code but it seemed to me it was better to release something imperfect now than something clean two years from now. If you want to improve it, be my guest.

Introduction

Yadex is written in a mixture of C and C++. The Unix version interfaces with X through Xlib directly ; it uses no toolkit. The DOS version uses BGI (Borland Graphics Interface), a rather low-level API to set the video mode, draw lines, text, etc.

Original platform

The Unix version has been developped with GCC 2.7.2, EGCS 1.0.3, EGCS 1.1.1, libc5, glibc2 and XFree 3.3 on a PC K6/200 with Linux 2.0.29, 2.0.30 and 2.0.34.

The DOS version has been developped with Borland C++ 4.0 on a PC 486 DX4/75 with MS-DOS 6.22.

Yadex should be compilable on all reasonable Unix-and-X11 platforms provided that

To compile on platforms where $c short or $c long don't have the needed size, just change the definitions of $c u16, $c i16, $c u32 and $c i32 in $c yadex.h.

Historic background

Yadex descends from DEU 5.21.

DEU 5.21 was written by Raphaël Quinet, Brendon Wyber and others and released on 1994-05-21. As you probably already know, DEU was a real-mode DOS program, in C, compiled with Borland C++ 4.0 (I think) and using BGI for its output.

In the mists of time (that is probably 1996), I began to hack DEU for my own use. In 1997, other people began to use my hack and I gave it a name : "Yade" (which meant Yet Another Doom Editor). It was still a real-mode DOS program.

In june 1998, tired of rebooting to DOS every time I wanted to do some Doom level editing, I started porting Yade to Linux. As there already was a Unix program called "Yade" (Yet Another Diagram Editor), I changed the name of my baby to "Yadex". At the same time, I began to use C++ in places so that's why Yadex is such an ugly mixture of languages.

Development cycle

Compiling, installing, testing

OK. So I want to hack Yadex ; what do I do now ? The obvious development cycle is

  1. modify the files in src/,
  2. type make,
  3. type su -c 'make install',
  4. run Yadex and test.

However, there are a few things to know that can make you more efficient.

To compile with debugging information, you don't have to modifiy the makefile ; just use the appropriate target (dyadex). The resulting executable is put in dobj/0/ instead of obj/0/.

You don't have to install to test : you can test in place with the special targets test and dtest (test is for the regular executable, dtest is for the debugging executable and is my favourite target by far). When testing through make, you can't pass arguments to Yadex directly : you have to pass them through the A macro. For example :

  $ make dtest A='-g heretic foo.wad'

You can also run your hack through the debugger with the dg (GDB) and dd (DDD) targets. With those targets, you can't use the A= convention : you have to type "set args blah blah blah" at the debugger prompt.

  $ make dg
  (gdb) set args -g heretic foo.wad
  (gdb) run

Even if you want to install your hack, you may want to keep the original Yadex around, for reference or for a good laugh. To do that, edit the VERSION file before compiling your hack. If your hack bears a different version number, it will not overwrite the original version. You don't have to change the version number much : you can just change "3.8.0" into "3.8.0a" or "3.8.0.0" for example.

The programming environment

Memory allocation

You're not supposed to use $c malloc() and $c free() but $c GetMemory(), $c FreeMemory(), $c GetFarMemory() and $c FreeFarMemory() instead. Why ?

$c GetMemory() and friends manage more things for you. They include an anti-fragmentation system, they try to swap things out when memory is tight (this is an only an issue for the 16-bit DOS version) and if they fail, they call $c fatal_error() so you don't need to check their return value.

The reason for $c GetFarMemory() is that, for the 16-bit DOS version, it can allocate more than 64 kB ($c GetMemory() cannot). I must say that I don't use $c GetFarMemory() a lot myself because I don't like the idea of having to use two different memory allocation routines depending on the size I expect to allocate. I modified $c GetMemory() so that it accepts an unsigned long but checks that the passed value fits in $c size_t. In other words, if you call $c GetMemory() with a size of 65,536 the 16-bit DOS version will trigger a fatal error immediately instead of silently allocating 1 byte and letting you search afterwards why the program behaves strangely. A better fix would be to make $c GetMemory() call $c GetFarMemory() when the block is too large for $c malloc(). Any volunteers ?

Memory allocated with $c GetMemory() is guaranteed to be freeable with $c free(). On the other hand, memory allocated with $c GetFarMemory() must be freeed with $c FreeFarMemory().

Endianness

The 16-bit and 32-bit integers in a wad file are always little-endian, whatever the platform.

On the other hand, Yadex keeps all its in-core integer in the platform's native endianness, i.e. in little-endian format on little-endian machines and in big-endian format on big-endian machines.

The wad endianness <-> native endianness conversion is done automagically by $c wad_read_i16() and $c wad_read_i32().

To maintain compatibility with big-endian platforms, all I/O of multibyte integers should be done with those functions.

The directory

Principle

Like Doom, Yadex accesses lumps through an indirection layer that called the directory. The directory is basically a list of all the lumps that exist in the iwad and/or at least one of the pwads with, for each lump, the information necessary to read it. Each directory entry has 4 fields ; the name of the lump, its offset in the wad, its length, and an indirect pointer to the file descriptor for the wad that contains it.

As you might expect, if the same lump is present in more than one wad, it has only one directory entry, pointing to the last occurence.

When you need to load a lump by name, you call FindMasterDir(). It returns you a pointer on its entry in the directory, which in turn contains everything you need to read it.

Managing the directory

The directory is kept in memory at all times. It is only modified when you initially load the iwad and when you load or unload a pwad. And of course when you delete the directory. Simple, isn't it ?

Ha-ha, gotcha, this is not how the actual API works. The actual set of operations is somewhat different in that it does not include unloading a pwad. Instead, there is a function to close all unused pwads, that is pwads that are not referenced by a single entry in the directory. I suppose that the reason why it was done this way is that unloading a pwad is a bit more complicated, since it involves finding the previous "provider" of all lumps that the wad to unload used to "provide". I guess the only way to do that would be rebuilding the directory from scratch. This "feature" is discussed in the Comments section below. Anyway, here is the API.

OpenMainWad()
Create the directory and fill it with the contents of the directory of the iwad.
OpenPatchWad()
Add a pwad to the list of the open wads and to the directory.
CloseUnusedWadFiles()
Close all wads that are not used in the directory anymore.
CloseWadFiles()
Close all open wads and delete the list of open wads and the directory.

Implementation

It has not been said explicitly so far but Yadex maintains a list of all open wads (iwads and pwads). It is a linked list of the WadFileInfo structure. The global variable WadeFileList is a pointer to the first element of the list, which is always the iwad. The type WadPtr is an alias for "struct WadFileInfo *" (yes, I know, it's confusing to have two names for the same thing).
struct WadFileInfo
   {
   WadPtr next;                 // Next file in linked list
   char *filename;              // Name of the wad file
   FILE *fd;                    // C file stream information
   char type[4];                // Type of wad file ("IWAD" or "PWAD")
   i32  dirsize;                // Directory size of wad
   i32  dirstart;               // Offset to start of directory
   DirPtr directory;            // Array of directory information
   };
typedef struct WadFileInfo *WadPtr;
extern WadPtr WadFileList;      // List of wad files

The directory itself is a linked list of the MasterDirectory structure. The global variable MasterDir points to the first element of the directory. The type MDirPtr is an alias for "struct MasterDirectory *".

struct Directory
   {
   i32  start;                  // Offset to start of data
   i32  size;                   // Byte size of data
   char name[WAD_NAME];         // Name of data block
   };
struct MasterDirectory
   {
   MDirPtr next;                // Next in list
   WadPtr wadfile;              // File of origin
   struct Directory dir;        // Directory data (name, offset, size)
   };
typedef struct MasterDirectory *MDirPtr;
extern MDirPtr MasterDir;       // The master directory

Sample code

To be written : searching for a directory entry. The same thing, in an incremental fashion.

Comments

The decision to use a directory is arguable. It is convenient for the programmer when he/she is looking for the effective instance of a lump, which is the case most of the time. But it also prevents the user from editing a resource (notably, a level), if it has been overridden in another wad. It's not a big deal but I don't like it.

The fact that the directory managing operations don't include removing a pwad from the directory means that there is no way for the user to "unload" a pwad. The "read" command has no inverse. The only way to do it is to restart Yadex.

Another somewhat non obvious design decision is that, in most places where the directory is updated, CloseUnusedWadFiles() is called too. This means that you can't load two pwads that have exactly the same lumps. As the second pwad is loaded, the first one is automatically (and silently) unloaded. Not a big deal either but, as a user, I don't like the programs I use to behave like that. I'll illustrate my point with the following scenario, which assumes the "unload pwad" function exists :

  1. Load pwad A (MAP01)
  2. Load pwad B (MAP01)
  3. Unload pwad B
  4. Edit MAP01
At this point, as you have backtracked on your action of loading B, you would expect to see the MAP01 from A, wouldn't you ? Instead you get the MAP01 from the iwad, because A was unloaded as you loaded B. From a user point of view, such a behaviour is confusing and therefore to be avoided.

I don't like the directory management API very much because it's unexpectedly asymmetric and therefore neither intuitive nor orthogonal. The way Yadex plays games with the directory is really disgusting and confusing to me.

I should look into replacing the iwad "on the fly", so that the user is able to change the game parameter dynamically, without restarting Yadex. In fact, the ultimate goal is to remove the game parameter completely or, more precisely, to make it local to a Level object, automatically adjusting and dynamically modifiable by the user.

The wad data

TBD

The level data

Structure

The data for a level is stored in 10 variables that are declared in levels.h and defined in levels.cc. Here they are :
int   NumThings;         /* number of things */
TPtr  Things;            /* things data */
int   NumLineDefs;       /* number of linedefs */
LDPtr LineDefs;          /* linedefs data */
int   NumSideDefs;       /* number of sidedefs */
SDPtr SideDefs;          /* sidedefs data */
int   NumVertices;       /* number of vertices */
VPtr  Vertices;          /* vertices data */
int   NumSectors;        /* number of sectors */
SPtr  Sectors;           /* sectors data */

Scope and lifetime

Since those variables (and other critical ones) are unfortunately static, it's not possible to open editing windows on several different levels simultaneously. This should be fixed in the future by making the level data a class and turning those variables into members of that class.

I think that the level data class should be separate from the editing window class because it might be useful to open several editing windows on the same level. Separate class should also make the design of the read level and write level routines cleaner and simpler.

Maintenance

It's of paramount importance for the stability and reliability of Yadex that the level data be maintained in a consistent state at all times. In particular,

Loading

The SEGS, SSECTORS, NODES, BLOCKMAP and REJECT lumps are ignored. The other lumps are read into the level data variables with a special case for VERTEXES ; vertices that are not used by any linedef are ignored (such vertices are believed to come from the nodes builder and therefore be irrelevant to level editing). The linedefs vertices references are updated if necessary.

Since the endianness of the wad files is fixed (little endian) and thus not necessarily identical to the endianness of the CPU, reading 2- and 4-byte integers from the file is done through special endianness-independant routines.

Saving

If $c MadeMapChanges is false, the SEGS, SSECTORS, NODES, BLOCKMAP, REJECT and VERTEXES lumps are copied from the original file. Else, they are output with a length of zero bytes, except the VERTEXES lump that is created from the the level data ($c NumVertices and $c Vertices).

Since the endianness of the wad files is fixed (little endian) and thus not necessarily identical to the endianness of the CPU, writing 2- and 4-byte integers to the file is done through special endianness-independant routines.

Editing windows, or the lack of it

Too many global variables...

See "_edit.h".

The editor loop

All the time the user spends editing a level is spent within a certain function, the editor loop, a.k.a. $c EditorLoop() in $c editloop.c. It's essential for you to understand it if you want to get how Yadex works right.

The $c EditorLoop() is an endless loop (okay, not really endless) which, for each iteration, first, refreshes the display, second, waits for an event, third, processes that event. I could have put things in a different order but I liked the idea of displaying something before waiting for user input.

Because the event input and the graphical output are complet argsuments is aimed at people who want to hack Yadex. It is very incomplete, partly due to lack of time, partly because as some subsystems are going to be rewritten, I'd rather not spend too much time documenting them. But if you're interested in a particular area of Yadex's bowels that does not appear here, don't hesitate to let me know.

I apologize for the poor quality of Yadex's code but it seemed to me it was better to release something imperfect now than something clean two years from now. If you want to improve it, be my guest.

Introduction

Yadex is written in a mixture of C and C++. The Unix version interfaces with X through Xlib directly ; it uses no toolkit. The DOS version uses BGI (Borland Graphics Interface), a rather low-level API to set the video mode, draw lines, text, etc.

Original platform

The Unix version has been developped with GCC 2.7.2, EGCS 1.0.3, EGCS 1.1.1, libc5, glibc2 and XFree 3.3 on a PC K6/200 with Linux 2.0.29, 2.0.30 and 2.0.34.

The DOS version has been developped with Borland C++ 4.0 on a PC 486 DX4/75 with MS-DOS 6.22.

Yadex should be compilable on all reasonable Unix-and-X11 platforms provided that

To compile on platforms where $c short or $c long don't have the needed size, just change the definitions of $c u16, $c i16, $c u32 and $c i32 in $c yadex.h.

Historic background

Yadex descends from DEU 5.21.

DEU 5.21 was written by Raphaël Quinet, Brendon Wyber and others and released on 1994-05-21. As you probably already know, DEU was a real-mode DOS program, in C, compiled with Borland C++ 4.0 (I think) and using BGI for its output.

In the mists of time (that is probably 1996), I began to hack DEU for my own use. In 1997, other people began to use my hack and I gave it a name : "Yade" (which meant Yet Another Doom Editor). It was still a real-mode DOS program.

In june 1998, tired of rebooting to DOS every time I wanted to do some Doom level editing, I started porting Yade to Linux. As there already was a Unix program called "Yade" (Yet Another Diagram Editor), I changed the name of my baby to "Yadex". At the same time, I began to use C++ in places so that's why Yadex is such an ugly mixture of languages.

Development cycle

Compiling, installing, testing

OK. So I want to hack Yadex ; what do I do now ? The obvious development cycle is

  1. modify the files in src/,
  2. type make,
  3. type su -c 'make install',
  4. run Yadex and test.

However, there are a few things to know that can make you more efficient.

To compile with debugging information, you don't have to modifiy the makefile ; just use the appropriate target (dyadex). The resulting executable is put in dobj/0/ instead of obj/0/.

You don't have to install to test : you can test in place with the special targets test and dtest (test is for the regular executable, dtest is for the debugging executable and is my favourite target by far). When testing through make, you can't pass arguments to Yadex directly : you have to pass them through the A macro. For example :

  $ make dtest A='-g heretic foo.wad'

You can also run your hack through the debugger with the dg (GDB) and dd (DDD) targets. With those targets, you can't use the A= convention : you have to type "set args blah blah blah" at the debugger prompt.

  $ make dg
  (gdb) set args -g heretic foo.wad
  (gdb) run

Even if you want to install your hack, you may want to keep the original Yadex around, for reference or for a good laugh. To do that, edit the VERSION file before compiling your hack. If your hack bears a different version number, it will not overwrite the original version. You don't have to change the version number much : you can just change "3.8.0" into "3.8.0a" or "3.8.0.0" for example.

The programming environment

Memory allocation

You're not supposed to use $c malloc() and $c free() but $c GetMemory(), $c FreeMemory(), $c GetFarMemory() and $c FreeFarMemory() instead. Why ?

$c GetMemory() and friends manage more things for you. They include an anti-fragmentation system, they try to swap things out when memory is tight (this is an only an issue for the 16-bit DOS version) and if they fail, they call $c fatal_error() so you don't need to check their return value.

The reason for $c GetFarMemory() is that, for the 16-bit DOS version, it can allocate more than 64 kB ($c GetMemory() cannot). I must say that I don't use $c GetFarMemory() a lot myself because I don't like the idea of having to use two different memory allocation routines depending on the size I expect to allocate. I modified $c GetMemory() so that it accepts an unsigned long but checks that the passed value fits in $c size_t. In other words, if you call $c GetMemory()