Getting Started with GZDoom Modding

If you are reading this on the github repo, try this link for some HTML styling. Then use the floating element labeled “Table of Contents” to the top-right of this page to navigate between sections by clicking on them. I’ve removed the scrollbar, so that is the only way. Click on the “Switch Color Mode” text at the top-left of the page to switch between light and dark modes. Zoom-in via browser for bigger font size.

This guide will introduce the basics of modding for the GZDoom source port. It is aimed at beginners with little-to-no technical knowledge. It will seem tedious to people with some proficiency with coding or GUI experience. For more succinct or advanced help, check the Tutorials page and the How-to-Start-Modding page on the zdoom wiki, or Jekyll Grim’s ZScript basics. The guide will focus on GZDoom, but might have limited applicability to other source ports. It is structured as a sequence of exercises that the reader is expected to follow along by actually implementing the instructions on their computer. It is meant to be done at a relaxed pace over several sessions spanning days, and not in a single sitting. It presents particular ways of doing things in particular chapters for pedagogical reasons and to inculcate good habits, like how one only practices scales for the first few weeks when they start learning a new musical instrument (see also: WAX ON, WAX OFF). It will not present the full context or historical information for everything, because it is meant to be a first-pass curriculum (see table of contents, top-right), where too much extraneous information can be overwhelming and counter-productive. The reader won’t be provided with too many choices for ways to do something at first to avoid decision paralysis. A stable foundation will first be built so that the reader will have somewhere to retreat to when they are lost.

You can recommend additions/changes to this guide by submitting PRs to the github repo. Please only make changes to the index.org file. Do not edit the html file directly. And do not expand the scope too much.

Running GZDoom and understanding WADs

Getting the Engine

First, let us get GZDoom running and playing a game. Download the latest version of GZDoom (and not ZDoom) for your operating system from here: https://zdoom.org/downloads

If you are running on Linux, you might be better off compiling it from source. That is not a ding. I do this myself.

After downloading and unpacking, you should end up with a folder somewhere on your computer with files named:

  • gzdoom.exe (or just gzdoom)
  • gzdoom.pk3
  • lights.pk3
  • brightmaps.pk3
  • game_support.pk3
  • game_widescreen_gfx.pk3
  • fm_banks (folder)
  • soundfonts (folder)

Depending on the OS you downloaded for, you might have some extra files like *.dll files. If you try to run the executable file (gzdoom.exe) by either double-clicking on it, or by running the command from a terminal/command-prompt from that folder location, it should throw up a message saying that it couldn’t find any IWADs.

To run as a command, open a terminal (Linux) or command prompt (Windows), and navigate to the directory location using the change-directory (cd) command. When there, list the files using the ls (Linux) or dir (Windows) command. It should print out all the files from the above list. Then from there, just type ./gzdoom (Linux) or gzdoom.exe (Windows). This has the same effect as double-clicking the file from your graphical file browser. Keep the terminal/command-prompt open (minimize it for now). We won’t be using the command method everytime but it helps with understanding what is happening under the hood.

Getting some IWADs

Running the engine executable by itself couldn’t run a game because you haven’t downloaded any actual games for the engine to run. Let us fix that. For this guide, we will be using Freedoom, but you could follow the same steps with Doom 1 and Doom 2 if you have purchased those wad files from you favorite legal videogame store.

Acquire the files freedoom1.wad and freedoom2.wad from https://freedoom.github.io/download.html (click on the Freedoom: Phase 1+2 hyperlink). Or better yet, get version 0.13.0 specifically from here, since that is the one this guide will be using. Extract these two *.wad files from the zip archive and place them in the same directory as your GZDoom executable. If you are having trouble opening a zip archive file, double-click or right-click on it and “extract” the contents. On the Linux terminal, a tool like unzip can also be used. With the files placed at the same location as your GZDoom executable, running GZDoom again should cause the program to auto-detect freedoom1.wad and freedoom2.wad and list them for selection on startup.

Let’s run Freedoom 2 from the command line. From the terminal/command-prompt, run the following command:

  • Windows
gzdoom.exe -iwad freedoom2.wad
  • Linux
./gzdoom -iwad freedoom2.wad

The game should launch, and you should be greeted with these starting screens of the main menu, and the first level:

screenshot01.jpg
Figure 1: Main menu screen for Freedoom 2 (v0.13.0)
screenshot02.jpg
Figure 2: Starting area of MAP01 in Freedoom 2 (v0.13.0)

These screenshots are from running version 0.13.0 of freedoom2.wad. Another version might look slightly different. And while we are here, go back into the main menu, go to Options, then enable Full Options Menu. In here, go to Display Options -> Texture Options -> Texture Filter mode and set it to “None”. Thank me later.

The files freedoom1.wad and freedoom2.wad are called IWADs, which is short for internal WADs. These are wad-files that have all the information within them for the engine to run a whole game. Later on, we will take a look into what is actually inside them by using another program.

Basic replacement mods

Texture replacement

Now we are going to make our first mod. Create a project folder somewhere on your computer. Call it something like “my_first_mod”. Inside of it, create a folder named “textures”. The name of this folder is important. It has to be spelt exactly and cannot be arbitrary. Now, right click the following image file and save it inside the “textures” folder. Make sure that it is named AQRUST08.png. The filename is important.

AQRUST08.png
Figure 3: AQRUST08.png modified wall texture

Now, zip the textures folder into a zip-archive file called something like my_first_mod.zip. The filename of this zip archive is not important and can be anything of your choosing. You can right-click the folder and compress it into a zip file. On Linux, you can run this command from the location of your project folder:

zip -r my_first_mod.zip textures/

This uses the zip program with the -r recursive flag to make sure that contents of subfolders end up inside the zip file. Congratulations. As far is the current version of GZDoom (4.14.0 as of this writing) is concerned, you just made your first mod. Now let’s run it. You can do this either by dragging and dropping the zip file onto your gzdoom.exe executable file, and selecting the freedoom2.wad IWAD when it asks, or by running the command:

./gzdoom -iwad freedoom2.wad -file <PATH-TO-PROJECT-FOLDER>/my_first_mod.zip

Once you launch the game, you should be greeted to this view:

screenshot03.jpg
Figure 4: Opening area of MAP01 in Freedoom 2 (v0.13.0) with a texture replaced

Analysis

Let us unpack what is happening here. You ran GZDoom with two files: freedoom2.wad and my_first_mod.zip. The first one is the IWAD, meaning the file that contains the base game. The second one is what is called a PWAD (short for patch WAD). The IWAD contained a texture lump (will explain later) named AQRUST08, and a map lump (among others) named MAP01, and instructions inside the map lump for the engine to paint that texture on a specific bunch of walls (with some offsets, lighting effects, etc.). This is what happens when you run GZDoom with just the IWAD freedoom2.wad by itself. But when you run the IWAD with this PWAD, which contains its own copy of the AQRUST08 texture (we’ll get into how I knew that later), the engine replaces all instances of that texture with the one from your PWAD. This is also why it was important to name the file exactly right. If you load two PWADs that both replace the same texture like so:

./gzdoom -iwad freedoom2.wad -file mod1.pk3 -file mod2.pk3

then the replacement texture from the last PWAD (mod2.pk3) will be used. Think of it as if the replacement instructions are executed serially, in the sequence of the files specified. For the rest of this guide, we will be naming the zip-archive file my_first_mod.pk3, but always remember that under the hood it is merely a zip file.

Get setup with a Launcher before proceeding further

This is one reason I illustrated the command line method of launching GZDoom. Selecting multiple mod files and dragging-and-dropping them onto the GZDoom executable does not give you control over the mod load order. People routinely play games on GZDoom with 5-15 mods loaded at once, often in a specific order. Don’t worry. No one is actually typing out long commands. They are using launcher programs like ZDL or DoomRunner. I myself am partial to DoomRunner. Pick one and stick to it.

Most launcher programs allow saving of presets for various mod and order combinations. On first launch, they typically ask you to select engine executables, a list of IWADs, and the usual location for various kinds of PWADs like map packs. So you don’t have to put files in your GZDoom folder, or any other specific location. You should feel free to organize your files on your computer any way you see fit. Here is a view of my DoomRunner front page:

doomrunner01.jpg

As you can see, I have highlighted a preset that I have named “Elementalism” that is using the GZDoom executable, the doom2.wad IWAD file, and a whole bunch of PWADs in a particular order. The lights.pk3 file from the GZDoom install is for dynamic lights (it doesn’t get loaded by default for performance reasons). Elementalism is an ambitious map pack, and Hellrider Vengeful is a weapons and player-movement mod. Here, I am adding a mod called Flashlight++ even though Hellrider already comes with a flashlight, because the maps in Elementalism have all been programmed to strip the player of all inventory items and pistol-start every level. And the flashlight in Flashlight++ happens to be unclearable using that method. If I were to try and and another mod that modifies weapons, like Beautiful Doom to this list, then the conflict with Hellrider will cause all weapons to be replaced by one mod and ammunition pickups be of the other. So not all mods are designed to go together. I mostly ignore DoomRunner’s separate map pack subwindow and load map-pack mods as regular mods, with full control over load order.

For the rest of this guide, I recommend picking a launcher program and launching GZDoom with my_first_mod.pk3 and freedoom2.wad just to develop good habits. Under the hood, all these Lauchers are just constructing and executing lengthy commands like the ones above.

doomrunner02.jpg

Remember that my_first_mod.pk3 is really just a zip file. The file extension doesn’t matter, and only exists to help you. Modern GZDoom PWADs are named *.pk3 and IWADs are named *.ipk3 (we’ll get there). You might recall that the engine’s internal files that came with the GZDoom download (lights.pk3, brightmaps.pk3, game_support.pk3, etc.) are also *.pk3 files. These are the only ones that should not be moved out of the GZDoom executable’s folder.

Older mods, and mods made to be interoperable with source ports other than GZDoom aren’t zip files, but are instead of the WAD format. While GZDoom can read WAD files (the IWAD freedoom2.wad is a WAD file, after all), the best practice is to make mods as *.pk3 files (which are secretly zip files). Only levels/maps need to be in the old WAD format. More on that later.

Text files as lumps

Let us continue adding to your mod. So far, you have a zip file (now named “my_first_mod.pk3”) which contains a folder named textures, which in-turn contains a png image file named AQRUST08.png. It was important for this file to be a png file. And it is very important that both the folder and file names are what they are for the mod to work. The filename had to be AQRUST08 because that tells the engine what texture it is meant to replace. The folder name textures is a reserved name. The engine (and most map editors) interpretes it as a location for texture files. You can put any png images in them, and even organize them into subfolders within. But you cannot put other arbitrary data in it and expect it to work. Nor can you just put the AQRUST08.png file alone in a zip archive and expect the replacement to work.

There are other reserved names, as far as files and folders in the root (top) location of the zip archive is concerned. These reserved names can have any capitalization. They can be camelcase, all upper case, or lower case. It is all the same to the engine. Reserved names for folders include “textures”, “flats”, “sprites”, “maps”, etc. A full list is on the wiki. You can have other folders and subfolders, but these and their contents are treated by the engine in a special way. Reserved names for files include TEXTURES, ZSCRIPT, MAPINFO, GLDEFS, etc. Again, the capitalization doesn’t matter, and you can give them any file extensions you want (*.lmp, *.txt, *.zsc). You can store other files in the root location of your mod (like a license file, a readme, or a credits file if you end up using other people’s work), but they cannot use these reserved names.

Files in *.pk3 archives may be referred to as lumps as a holdover from the wad-format days. The wad-format is also a sort of archive format like zip. Contiguous sequence of bits inside a wad-file (often marked with a start and end markers) represent specific types of data. We can explore this later, but we don’t have to worry about that as long as we have subfolders and reserved names. These special lumps are often just text files that have their own format for presenting data to the engine. Let us try and use one to get a better feel.

For this next exercise, we will replace the texture on the door visible from the starting area in Freedoom 2. The lump name for this texture needs to be BIGDOOR1 (again, I’ll get to how I know this later). But we will not just be using a file named BIGDOOR1.png in the textures folder, even though that will work. We will instead use some other file name, and try to use the TEXTURES lump to make it work.

Pick any png image file you want. I’m going with John Romero’s forehead. Crop/scale the image using some image manipulation program like GIMP, Photoshop, or even MSPaint, to fit into 128 \(\times\) 96 pixels to match the door’s texture size. I named my file John_Romero.png, and I placed it inside a subfolder inside the textures folder called custom. Then, open a text file called TEXTURES.lmp in the project root location. Put the following lines into it (replace the path and file name in the Patch line to match your new image):

Texture BIGDOOR1, 128, 96
{
   Patch "textures/custom/John_Romero.png", 0, 0
}

The commas and the curly-brackets { & } are important and not just for show. Now re-compress the zip archive with these two new files and the new subfolder.

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp

The archive’s internal structure should look something like this:

filestructure01.jpg

Now, running the PWAD with the freedoom2.wad IWAD (through a launcher like DoomRunner) should bring up this view:

screenshot06.jpg

What the TEXTURES.lmp file (or the TEXTURES lump) did was create a new, virtual texture container with the name BIGDOOR1 with the image John_Romer.png patched in, and presented that to the engine. You can create new virtual textures this way by combining and mashing multiple other textures together (yes, even other virtual ones). You can scale, rotate, skew, mirror/flip, and mask, as well as do other kinds of transformations without actually creating new image files to be stored in the PWAD. The TEXTURES lump is a great way to put a decorative poster or graffiti onto an existing wall texture. An example we will go through later will involve slapping an interactable switch onto a wall texture for use in a custom map.

These lump names, you will notice, have both been eight characters long (AQRUST08, BIGDOOR1). This is another hold-over from the DOS days, where file names would be truncated to eight characters. The case doesn’t matter, but you have to stick to this convention while naming lumps. Later on when we get into map making, you will find that certain map formats allow for use of full texture filenames with full paths instead of these short lump names. It is still recommended that you use these lump names instead. Because this makes your maps easily moddable (by you as well as others!) if the lump names are standardized.

Basics of SLADE

Next, we will introduce another helpful program to our modding toolbelt. This one is a WAD-editor called SLADE. You can acquire it for your preferred OS from here or here. SLADE is in principle capable of many things. You can write code, compile code, paint textures, create and modify brightmaps, and even make/edit levels. There are Doom mod authors who develop entirely on SLADE (some of them livestream the process). But in this guide, as a rule, we will only be using SLADE to take a peek into WADs, and possibly extract content. We will not be using SLADE to modify any data within wads. Feel free to learn its intricacies on your own, later.

To not overwhelm ourselves, let us first use SLADE to open the simplest mod we have: “my_first_mod.pk3”. Here is the view you should be presented with:

SLADE01.jpg

It’s all fairly intuitive. There is a panel that shows the files or “lumps” in your “WAD” (I’ve expanded the folders and subfolders), and a bigger panel to the right that shows the content of the currently selected lump. In the image, I have selected the TEXTURES.lmp file/lump to show its textual content. A few things of note here is the SLADE has correctly identified the file-types of our lumps as two PNG Graphic files and a “Texture Definition” lump. It says so next to the file as well as in the bottom bar of the window. Furthermore, it has auto-selected the “ZDoom Textures” option in the “Text Language” pulldown menu above the textual-content panel for syntax highlighting. All of these areas of the SLADE window are important to us. You can select the AQRUST08.png file and watch the image displayed in the content panel, along with the image size in the bottom bar.

Now let’s open a bigger “WAD” file: freedoom2.wad. Be sure to create a backup of this file before opening it in SLADE, just to avoid accidentally causing a change in it. That would violate our rule regarding SLADE for this guide.

Since this is a wad-format file, you should be confronted with a lengthy, flat list of lumps with no hierarchical subfolder structure. The lumps are by default, ordered in the way they are stored in the wad (the ordering is important in the wad format). But you can click on the “Name” tab at the top of the lumps panel to display the lumps in ascending order of their name strings. If you scroll down to Freedoom 2’s version of the AQRUST08 graphic (the one your mod replaced), you will see that the “Type” field says “Graphic (Doom)” instead of “Graphic (PNG)”. The image is stored in a Paletted raw format. If the image appears in black-and-white in the contents panel, you can instruct SLADE to use Doom’s color palette in the drop-down menu to the top-right. The same goes for exporting graphics. You can’t just right-click on the AQRUST08 lump and click export, as it will result in a binary lump file. You would have to navigate to the sub-popup menu under “Graphic” after you right-click, and select the “Export as PNG” option. If you’d like to practice further, try exporting the SLIME14 graphic as a PNG file and modify it, then include it in my_first_mod.pk3. This should apply to the floor in the opening area of Freedoom 2.

Another curiosity that should be of interest is that there is no lump named BIGDOOR1 (at least as of Freedoom 2 version 0.13.0). There is one called DOOR2_1 that looks suspiciously like the door from the opening level, but it is too small (96 \(\times\) 96 pixels, see the bottom bar). You can test that it isn’t the right one by attempting a DOOR2_1 lump replacement file in your mod. If will replace all instances of DOOR2_1 usage in the levels of Freedoom 2, but all BIGDOOR1 instances (including the one in the opening area) will remain unaffected.

Freedoom 2 is actually defining the BIGDOOR1 lump inside its TEXTURE1 lump. You can scroll to it and highlight it with a click. This is stored in the older wad-centric format here and not as a text file (unlike in my_first_mod.pk3). Which is why the “Type” field says “TEXTUREx”. But SLADE lets you edit it if you click on the “Edit Textures” button that should have appeared in the content panel. Clicking on it should open this lump in its own tab and present you with a list of virtual textures defined within.

SLADE02.jpg

Scrolling this list and highlighting BIGDOOR1 should reveal to you (in a “Patches” panel to the right) the five patches used to make this lump. There’s four copies of the W13_1 patch with the corresponding offsets forming a background canvas of size 128 \(\times\) 96 pixels, and one instance of DOOR2_1 patch slapped on top at the center. You can look at the W13_1 graphic lump by tabbing back to the freedoom2.wad tab. If you’ve been following this guide in detail, you get exactly what is going on here.

Sprite replacement and offsets

So far, we have made basic replacements for textures. Next, let us replace some sprites. You would think that this is more of the same, but there is a subtletly that lets us learn a couple of new concepts. Get back into the game Freedoom 2, and turn left in the starting area. You should see two health-pickup items called “StimPacks”. I have circled them in green in the following screenshot image:

screenshot04.jpg

These are a pickup item that can restore up to 10 points of health. They can be picked up by walking over them. But your health (indicated in the HUD below) needs to be less than 100 points for a successful pickup. You can lose some health points by going forward in the corridor and deliberately getting shot at by the two enemy zombies in the cubby-room to the left of the door. Returning to the StimPacks and walking over them should restore up to 10 points. Just walking around the StimPacks, you will notice that they are depicted on screen by a single sprite/image that always faces your viewpoint (meaning you can never see “behind” the sprite by walking around it).

The sprite used by this object (the technical term is actor) is a lump called STIMA0. You can find it inside freedoom2.wad via SLADE. You would have to right-click and use the “Graphics” sub-menu popup and “Export as PNG” if you want it as an image. To replace the STIMA0 sprite with one of your own in my_first_mid.pk3, you will have to create one of the reserved-name folders called sprites and place your image with the name STIMA0.png inside it, and then make sure to zip it with the others. If you are having trouble finding/deciding on a replacement sprite, you can try this red-colored version. Just right-click and save the png image inside the sprites folder of your mod.

STIMA0.png
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/

If you are using some other image, you can try and match the original lump’s size, which was 19 \(\times\) 10 pixels (check this in the bottom bar inside the contents panel in SLADE). But you don’t have to match it. So now the file structure inside of my_first_mod.pk3 should look like this:

filestructure02.jpg

If you start the game now, and turn left at the starting area, you will be surprised to find … nothing! But the two StimPack actors are actually there. You can confirm this by losing some health (get shot at) and running back here to “pick” them up. However, you can’t see the new sprites. In truth, they are below the floor. By default, sprites in GZDoom are offset relative to the top-left corner of the image file. You can add the correct offsets to PNG files using SLADE (they get stored in the PNG format’s grAb chunk) but we won’t do that. For one, it violates our rule about not using SLADE to modify things. And secondly, it would have to be redone everytime you replace or modify the image file using some other third-party program. It might not seem like much for a single sprite, but once you have hundreds …

Let’s instead use our already existing TEXTURES.lmp file. Add the following lines to it (i.e. append below the existing lines), and then re-zip the archive:

Sprite STIMA0, 19, 10
{
    Patch "sprites/STIMA0.png", 0, 0
    Offset 10, 10
}

Note the new Offset field here. Change the numbers (as well as the overall size numbers) according to your choice of image. There is also a Scale field that you could use if your image is much larger than 19 \(\times\) 10 pixels. The TEXTURES lump uses inverse scale (2 = 50% of the original size, 0.5 = 200% of the original size, etc.). With the offset specified in the TEXTURES.lmp file, now you can modify the PNG file as much as you want without worrying about preserving or resetting the offsets within that file. The sprite replacement should work in the game now.

ZScript class replacements

Now we will learn the very basics of a scripting language called ZScript. GZDoom understands multiple scripting languages for modding. But most of them are deprecated and are only supported for backwards compatibility with older mods. For modern GZDoom, there are only two scripting languages you need to learn: ACS and ZScript. This guide won’t be going into ACS, but know that that is mostly only used for map specials (conditional triggers and scripted sequences). For everything else, stick to ZScript. There are more advanced, and comprehensive guides to learning ZScript: Ash’s ZScript basics and David Newton’s Youtube Tutorials. In this guide, we will show the use of ZScript to modify two things present in this animated view from Freedoom 2, visible after you turn left at the big door in the starting area:

animated01.gif
Figure 5: We will be modifying the green HealthBonus items, and the Zombieman enemy

New pickup item

First, let’s replace those green, flashing bottles from the image above. That is a HealthBonus. Unlike the StimPacks from before, these give the player 1 point of health all the way up to a maximum of 200 points. Meaning that you can pick them up even if you have a 100 health points. Try doing this. There are two more HealthBonus items to the left of the view which can be picked up without alerting the two Zombieman enemies, as long as you don’t fire your weapon or bump into them (or get into their light of sight).

Create a text file named ZSCRIPT.zsc in your project folder (the extension doesn’t matter. ZSCRIPT is one of those reserved lump names). In it, put the following text:

version "4.14"

class MFM_Elixir : HealthBonus replaces HealthBonus
{
  Default
  {
    Inventory.Amount 5;
    RenderStyle "Shaded";
    StencilColor "Red";
  }

  States
  {
  Spawn:
    BON1 ABCDCB 3;
    Loop;
  }
}

The syntax here is different from what we’ve seen so far in non-zscript files like the TEXTURES lump. The semicolons “;” at the ends of some of the lines are very important. If you know some C or C-like programming language, this should be familiar. The colon (“:”) after the word “Spawn” is deliberate and not a typo. Now re-zip the archive with the new file:

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc

and launch the game with your mod (I hope you have habituated to using a launcher by now). Go close to the big door and turn left to the same location as before. All of the green HealthBonus items should now be replaced by translucent, red bottles that are flashing at twice the rate. And if you pick one of them up, they should give you 5 health points instead of 1.

Analysis

Let us go through the contents of the ZSCRIPT.zsc file to see how this is achieved. The first line reads version "4.14". This is a necessary clause at the beginning of the ZSCRIPT lump, and signals a minimum version that your mod can now be run in. If you try and load my_first_mod.pk3 in GZDoom 4.13.2 with the ZSCRIPT.zsc file in, it should throw an error. This facility exists to ensure that mods that use new, advanced ZScript features don’t get accidentally launched by older versions of the engine, which would result in a crash.

Looking further down, even if you have no experience with programming, you can notice the pairs of curly brackets encapsulating content, which has been formatted with TAB-indentations for clarity. There is a master-pair of curly brackets “open” after the line that starts with the term “class”, and encapsulate everything else. This “everything else” is in the “class” “block”, or belongs to the “class”. Within the class, there are two other blocks: the Default block, and the States block. All of these are reserved keywords that the engine assigns special meaning to.

Classes are a very common structure within programming languages, and their complete definition is beyond the scope of this guide. But in this limited context, a class is a … well … class of entity that can exist in the game’s simulation. A class can be of various types: inventory item, monster, the player’s character (called a PlayerPawn), a flying rocket, a falling rain drop, or even an invisible “thinker”. Every entity within the game is an instantiation of some class. The engine “ticks” about 35 times every second, and during each “tick” it runs through the list of entities on the map and runs some standard “Tick()” functions belonging to their class definitions. There is a little more to that, but this is basically how the game runs. Physics collisions, actors interacting with each other and the map, etc., all happen in “ticks”, and there’s 35 of them in about a second.

If the last paragraph was hard to understand, that is okay. The most important thing about classes is that you can inherit properties from a parent class and then modify them. For the purposes of modding, the syntax for declaring a class object is:

class <NEW-CLASS-NAME> : <PARENT-CLASS-NAME> [ replaces <SOME-OTHER-CLASS-NAME> ]

The part between the square brackets [ & ] is optional (the square brackets themselves shouldn’t be typed). A new class doesn’t have to replace an existing class. The new definition can exist and operate independently. To use our own example, the line reads:

class MFM_Elixir : HealthBonus replaces HealthBonus

Here we have defined a new class with the name MFM_Elixir. The prefix MFM_ stands for “My first Mod” (the name of your mod). It is good practice to add a unique prefix to all new class definitions. That way, there will be no conflict when your mod is loaded along with another mod that might happen to have its own elixir class, since that is a common word. Anyway, this new class is of the type HealthBonus which plays the role of the parent class that all properties can be inherited from. This HealthBonus class is an existing class that is defined inside the engine itself. You can take a look at its definition in GZDoom’s source code here. The class HealthBonus has the class Health as its parent class (further down in the same file, you can see the definition for the StimPack class, which also has Health as a parent class). You can find the same class definition inside the gzdoom.pk3 file that came with the engine download. Find it using SLADE (zscript->actors->doom->doomhealth.zs), but remember the rule: do not modify the file. GZDoom automatically loads gzdoom.pk3 when you run it (which is why it should always be present the same folder as the executable).

Looking at the first few lines of class HealthBonus, you can see its Default and States blocks. We inherit these properties, and change some values within the Default block of our MFM_Elixir class definition. Namely, we changed something called Inventory.Amount from 1 to 5 (to make it give you 5 health points on pickup), and changed some other internal rendering properties like RenderStyle and StencilColor which affects how its sprites are rendered. And lastly, the States block is meant to contain the actor’s state labels, which contain a state sequence. And each state in the sequence is specified by the sprite to display, the time (in “ticks”) to remain in this state for, and any other functions that need to be run. In our example, MFM_Elixir class’s States block has a single state label (Spawn) just like its parent HealthBonus class. This marks the state sequence that all actors first enter when they are spawned on the map. The sequence line reads:

BON1 ABCDCB 3;

This tells the engine to display the sprites BON1A0, BON1B0, BON1C0, BON1D0, BON1C0, and BON1B0 again in that order, each for 3 ticks. The Loop; statement in the next line causes this sequence to repeat indefinitely. You can find these sprites inside freedoom2.wad using SLADE. The original HealthBonus class displayed each of these sprites for 6 ticks, which is why it was flashing at half the speed of MFM_Elixir.

Lastly, the replace HealthBonus clause instructed the engine to replace all instances of the HealthBonus item on any loaded map with MFM_Elixir. The replaced class doesn’t have to be the same as the parent class. You could, if you wanted to, replace all instances of the Zombieman class with MFM_Elixir. You can test the replacement effect by using console commands. Open the console while playing Freedoom 2 (by hitting the tilde “~” key below the Escape-key on your keyboard). This should pause the game and give you a command prompt. In it, type “summon MFM_Elixir” (without quotes) and hit ENTER. Now close the console by hitting the “~” key again. Provided that there is enough space in front of your player character, an instance of MFM_Elixir will have been spawned in front of you and will have fallen to the ground. If you repeat the exercise and try to summon HealthBonus instead, the replacement clause will cause another instance of MFM_Elixir to fall at your feet.

Let us remove the replacement clause by either deleting the words replace HealthBonus or by commenting them out by prefixing a double-slash like so:

class MFM_Elixir : HealthBonus // replaces HealthBonus

All text in a line that follows a double-slash “//” is treated as non-existent by the engine. So this is a great way to leave comments or notes-to-self all over your ZScript code to help yourself (and others) understand the code better. After doing this, you should be able to summon HealthBonus and summon MFM_Elixir via the console and watch both items flashing next to each other like so:

potions.gif

Let us leave the mod in a state where MFM_Elixir is NOT replacing HealthBonus for now, as we will be using this to learn something new in the Basics of mapping section later.

Homework assignment

The sprites that we have explored so far: STIMA0, and the BON1[ABCD]0, all share a common feature. They all end in a “0”. The sprite names start with a 4-character string, followed by a single-character frame ID, and then a number. The two items that these sprites depict: StimPack and HealthBonus, both look the same from all directions. But we have seen sprited objects/actors in this game that look different from different angles (like the Zombieman). Let us try and understand how this is done. Let us replace the MFM_Elixir sprite sequence with another set of sprites from freedoom2.wad that also have the [ABCD] frames, but do not end with a “0” character. Replace the state-sequence line with:

HEAD ABCDCB 3;

Get back into the game and summon MFM_Elixir in a brightly lit region. Now walk around it and see how the sprite changes based on the angle. To make it clearer to see, comment out the RenderStyle line in the Default block and use a single sprite frame to keep it from animating:

HEAD A 1;

Now actually read the Angles and Mirroring sections from the Sprites wiki page. Look up the names of all the sprite lumps that start with the characters HEADA (there should be five of them). See if the names make sense in terms of in-game viewing angle. Do not proceed further until you have understood how sprite-naming convention can affect sprite rotations. You can get pretty close to smooth rotations if you use all 16-rotation characters, but at that point, especially if you have a lot of animations, you are better of learning to use 3D models.

Before proceeding, I want to confirm that you are not modifying the internal contents of my_first_mod.pk3 archive file using SLADE, but are directly modifying the files outside in your project folder and re-compressing the zip archive. The importance of this habit will become clear later.

Modifying Zombieman

Okay. Now we do enemies. Boot up the game. You see these two former gentlemen admiring the flashing HealthBonus bottles:

screenshot05.jpg
Figure 6: Zombiemen of culture

You can alert them by either bumping into one of them or firing your weapon. Even punching the air with your fist (weapon-slot 1) should do it. Get a feel for their behavior. Watch how they move around, and stop to shoot. With clever positioning, you can even get one of them to accidentally shoot the other and cause some in-fighting among them. After you’ve gained some feel for their behavior, take a gander at their class ZombieMan definition in the engine source code (or inside the gzdoom.pk3 file). It has a master parent-class simply called Actor, which has a parent-class called Thinker, which is doing most of the heavy lifting.

ZScript is powerful enough to allow you to define any general sort of game actor with very unique, custom behavior. But since GZDoom started life as a Doom source port, it supports Doom-style functions and behavior out of the box. ZombieMan uses a very small set of built-in functions: A_Look(), A_Chase(), A_FaceTarget(), A_PosAttack(), A_Pain(), A_Scream(), A_NoBlocking(), and A_XScream(). These functions rely on values of some default fields to perform some actions and kick the actor into one of the standard state labels. The standard state labels defined for the ZombieMan are Spawn, See, Missile, Pain, Death, XDeath, and Raise. These (along with some others used in interactive actors) are built in for the standard functions. You can define a custom damage type named XYZ, for example, that can have its own Pain.XYZ and Death.XYZ states to transition to if they were defined (the default Pain and Death states are used otherwise). XDeath is entered when damage incurred exceeds a certain threshold value. Raise comes in when a dead actor is resurrected using a built-in method.

The ZombieMan has very simple, basic, AI. A_Look() makes it look for hostiles. A_Chase() makes it turn 45-degrees in a random direction (biased towards its target actor) when ever it gets called (or the actor collides with something). The rest should be self-explanatory. You can read the wiki pages that I have hyperlinked for more. Suffice to say that GZDoom default AI functions have no concept of path finding or advanced goals and behaviors. Modders had to code it all themselves. Before the days of ZScript this was done using token inventory items. Although, at the time of this writing, GZDoom got some code updates for a behaviors subsystem. So in the future, we should see that getting used (and someone will need to tutorialize it).

You can define your own state labels, but will need custom functions to handle state transitions into and out of them. Let us now create a replacement class named MFM_InviZombie to make this enemy slightly more exciting. You can add these lines to the same ZSCRIPT.zsc file below the MFM_Elixir class definition. Let is add/modify the state labels one at a time. For starters, we will split the See state label into two:

class MFM_InviZombie : ZombieMan replaces ZombieMan
{
   Default
   {
     StencilColor "DDDDFF";
     Speed 12;
   }
   States
   {
   See:
     POSS A 1 A_StartSound("brain/cube", starttime: 0.6);
     POSS A 4 A_SetRenderstyle(alpha, STYLE_Shaded);
     POSS A 4 A_SetRenderstyle(alpha, STYLE_Fuzzy);
   See2:
     POSS AABBCCDD 4 A_Chase;
     Loop;
   }
}

So here, I gave the class a default StencilColor (a blue-ish white) and a Speed of 12 (the default ZombieMan’s Speed was 8). We omit defining the Spawn state label since we don’t want to change it. We start the See state label by playing the “brain/cube” sound lump using the A_StartSound() function (it’s just the DSBOSCUB lump from freedoom2.wad originally used in Chex’s Quest) with the starting time 60% into the audio. And then the A_SetRenderStyle() function is used and the sprites are set to be rendered in the “shaded” style (just like for MFM_Elixir) with the new StencilColor for 4 ticks. And then the RenderStyle gets changed again to “fuzzy” style, which is internally defined for use with the Spectre class of enemy from the Doom games. After 4 more ticks, MFM_InviZombie should enter our custom See2 state label, which is just the looped state sequence from the default See state label in the original ZombieMan class. In game, the actor will be visible normally in the Spawn state label, but will enter the See (and eventually the See2) state label and look like a dark shade when alerted, like so:

invi01.gif

When in the See2 looped state sequence, the sprites are cycling between the POSS[ABCD] walking-animation frames at the speed defined in the code (look them up using SLADE). Next, let us add a modified Missile state label to the States block of MFM_InviZombie class:

Missile:
  POSS E 1 A_StartSound("misc/spawn");
  POSS E 4 A_SetRenderstyle(alpha, STYLE_Shaded);
  POSS E 4 A_SetRenderstyle(alpha, STYLE_Normal);
  POSS E 12 A_FaceTarget;
  POSS F 8 A_PosAttack;
  POSS E 8;
  Goto See;

If you compare this to the default Missile state label from the original ZombieMan class, you will see that the “misc/spawn” sound lump is played (it’s just the DSITMBK lump from freedoom2.wad originally used in Chex’s Quest), the render style is briefly set to “shaded” for 4 ticks, and then the rendering style is set to “normal”. Also, it now takes 1+4+4+12 meaning 21 ticks to get to the A_PosAttack() call, which is longer than the default 10 ticks. This is to keep the monster difficulty roughly the same as before, giving the player more of a chance to execute a counter attack. Lastly, the actor is returned to the See state label which should play the other sound lump and set the render style back to shaded (as we’ve already seen). This should look like this:

invi02.gif

To finish it up, we can also add new definitions of the Pain, Death, and XDeath state labels to our replacement class to make MFM_InviZombie visible in those states too:

Pain:
  POSS G 1 A_StartSound("misc/spawn");
  POSS G 4 A_SetRenderstyle(alpha, STYLE_Shaded);
  POSS G 4 A_SetRenderstyle(alpha, STYLE_Normal);
  POSS G 3 A_Pain;
  Goto See;
Death:
  POSS H 1 A_StartSound("misc/spawn");
  POSS H 4 A_SetRenderstyle(alpha, STYLE_Shaded);
  POSS H 5 A_SetRenderstyle(alpha, STYLE_Normal);
  POSS I 5 A_Scream;
  POSS J 5 A_NoBlocking;
  POSS K 5;
  POSS L -1 A_DropItem('MFM_Elixir');
  Stop;
XDeath:
  POSS M 1 A_StartSound("misc/spawn");
  POSS M 3 A_SetRenderstyle(alpha, STYLE_Shaded);
  POSS M 1 A_SetRenderstyle(alpha, STYLE_Normal);
  POSS N 5 A_XScream;
  POSS O 5 A_NoBlocking;
  POSS PQRST 5;
  POSS U -1;
  Stop;

You should, once again, compare these to their counterparts in the original ZombieMan class. We won’t be redefining the Raise state label. I have thrown in the use of the A_DropItem() function at the end of the Death state sequence to make the MFM_InviZombie drop a bottle of MFM_Elixir when killed. This item is dropped along with the ZombieMan’s default DropItem (Clip). This should make the difficulty more fair.

invi03.gif
Figure 7: Look closely. That other guy in the back stole my kill!

I have omitted dropping the MFM_Elixir from the XDeath state to avoid rewarding the act of blowing the monster up with some splash-damage weapon that didn’t require as much precision, like shooting an explosive barrel next to the monster (try summoning an ExplosiveBarrel via the console next to an unalerted MFM_InviZombie and shooting it). From a game-logic-rationalization point-of-view, the explosion smashed the bottle with the elixir!

I have not justified some of the decisions made here (speed increase, partial spectre-like visibility, inclusion of sound cues, transitional blue-ish white shading, longer tick durations, conditional drop items) as game design is beyond the scope of this guide. But you can vary these parameters yourself and see what suits your needs best. You can make the monster completely invisible in any state sequence by replacing the sprites with TNT1A0, which is a dummy lump name for a blank sprite. Better yet, the engine already provides a convenient flag that you can set to turn any monster into a Stealth Monster. All you have to do is add the line +STEALTH to your class’s Default block. Here is the minimum code you would need:

class MFM_StealthZombie : ZombieMan
{
  Default
  {
    +STEALTH;
    RenderStyle "Translucent";
    Alpha 0;
  }
}

Feel free to summon MFM_StealthZombie from the console and see how differently this plays. This flag also has a built-in slow fade-in/fade-out system so that the transitions aren’t abrupt. However, we lose some control by doing it this way. For one, the monster is invisible even in the starting Spawn state. So there is no reward for a stealthy player who managed to sneak up on the monster without alerting it. The timings of all the state sequences are also the same as the default, and no sound cues have been added. These types of Stealth Monsters are highly discouraged in the modding scene as they are very difficult to balance. They get abused by novice modders and end up becoming very unfair. Game-design sense is an important skill that, imho, you cannot learn from a guide or tutorial.

ZScript is a lot more powerful than what I have presented here. Everything we have covered could have also been done in the older scripting language called Decorate. I didn’t introduce EventHandlers, custom functions, dynamic arrays, or manipulation of level data. Consider this guide just the start of your journey, and devour the more advanced guides next: Ash’s ZScript basics and David Newton’s Youtube Tutorials.

I have introduced the use of the A_StartSound() function but did not introduce the SNDINFO or SNDSEQ lumps or show you how to include custom sounds in your mod. I encourage you to research and experiment with these on your own.

Basics of mapping

Doom modding is dominated by custom maps. This is the most popular format for sharing and enjoying the community’s creations. Over the years, there have been several mapping contests: spanning long-term veteran quality collections (see Doomer Board Projects) to newbie-friendly, all-comers compilations (checkout RAMP, run by David Newton). Single-map mods have won Cacowards quite often. Mapping for Doom can be a very fun, addicting experience. In this guide, I will only cover the basic setup of one of the programs that can be used for it, as well as using custom resources, and some quirks about map-only WAD files and their lump naming and packaging.

Mapping is fundamentally a visual task. You need an editor with a GUI to even get anywhere. Therefore, the best tutorials are all in video format. In fact, (semi-)professional doom mappers livestream their process on streaming websites. If you were to get on a video-sharing site like youtube and search for something like “doom mapping tutorials,” and filter for playlists, some of the earliest uploads you will see are from a personality who calls himself Chubz. He has a couple of series (here and here) on an editor called Doom Builder (he seems to have created one for ACS scripting too). We won’t be using Doom Builder here, but the one I will introduce (Ultimate Doom Builder, or UDB for short) shares some genealogy with Doom Builder and related editors. Most of those lessons are directly applicable with very few required changes that you will be able to figure out on your own.

More modern playlists from lazygamer or raven67854 are more explicitly geared towards UDB and GZDoom, and are an excellent resource. There are even videos by detractors who don’t like the pace and tenor of these playlists, who have their own video take (some strong language in that one). John Romero himself (who was part of the original Doom team in 1993) livestreamed some mapping he did on Sigil 2 (and UDB was good enough for him). Famous mappers like bridgeburner and Dragonfly make spot-tutorials for obscure/esoteric mapping tricks that serve as reference material for even hardcore modders. James Paddock (who made the midi soundtrack for Sigil 2) and our friend David Newton (why isn’t he writing this!!) also have great mapping videos. You should watch those last two links both before and after going through this section of the guide. It will make more sense the second time. I apologize to other famous mappers with a catalog of video tutorials for not mentioning them here.

Getting UDB

Now that we’ve gotten past the hyperlinks to youtube videos and playlists, let’s get back to some exercises. This being a GUI, this section will have a lot of screenshots. First, create a directory named maps in your mod’s project folder (this is a reserved folder name). Secondly, to get Ultimate Doom Builder, you can try their official website, or the ZDoom forums. For Linux, the build instructions are in the README file on the github repository (although on modern Linux systems, install winetricks dotnet472 and wine-mono, and use wine to run the portable archive Builder.exe). Once you launch UDB, you will be greeted to an opening screen with some graphic. Using the top-left popup menus, navigate to Tools -> Game Configurations (or just press F6). This will bring up a Game Configurations window with a list of map formats on the left and a bunch of tabs for panels on the right. Select GZDoom: Doom2 (UDMF) format from the list, and in the Resources panel, add freedoom2.wad, gzdoom.pk3, and my_fist_mod.pk3 files to the list (so that we can use stuff from your mod!). The Game Configurations window will look something like this:

screenshot07.jpg

In the Nodebuilder tab, you can set both configurations to (do not build nodes) if you are on Linux. The engine can build BSP nodes during loading. In the Testing tab, you can let UDB know where your GZDoom executable is if you want to. This is to be able to launch the engine directly from UDB for rapid testing. This guide won’t be using that method, as it robs us of the opportunity to learn a few other remaining, beginner concepts.

With the Game Configurations out of the way, now navigate to File -> New Map (or just press Ctrl + N). It should bring up this Map Options window:

screenshot08.jpg

Select the GZDoom: Doom 2 (UDMF) option for Game configuration. Leave Script Type option as ZDoom ACS (even though we won’t be bothering with ACS scripting here). Make sure that the Level Name option is MAP01. This is important. This is not the name of your file. Nor is it the conceptual name of your map. It is an engine internal identifier lump, and I will explain its significance further down. You can re-add the three resource files: freedoom2.wad, gzdoom.pk3, and my_first_mod.pk3 here. Hit okay and you will be greeted to a blank grid, with some unlabeled buttons to the top and the left (hover over for a tool tip). There will be a broad, bottom bar that says Vertices Mode. This can be hidden, but let’s leave it open since we are a beginner. At the bottom right, among other things, there will be a grid-spacing setting (I think it defaults to 32 mp). The view should look like this:

screenshot09.jpg

Your first room

For starters, do not click anything. Save the blank map inside the maps subfolder in your mod’s project folder. Give it a name like myfirstmap.wad. The top title of the UDB window should now say “myfirstmap.wad (MAP01)”. Now let us learn our first hotkeys. Press the L-key on your keyboard. The broad, bottom bar should now read Linedefs Mode instead of Vertices Mode. You can get back into Vertices Mode by pressing V. Pressing S should swap you to Sectors Mode, and hitting T will take you to Things Mode. Just practice changing between these various modes. What your left-click or right-click is able to do depends on what mode you are in. To draw or select lines, you will have to be in Linedefs Mode. To draw/select sectors, be in Sectors Mode, and so on. You can, of course, switch between these same modes by clicking on the appropriate button on the left panel. But I want you to get used to using hotkeys. By the end of year-two of Doom mapping, I want you to be able to match Bridgeburner’s hotkey prowess from this video. Write them down in a cheatsheet if you have to. You can, of course, change the default hotkeys to your preferred bindings later.

Right. Now let’s focus on the blank grid expanse in the center of the screen. This grid presents a top-down plan view of your map (which is empty right now). Here, you can use the mouse’s scroll wheel to zoom in and out. And if you hold down the spacebar-key, your mouse movements will cause the grid to pan. It might be hard to tell with the only fixed-point of reference being the plus-sign at the center/origin. So let’s create a point of reference by drawing a rectangular room somewhere. Press Ctrl + Shift + D to enter the Draw Rectangle Mode. Now left click somewhere in the blank grid a little bit to the top-left of center. Let go of the mouse button. Now move the mouse pointer towards the bottom-right until the rectangle being dragged open is a decent size (something over 256 \(\times\) 256 is good). Left click again. This should materialize the rectangle and kick you out of the Draw Rectangle Mode.

You are looking at the top-down view of your first room. All you can see is its floor texture (there is a button that switches it to show the ceiling texture in the top-down view, but I won’t tell you where it is!). This room has one Sector, four Vertices and four Lines. The Lines have a little notch at the center pointing inwards into the room. This notch tells you where the “front-side” of the line is. These four Lines are single-sided lines since their backs are facing the void. Now if you switch to Sectors Mode (S) and left-click on the room’s floor anywhere, you will “select” the sector. Press the C-key to cancel the selection. Switch to Linedefs Mode (L) and you will be able to select the lines by left-clicking on them. You can select multiple lines by drag selecting, or clicking on multiple of them in sequence. Again, hit the C-key to clear the selection. The C-key is your best friend.

When in any of these modes, if you right-click on the appropriate entity (and not in the void or empty region), you will bring up a window for its properties. If you had multiple entities selected before you right-clicked on one of them, the properties will collectively apply to all of them. If you accidentally right-click in the void (or within a Sector while in Linedefs Mode), you will start drawing lines. Hit the Escape-key to snap out of it. The Escape-key is your other best friend. If you right-click on a Line while in Vertices Mode you will create a new Vertex and split the Line into two Lines there. Hit Ctrl + Z to undo that action. Or select that Vertex while in Vertices Mode and Delete it.

Using custom textures from your mod

Now, get into Linedefs Mode (L) and right-click on the top (North) Line of your room. It should bring of a properties window with tabs in a top row. Click on the Front tab (remember, this line is one-sided and doesn’t really have a Back-side). To the right you will see a column of three gray squares, with the middle square displaying a texture and a name like STARTAN1 or STARTAN2 (these are just the UDB defaults). Left-click on the middle square to bring up another window that allows you to select some other texture. It will show you all the available textures to choose from based on the resource files (freedoom2.wad, gzdoom.pk3, and my_first_mod.pk3) that you specified in the Game Configuration setting.

screenshot10.jpg

Scroll down until you see my_first_mod.pk3 in the list. Selecting it and then All will bring up three textures. If you recall, I used a custom image (of John Romero’s forehead) called John_Romero.png and then created a virtual texture lump in the TEXTURES.lmp patch called BIGDOOR1. The both of these, as well as our modified AQRUST08 texture are showing up as selectable options. Note that only the first-eight DOS-friendly characters of John_Romero.png show up. Let us select our modified AQRUST08 and apply it to the middle-texture slot of the Line. As far as playing is concerned, it doesn’t matter whether you use the AQRUST08 texture lump from freedoom2.wad or my_first_mod.pk3 here, as the mod ensures that the replacement gets applied in GZDoom. But for UDB to visually display the right one within its views, we might as well use our modified lump.

There is a checkbox for “Long Texture Names” in the Browse texture window. NEVER turn that on. It will store the absolute file-path to your texture file in the map wad instead of the short-lump name. It is better to use the lump names, as this keeps your mod … well … mod-able (both by yourself and others). If someone in the future makes a replacement mod that, say, replaces AQRUST08 with a higher-resolution version with PBR-material shaders for realistic lighting and reflections, that replacement won’t take effect on your map if it isn’t just using the simple lump name AQRUST08.

Okay great. We have a room with one wall painted with our modified texture. To actually make this map playable, we need to add a player-start Thing. Enter Things Mode (T) and right-click anywhere inside the room. This should, by default, place a Player 1 startThing” on the map and bring up its properties window. Here you can set the starting Angle you want the player to face and hit OK. If you accidentally placed additional Things, select them in Things Mode by left-clicking them and hit Delete (or use Ctrl + Z a bunch to undo recent steps).

screenshot11.jpg

Now just an empty room (albeit with some art on one of the walls) is boring. Let’s add some other Things for the player to interact with. While still in Things Mode, right-click at some corner of the room and expand the Monsters folder in the Things selection panel. You should be able to select an entry called $FN_ZOMBIE. Normally, this would have said “Former Human”. But since we added a class-replacement clause in our ZScript file for the ZombieMan class, UDB doesn’t know which resource file to prioritize. So it is trying to display a Tag Name, but is running into a LANGUAGE lump substitution bug (don’t worry about it, it might have gotten fixed by the time you read this). Anyway, select $FN_ZOMBIE and make sure that his Angle is facing away from the Player 1 start Thing so that he doesn’t get alerted as soon as the game starts. When you launch the game with your mod, the class replacement will automatically kick in, and you will be fighting an MFM_InviZombie. Similarly, scroll down to find the Health Bonus Thing and place it somewhere else in the room.

FN_Zombie.jpg

Placing custom things from your mod

Now you might be wonder, how do we place our shiny MFM_Elixir Thing into the map. Even though we defined it, we removed the replacement clause (like I asked you to!). So it exists as a separate independent entity that can be summoned in the game from the console (and by killing the MFM_InviZombie class with weak bullets). But it is not available in this list for pre-placement on the map. To fix this, we have to give it an Editor Number. Leave UDB open in the background (minimize it or switch to a different workspace if your OS allows it). Now create a new file in your mod’s project folder called MAPINFO.lmp. In it, put the following text:

DoomEdNums
{
  20001 = "MFM_Elixir"
}

Map wad files aren’t storing Things in them by their entire definition. It only stores a Thing’s editor number (along with some map properties like starting angle). Most standard Things have a standard editor number that all Doom source ports and map editors respect (more or less). For GZDoom, as of this writing, all editor numbers between 11,000 to 14,000, and between 14,166 to 31,999 are available to modders for custom classes. Here, we have assigned the number 20001 to our custom MFM_Elixir class in a new (reserved name) lump file MAPINFO.lmp. Now re-zip the archive:

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp

I understand that this zip command is getting a bit long. I will present my solution to this problem later. For now, it is important to know what is (and what is not) going into the pk3. For example, I am still leaving out the maps folder that I had you create a while back in this section. Also, since we are editing these files, the editing software can sometimes create temporary or permanent backup copies of the files (usually with the same names and a tilde “~” character appended in the end). Make sure that you don’t accidentally zip those into the pk3 file.

Now return to UDB, navigate to the Tools popup menu and reload resources (F8-key) from your resource files (freedoom2.wad, etc.). Right-clicking in Things Mode again should allow you to scroll down to the User-defined folder and place the MFM_Elixir class in your map. The display sprite for it in UDB will be the same as that of the Health Bonus since we didn’t actually change its Spawn state sprite. We merely changed the rendering style, which is an engine thing, not a UDB thing.

elixir.jpg

Visual mode and Line action special

The room is going to be fun for a while. But eventually, the player will want to leave it. Let’s add an exit button. Switch to Vertices Mode (V). Right click at two places on the northern Line of the room again to place two Vertices exactly 64 map-units apart (it should be easy if the grid-spacing is set to 32 mp at the bottom right). This splits the northern Line into three. Switch to Linedefs Mode (L) and right-click on the middle Line as shown to bring up its properties window. In the Front tab again, change the middle texture to a switch texture like SW1COMP (you can type it below the gray square without bringing up the Browse texture window). Any switch texture that is 64 units wide or greater is good here. Hit OK.

screenshot12.jpg

The switch is just a texture painted on the wall. You can’t activate it yet. Before we learn to make it do that, I want to introduce you to the visual mode in UDB. So far, we have been looking at our level in the top-down “plan” view. Here, we could switch amongst a bunch of “modes” and operate on the map. But you can look at your map in a 3D perspective from within UDB. Move your mouse cursor somewhere close to or inside the room, and hit the Q-key. Now look around using the mouse. Do you see your room? Don’t click on anything. Hit Q again to return to the “plan” view. Move the mouse cursor over to a different location and enter visual mode again (Q) to spawn your viewpoint at that new location.

In visual mode, you can actually fly the viewpoint through the map using the E-S-D-F keys by default (these are shifted by one key over to the right from the standard W-A-S-D movement keys for FPS games). While in visual mode, the “modes” have no meaning. The map-entity (walls, floors, ceilings, or Things) that the central cursor is pointing at will be highlighted with a pulsating orange glow, unless highlighting has been toggled off with the H-key. Anything you do (like hit keys or scroll the mouse wheel) will affect the entity highlighted, even if the highlighting effect has been visually turned off (hit H to turn it back on again). Like pressing the arrow-keys (with or without the Shift-key pressed) while a wall or floor is highlighted will offset its texture. The arrow-keys can move locations of highlighted Things. And the mouse wheel can raise or lower a highlighted floor or ceiling. People, epecially newcomers, find this behavior very frustrating. The way to avoid this problem is to actually select the entities you want to manipulate by left-clicking them (yes, while in visual mode). Selected objects pulsate in red, and you can selected multiple entities by sequential clicking on them (press your best friend the C-key to clear selection). When one or more entities are selected, hitting keys or scrolling the mouse wheel will now only manipulate the selected entities. Let’s practice doing it this way.

screenshot13.jpg

Find the middle-segment of the northern wall with the switch texture. Unless you really lucked out with your room Vertices location, it will most likely be misaligned as shown. Left-click on it while still in visual mode so that it pulsates with a red highlight instead of orange. Now use Shift plus the left or right arrow keys to change the texture offsets until you are satisfied with the alignment. Hit C to clear the selection and then Q to exit back to the “plan” view mode.

Now, to make the switch actually usable, we have to give that middle Line an action special. From Linedefs Mode, right-click on that line, and in the Properties tab, in the Action panel, you can click on the list icon to the right of the wide bar and search for the End Normal action special (or just type the number 243 in the smaller leftward box as shown). Then in the Activation panel, click on the “When player presses use” checkbox to tick it on. Hit OK. Now your Line will execute the action special End Normal defined in the UDMF map standard when the player presses the USE key while facing it (and is close enough). Note that we assigned this to the Line, and not the switch texture. That whole wall is technically the switch, and can be activated from any height (like from a ledge or ladder). That picture of that switch at the bottom is just for show. This is just how Doom works. If you want a height restriction, you will have to do something clever like a lowered ceiling and a recessed sector.

screenshot14.jpg

Save your map (Ctrl + S). Now to play it, I already committed to not showing you how to do it from within UDB. So we use the launcher program and add the map file myfirstmap.wad (it was saved in the maps subfolder of your mod’s project folder) to the list of mods in our preset. Then launch the game. You can (in my experience) do this while the map file is open in UDB. If you want to launch from a terminal command prompt instead, the command would be:

./gzdoom -iwad freedoom2.wad -file <PATH-TO-PROJECT-FOLDER>/my_first_mod.pk3 <PATH-TO-PROJECT-FOLDER>/maps/myfirstmap.wad

Voila. You are playing your map in the game. There is the MFM_Elixir, and the MFM_InviZombie. And that tempting switch. If you rush forward and hit it, the level MAP01 will end and the game will show you the intermission screen, and then load MAP02 for you.

Switch patch and animation

You might have noticed that when you hit the level-exit switch, the level just ends. The switch didn’t actually visually flip, and there was no switching sound made. While this is okay for a level-exit switch (after all, there is no time for the sound to play), it will become problematic if you want to use that switch texture for other things (like opening doors) in other places on the map. Side-note: Typically, you are supposed to have a distinctive looking special switch for level exits that don’t look like other switches that do other things. A game-design aspect of visual clarity. But let’s leave that aside for now.

When I introduced the TEXTURES lump, I promised to show you how to slap a switch onto any wall texture. The switch textures on offer from freedoom2.wad (browse through them through UDB now instead of SLADE) all come with background wall textures, and none of them are on AQRUST08. Let’s fix that. In our TEXTURES.lmp file, we can define two new texture lumps by combining our modified AQRUST08.png file and two internal switch lumps called SW1S0 and SW1S1 from inside freedoom2.wad like so:

Texture MYSW1, 64, 128
{
  Patch "textures/AQRUST08.png", 0, 0
  Patch SW1S0, 16, 82
}

Texture MYSW1ON, 64, 128
{
  Patch "textures/AQRUST08.png", 0, 0
  Patch SW1S1, 16, 82
}

I have called these new virtual texture lumps MYSW1 and MYSW1ON, and their overall sizes are 64 \(\times\) 128 pixels. The patches are applied in the order they are specified, and I have carefully chosen the patch offsets to put the switch at the bottom center. Now, we create a new file with yet another reserved lump name: ANIMDEFS.lmp (be sure to not type “anime” by mistake). Its contents should be:

switch MYSW1 on sound Switch1 pic MYSW1ON tics 0
switch MYSW1ON off sound Switch2 pic MYSW1 tics 0

This is invoking internal sound lumps named Switch1 and Switch2 from inside freedoom2.wad, and telling the engine to treat our two new virtual textures as switches that are complementary states of each other. Now zip-up the whole thing again:

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp ANIMDEFS.lmp

In UDB, you can reload the resource files (F8) and change that one Line’s texture to the newly available MYSW1. Go into the visual mode and align the texture of that middle wall (and/or neighboring walls). Come out of visual mode. Save (Ctrl + S). Now launch the game again with your map. This time, assuming that your framerate is high enough, you should see the switch change states (and briefly hear the Switch1 sound effect) as the level ends.

screenshot15.jpg

Practice a small set of hotkeys over and over again until you get the hang of them. Then add one new hotkey for every new feature you learn. To review, watch this. And this and this.

The Mapfile lump

You can close UDB now. We are done with that.

Let us analyze what happened when you launched the game with myfirstmap.wad. You might recall that when you first started a new map in UDB, it opened a Map Options window where there was an option called Level Name. I had asked you to leave this as the default MAP01. Well the IWAD that you feed the engine contains instructions for what map to start the game in, and which map follows which. freedoom2.wad, for example, instructs the engine that a map with the lump name MAP01 is supposed to be the first map, and that MAP02 should be the next one when, for example, the End Normal action special is executed from MAP01. And this is why, when you loaded you map with the freedoom2.wad as the IWAD, it started you off in your custom map. And upon hitting the exit switch, it sends you off to MAP02 (contained within freedoom2.wad). Your map replaced the default MAP01 lump inside the IWAD.

Changing the map lump name

Now let’s do some experiments. Open the launcher program again, and this time launch your mod with freedoom1.wad as the IWAD (I hope you didn’t delete that one after the download!). You will note that the game does not start you off on your custom map. It just starts you off in Freedoom 1’s regular maps, no matter which of the four “Episodes” you start with. In fact, you can play through the whole of Freedoom 1 and never encounter your map.

That is because Freedoom 1 instructs the engine to follow a different set of map lump naming conventions. If you start with Episode 1, the engine starts you off in map lump E1M1. The next map in the sequence is called E1M2. And so on. You can still tell the engine to force you into the map though. Launch with freedoom1.wad as the IWAD again, and this time, run `changemap MAP01` in the console. If you hit the level exit switch though, it will take you to the end credits sequence, since the IWAD freedoom1.wad doesn’t tell the engine what the next lump after MAP01 should be.

You can define your own sequence of maps and map lumps in your MAPINFO.lmp file. This is also where you would specify the level soundtracks, what sky texture to use for each level, give them fancy nicknames like “Hanger Base”, and so on. I won’t show you how, but know that this exists. And it doesn’t have to be a linear sequence of maps (with or without episodes) either. You can even specify clusters, with a central hub map and several “spoke” maps that you can travel to and return from. Godless Night is my favorite example of this structure. We will see an example PWAD that does this later.

Open myfirstmap.wad in SLADE. It can do it. It is a WAD file after all. You should see its insides:

insidemap.jpg

It is simple, as it should be. Maps can get a little heavier that that later on, when you add more complicated things to your map, like ACS scripts, and Dialogue lumps. Remember that rule I had about not modifying anything through SLADE? This is where we break that rule. Rename the first marker lump from MAP01 to MAP02. Save. Do not change the actual WAD file name. Now launch the game again with freedoom2.wad. This time, you will have to play through the entire first level of the base game before it plops you into your custom map, which now has the lump name MAP02. And after you hit the exit switch in that, the engine will load MAP03 for you. You could have named it E1M1 and launched it with freedoom1.wad as well. Now change the name back to MAP01 again through SLADE (hit save before closing).

Packing the map with your mod

So far we have some texture/sprite replacements, and custom classes mod in a pk3 file (my_first_mod.pk3), and a map in a wad file (myfirstmap.wad). But can we smoosh them together? The pk3 format already has a way to do that. First, rename myfirstmap.wad to map01.wad (the file name has to match the lump name for this method). Then, just add the map file (along with the maps subfolder) to the zip archive:

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp ANIMDEFS.lmp maps/map01.wad

And you are done. Note that I specified the file name maps/map01.wad in full in the command instead of just the subfolder name maps/. This is because if you look inside the subfolder, you will see a lot of backup files that got created by UDB and SLADE when we opened the file in them. We don’t want these files ending up in our pk3 file. I could have specified a regular expression like maps/*.wad too to catch only the files that end in a .wad. In the end, the structure of your full my_first_mod.pk3 should be:

final_pk3.jpg

Now if you launch the game with just freedoom2.wad (or Doom2.wad) as the IWAD, and your mod my_first_mod.pk3 as the PWAD. No extra map file needed. You should be playing your mod in your map. You can create more maps, name them MAP02, MAP03, and so on, and package them all inside the maps subfolder in your pk3 file. There is some more work to be done if you want to make your own standalone IWAD. We will analyze some publicly available IWADs in SLADE later. But for all intents and purposes, you are now a certified doom modder. Now go create something!

Misc.

Studying some public mods

We have barely brushed the surface of most concepts in this guide. So our knowledge is limited when it comes to analyzing extant mods. But there are plenty of popular but simple mods out there that we can look into, and much can be gleaned from even the complicated ones using some guesswork and context clues. Let us download the following mods in order:

OTEX texture pack

The OTEX textures are a wildly popular option for modern megawads and mappacks. They are created by the veteran texture artist Ola “Ukiro” Björling, who has also made some famous Doom maps back in the day, and dabbles in music. If you’ve played through either of the Eviternity megawads, you’ve seen these textures. You can get them from this forum post on Doomworld. It links to a downloads’ page on Ukiro’s on website, wherein you can read his permissions text, and download links to both the *.wad and *.pk3 versions. Download the pk3 file and open it in SLADE. You should see this simple file structure inside:

otexfiles.jpg

All three folder names are reserved names, as is ANIMDEFS. The README.txt file contains the credits list, the redistribution permissions, a changelog, and some useful information about creating new textures using the png images inside the patches folder. You would do what you learnt regarding the TEXTURES lump in making the level-exit switch for you first map. Ukiro has been kind enough to not just share finished textures, but to make the raw patches public to allow anyone to mix and mash them into new textures.

The ANIMDEFS lump contains a short list of switches, just like we defined in the mapping segment for the level exit. Further down, it contains a bunch of texture sequences meant for animations:

otex2.jpg

For example, the first line is instructing the engine to animate the texture lumps OROCKO2A, OROCKO2B, OROCKO2C, and OROCKO2D by sequentially replacement every 4 ticks (remember that there are 35 ticks per second, see the wiki for the syntax). You can find these textures in the Textures folder and cycle through them to see how they animate. They constitute a pulsing, glowing, solidified lava surface. Further down in ANIMDEFS.txt you will find the line:

Flat	Optional	0ICYWA01	Range	0ICYWA08	Tics 5

The texture lumps 0ICYWA0x (x goes from 1 through 8) simulate an icy water surface (you can find these in the Flats folder). This is a great way to animate liquid-surface textures like water, nukage, lava, blood, or sewage goop. Every frame of these liquid textures is designed to tile seamlessly with themselves, so you can cover arbitrary shapes and sizes of contiguous areas with them.

If you are getting into mapping, adding the OTEX pk3 as a staple resource file (along with the UDB config on Ukiro’s website that helps arrange the textures into subcategories) will offer a great many inspiring set of textures to play with. Just be sure to include the pk3 file in your launcher too!

IDKFA music wad

The IDKFA album (named after one of the cheat codes from the original Doom release) is a modern, metal reimagining of Bobby Prince’s original Doom 1 soundtrack. It was created by Andrew Hulshult, who has a lot of music credits in commercial games you may have heard of. The zip-download on the modDB page is around 720 MB large, but it includes the music in *.flac and *.mp3 format for your enjoyment outside of the engine. The actual IDKFAv2.wad file within is only 87 MB large. If you open this in SLADE, you will only find sound lumps with very specific names. These are the default soundtrack lump names from Doom 1 (and not Doom 2), and this is a simple lump-replacement wad. In case you don’t have Doom 1, you can play this with freedoom1.wad too, since the whole point of the FreeDoom project is to use all the same lump names.

If you open gzdoom.pk3 in SLADE and navigate to mapinfo->doom1.txt, below the GameInfo block and the episode definitions (Doom 1 and Freedoom 1 both arrange their levels into episodes), you will see the map definitions like so:

idkfa.jpg

The music for map E1M1 is defined as the variable $MUSIC_E1M1, which is in turn defined in the language.def lump in the root directory. As a homework assignment, look up both mapinfo->doom2.txt and the language.def file for what to rename the first soundtrack in IDKFAv2.wad so that it plays on MAP01 in Doom 2 or Freedoom 2, even with your my_first_mod.pk3 (don’t forget the “D_” prefix).

Bow and arrow weapon

Next we will look at a small weapon mod by Gothic. This pk3 file can be download from Realm667.com, which is a long-running repository of sorts for community creations. Before opening the mod in SLADE, I suggest just playing it in Freedoom 1 or 2 to get an idea of what is being implemented. Put the pk3 in you launcher mod list and start the game. Open the console and run summon bow. It should spawn a bow with ten arrows that you can collect (sits in weapon-slot 3). Now, if you press-and-hold the primary fire button, the bow gets drawn and remains drawn. If you let go, the arrow flies (and sticks to the wall for some time before disappearing). Run give arrowammo or summon arrowammo in the console if you run out of arrows. If you press-and-hold the alt-fire button (typically bound to the mouse right-click button), your screen gets an overlay like so:

bow.jpg

Using the W-A-S-D movement keys while the alt-fire button is still pressed lets you select between four types of arrows. Letting go of the button will play a weapon-sprite animation and change your arrow type. The fire arrows explode, and the frost arrow freezes its target. You can then walk up to it and punch it to pieces!

This is obviously a fairly complex weapon. Analyzing the code for it will also be difficult with our current skill level. Luckily, Gothic has also included code for a simplified version of the weapon called BowSimple. You can summon this in the game by running give bowsimple. This gives you a simplified bow with no alt-fire magic abilities. This one hasn’t been bound to a weapon slot so don’t deselect the weapon!

Now we can open Bow.pk3 in SLADE. The file structure should look like so:

bow2.jpg

The big thing to point out here is the lump named DECORATE.txt. This is the older scripting language that ZScript has since superseded. You should learn and stick to ZScript, since it is more powerful and will see continuous development. But some of the older mods still have a lot of DECORATE code in them, and it is good to know how to look at it and parse it well enough. If you look at the contents of the DECORATE.txt file, you will see a definition for the Bow class at the top. But the syntax is very different from ZScript! The class-definition line is different, there is no Default block, and where are the semicolons!!

Let us skip past the Bow class and scroll down to line 212. Here is the code:

////Simplified Bow, if you just want a regular bow
Actor BowSimple : Weapon
{
  Inventory.PickUpMessage "Bow"
  Weapon.AmmoUse 1
  Weapon.AmmoGive 10
  Weapon.AmmoType "ArrowAmmo"
  Tag "Bow"
  //+WEAPON.NOALERT
  States
  {
  Spawn:
    BWPU A -1
    Stop
  Select:
    TNT1 A 0 A_ZoomFactor(1.0,ZOOM_INSTANT)
    DBOW A 1 A_Raise
    Loop
  Deselect:
    TNT1 A 0 A_ZoomFactor(1.0,ZOOM_INSTANT)
    DBOW A 1 A_Lower
    Loop
  Ready:
    DBOW A 1 A_WeaponReady
    Loop
  Fire:
    DBOW B 4
    DBOW C 4 A_StartSound("Bow/Hold",CHAN_WEAPON,CHANF_OVERLAP)
    Goto Hold
  Hold:
    DBOW D 1
    {
      if(GetPlayerInput(INPUT_BUTTONS) & (BT_ZOOM)) { A_ZoomFactor(4.0); }
      else { A_ZoomFactor(1.0); }
    }
    TNT1 A 0 A_Refire
    DBOW E 3 { A_ZoomFactor(1.0); A_Overlay(7,""); A_StartSound("Bow/Shoot",CHAN_WEAPON); A_FireProjectile("ArrowProj",0,true); }
    DBOW FBA 3
    Goto Ready
  }
}

The state block is using some standard state labels for weapons: Spawn, Select, Deselect, Ready, Fire, and Hold. You can guess what these standard states correspond to by their names (you can look up the sprites being used for some contextual help). The Fire state sequence is obviously entered when the fire button is pressed. The weapon then runs through the DBOWB0 and DBOWC0 sprites for 4-ticks each and plays the “Bow/Hold” sound lump, and transitions to the Hold state sequence. Here, there is an additional test to check if the zoom-in button is pressed (you can bind this to something like the Z-key from the options menu). And then there is a call to A_Refire(), a built-in action function that checks if the fire button is still being pressed, and returns the actor to the start of the Hold state sequence if so. This is the loop the weapon gets stuck in (with the bow drawn, held in DBOWD0 sprite) for as long as the fire button is pressed-and-held. But once it is released, the A_Refire check will fall through to the next line, where a sound lump (“Bow/Shoot”) is played and A_FireProjectile() is called with the projectile type “ArrowProj” (defined further down in the file, from line 255 onwards). The weapon turns through a three-sprite-frame run-down animation and returns to the Ready state sequence.

If you move the TNT1 A 0 A_Refire line to the end of the Hold state block just above the Goto Ready line, then the BowSimple weapon will lose its press-and-hold-fire-to-keep-bow-drawn property and instead fire a rapid succession of arrows (you can increase the number of ticks for one of the earlier sprite frames in the Hold block to drop the fire rate). You can even replace the projectile type to one of the predefined ones in gzdoom.pk3, like PlasmaBall, to have the bow fire that. A good exercise might be to move both the BowSimple and ArrowProj class definitions to your ZScript lump in my_first_mod.pk3 and convert the syntax over to ZScript to make it work with just your mod. You’ll need to modify the starting class-definition lines, add Default blocks, and throw in a whole bunch of semicolons. The GetPlayerInput(INPUT_BUTTONS) & (BT_ZOOM) check is also done differently in ZScript (player.cmd.buttons & BT_ZOOM). You could even add a slot number to the weapon in its Default block. Don’t forget to copy the sprites and sounds over (and credit Gothic). Note the two new (to you) built-in class types: Weapon and Projectile.

I had you skip the regular magic Bow class definition at the beginning of the file because it is more complicated. Feel free to glance at it. It has a whole bunch of non-standard custom state labels. And it uses a lot of dummy inventory items as tokens to keep track of what magic-arrow “mode” you are in, and do the conditional thing. DECORATE was really limited when it came to custom variables and functions, so giving and taking inventory tokens (and checking for their presence/absence) used to be the only way to institute conditional logic back in the day. We can look at a slightly simpler example of that concept in this next mod.

Fast-travel through dialogue lump

Now for some shameless self-promotion: we can analyze one of my own template mods. You can read my description of it from the forum post, but it is better to just play it (with freedoom2.wad). There is a 3D-model canoe in the starting level (in every level really) that you can “talk” to by pressing the “use” key in front of it. If you haven’t collected the world map (or the partial world map), it won’t give you any options. But if you had either of the maps, it should let you travel to any of the other levels shown in the map. I made four small template maps out of OTEX textures, and threw in some public-domain ambient soundscapes to sell the location.

fast_travel_map.gif
Figure 8: Collect that map and talk to the canoe!

I’ve hijacked the built-in character conversation/dialogue system (originally invented for Strife) that allows you to converse through dialogue trees with any actor. The maps have some interesting things too. If you open them in UDB and right-click on the lines at the edge of the sea, you’ll see the Line Horizon property being used, making the horizon look infinitely far away. Some other lines have the Block players flag on. Additionally, the sectors representing the floating ice sheets have the floor waggle function being called on them via ACS scripting (something we didn’t go over in this guide). And lastly, all the levels are setup to be in a hub cluster. This means that changes made to the level are persistent even if you leave them and return, even through saving and loading. You can test this by destroying the explosive barrels placed in some levels or writing your name in charred decals on the wall (use give plasmarifle and give cell in the console).

Let’s open the pk3 in SLADE and look inside. The file structure looks like so:

fast_travel_files.jpg

First, look into the mapinfo.lmp file. The block

clusterdef 1
{
  hub
}

declares the cluster “1” as a “hub” type. The four map blocks further below have all been assigned cluster = 1. The map blocks also have a “next =” line that has some other map assigned to each. I am not really using this feature, but this dictates the next map that the game would have loaded had I executed a line special like End Normal, just like you learned to do in the Basics of mapping section.

Next, look at the zscript.zc file in the root directory. It has the version string and two #include lines for two other ZScript files in some subfolders: zscript/fasttravelzc/CustomActors.zc and zscript/fasttravelzc/EventHandlers.zc. This is a great way to organize ZScript code into separate files instead of dumping it all into a single lump. I won’t go over the EventHandlers.zc file (it contains some code that dictates when, which, and how to draw the world-map images to the screen). Let’s look at the contents of CustomActors.zc. It opens with a class definition called Canoe of the type Actor. Within this, besides the Default block and the States block, there is a block we haven’t seen before:

override void Tick()
{
  Super.Tick();
  Vel.Z = 0.03*Sin(10*Level.Maptime);
}

This is “overriding” a virtual function called Tick() that was inherited from the parent class (Actor). This Tick() function gets called every tick by the engine (35 times a second). Inside it, I am running Super.Tick(), which basically runs the Tick() function from the parent, just to make sure nothing important gets missed. Then I am modulating the vertical velocity of the Canoe sinusoidally to simulate the boat bobbing in the water. I could have gotten some default bobbing behavior by flipping the “+FLOATBOB” flag in the Default block, but this approach gave me more control.

Further down the file, starting from line 29, I define a class named WorldMaps of type Inventory. It has some lengthy definition that we can skip past. Starting from line 91 onwards, I’ve defined two classes named WorldMapPartial and WorldMapFull, both of type WorldMaps (meaning they inherit all that lengthy definition that we skipped analyzing). They both sport a couple of custom Default block lines, and a couple of overrides for standard Inventory item functions. The HandlePickup() overrides make sure that WorldMapFull replaces WorldMapPartial in the inventory when picked up, and that WorldMapPartial cannot be picked up if the toucher already possesses an instance of WorldMapFull. The TryPickup() overrides are actually giving the toucher additional inventory items. The WorldMapPartial comes packaged with a FortBluePass item and a BrickPortPass item. WorldMapFull also awards those, along with an additional WasteProcPass item, and an ArcticBasePass item.

You might have noticed that these items bare the short-hand names of the four levels in this hub. Lines 145-170 show that these are dummy inventory tokens of a custom type: “Passport”. These are used by the canoe actor in deciding what options to make available to the player based on what passports the player has in their possession. More on that later.

Line 172 starts a lengthy definitions for yet another custom Inventory type class named FastTravelTicket. The code handles a screen fade on a timer, followed by a call to level.ChangeLevel(). Below this are four quick tickets defined (one for each level), all of type FastTravelTicket (and thus, inheriting its parent-class properties). These inventory items will begin a level-transition sequence as soon as they enter the player’s inventory, and destroy themselves once the transition is triggered. You can test this in the game. In the opening area, run give BrickPortTicket in the console to immediately begin transitioning into MAP02. These inventory items can even be placed/summoned into the map. They will be invisible since their Spawn state labels lack a sprite assignment, but if you run summon WasteProcTicket instead of give WasteProcTicket and then walk forward, you will “pick” the ticket up and transition to MAP03.

Lastly, let’s address the conversation/dialogue system. The detailed wiki article is pretty good, but the video tutorial by Chubz is more instructive. Towards the end, he introduces a shopkeeper mechanic and shows how the player’s inventory can be scanned for conditions (presence of a particular gun type or certain amount of currency) and conditionally display specific dialogue options based off of that. I have exploited this mechanic to check for possession of world map items (and passports), and conditionally display options to go to specific levels. You can look at the dialogue lump within, say, MAP01.wad in this pk3 by either opening it in UDB, or double-clicking on MAP01.wad in SLADE and having it open in a separate tab like so:

fast_travel_dialogue.jpg

The Universal Strife Dialogue Format has its own unique syntax, but the terms use words from natural language in an intuitive manner. Terms such as link, ifitem, choice, require, and giveitem explain themselves.

MyStandaloneGame template

To close this section off, let’s cover at least one IWAD (*.ipk3). Nash Muhandes is part of the engine Dev team, and has a big catalog of GZDoom projects. His MyStandaloneGame became the base template for a multitude of projects (it even got linked to on the wiki). It is launched by itself with GZDoom (meaning not with Freedoom 2 or Doom 2). Meaning:

./gzdoom -iwad MyStandaloneGame.ipk3

Judging by the version number in line 1 of the zscript.zc lump, this was originally created for GZDoom version 3.3.2. So launching it in modern GZDoom will throw out some warnings about the defcvars.txt lump trying to set some engine CVars that no longer exist. But the game will still launch. Among the new lumps in the root directory, the iwadinfo.lmp and the playpal.lmp lumps are the necessary ones for the engine to be able to recognize it as an IWAD. The playpal reserved name is meant for a palette lump. Since we have been using truecolor png files for textures and sprites, the playpal.lmp file can just be a dummy placeholder.

Inside the file zscript->MyStandaloneGamePlayer->MyStandaloneGamePlayer.zc is the definition of a class named MyStandaloneGamePlayer of type PlayerPawn. This is a special class for the actor that the player controls, and gets really complicated the more you dig into it. Nash has used his minimalist sprites to populate a whole bunch of standard state labels, which the engine will automatically transition the player into when certain conditions are met. And then the line in mapinfo.lmp that goes

PlayerClasses = "MyStandaloneGamePlayer"

assigns this PlayerPawn class to your player in this IWAD. You can actually play through MAP01 of Freedoom 2 as this player by running:

./gzdoom -iwad MyStandaloneGame.ipk3 -file freedoom2.wad

where freedoom2.wad is being interpreted as a PWAD despite being an IWAD. What happens when you get to the end of MAP01 and hit the exit switch? Can you think of the smallest change necessary to mapinfo.lmp that would let you transition from MAP01 to MAP02 of freedoom2.wad on level exit, instead of going to the end-credits? Try adding Next = "MAP02" (with the quotes) to the MAP block.

Try running:

./gzdoom -iwad MyStandaloneGame.ipk3 -file freedoom1.wad

Why didn’t the game start you in the starting level of Freedoom 1? Try running changemap E1M1 in the console. Does that reveal the problem? Now what about treating this ipk3 as a PWAD?

./gzdoom -iwad freedoom2.wad -file MyStandaloneGame.ipk3

What does the above command do? Or how about:

./gzdoom -iwad freedoom1.wad -file MyStandaloneGame.ipk3 -file freedoom2.wad

Watch out! The enemies and pickups now exist but your PlayerPawn doesn’t start with a weapon. Quick, run give shotgun in the console!

Productivity boosting microhabits

Anything good takes a long time to produce. So it necessarily has to take several sessions, perhaps over months/years. We have this romanticized idea of the crazed artist being struck by inspiration and then running to the basement and etching a piece or composing a poem in one sitting. While art that drains the artist has its place, I recommend the more structured approach of habit formation. This will let you sustain hobbies for much longer and actually stick with projects until the finish line without risking burnout. It is good to have a set time table for modding, and it is actually a good thing to stop working when you are “hot.” It makes the first few things to do in the next session very obvious, allowing you to hit the ground running and not waste cognitive energy on pointless decision-making.

Workstation setup and IDEs/text-editors

Much like in gaming, working consists of several nested state-loops operating over various timescales. And tiny discomforts or frustrations in the smallest loop can snowball over time into unpleasantness, and can cause us to not want to start/continue working without realizing why. It is important for the steps that are the smallest but are also repeated the most often to be as stress-free as possible. All micro-stressors should be eliminated. Just arranging your dedicated workstation at a comfortable desk and chair, and mounting the screen at mid-eye level while maintaining good posture can have a huge impact on your productivity. Have drinking water available on a different and/or lower platform to avoid spilling. If making/editing maps, drop the in-game music volume to zero and play some instrumental soundtracks in the background on your computer, so that it keeps playing constantly as you go back-and-forth between UDB and the engine. Reduced brightness and a low contrast between background and text also goes a long way when working with text.

Almost never having to move your arms maintains a low-latency between the act of desiring something of the computer and having it fullfilled. So avoid moving one hand to the mouse and back, or scanning and navigating popup menus, whenever possible. The OS-specific keyboard shortcuts for switching between windows (typically Alt + Tab and Shift + Alt + Tab) or switching workspaces (Ctrl + Alt + Arrow Keys) are a must if you are new to them. Much like web browsers, certain terminal programs support multiple tabs within the same window, which can be switched between with, typically, Alt + Number Key. Most terminals and command prompt programs also remember the history of commands, which can be revisited or cycled through by hitting the UP-Arrrow Key (or Ctrl + P if you are on Linux). Similarly, I use keyboard shortcuts to (a) move the cursor to the start/end of the line, (b) move cursor forward/backward by whole words, (c) delete rest of the word/command to the right of the cursor, (d) delete one word at a time, (c) hit Tab a bunch of times after having typed a “word” partially to either autocomplete it, or list available valid autocompletions. That Tab-completion trick is great for file-system navigation and typing out long file names. So I never find typing out commands frustrating. I can revisit a previously executed malformed command, navigate to the mistake, fix it and re-execute it with very few key presses without moving either of my wrists.

If you are new to coding, I suggest looking into IDEs. ZScript is not a complicated language, and you will never generate too many libraries and #include clauses but a good IDE can still be helpful. It’ll give you syntax highlighting, auto-indentation, code folding and fast intra- and inter-file navigation, and popup hints for function arguments/definitions and possible completions as you type them. Power-coders in the community swear by VSCode and have some extant packages available to configure it for ZScript. Someone even made a ZScript syntax highlighter for notepad++. I myself use Emacs on Linux. Text search/navigation and mass-text manipulation, managing multiple buffers and sub-windows, running a Bash terminal within Emacs, and setting it up to open *.zsc files in c-mode gives me most of what I need. With a little extra work, Emacs can be turned into a full IDE. Hell, this guide was written in Emacs (in org-mode markup and then a modified ox-tufte.el script with custom css headers for export to html, but I digress).

And get a good, wired, mechanical keyboard.

Makefile

Throughout this guide, I have had you manually executing the zip command to generate the pk3 archive. You technically didn’t need to generate said archive, as GZDoom can work with whole folders. My purpose was two-fold: (a) most people, especially when new, conceptualize a mod as a single, share-able file. And (b) I didn’t want to bring up temporary backup files that programs like UDB, SLADE, and text editors (even Emacs) generate when working on files, which can duplicate code and throw errors. So a tool like zip was used to generate the archive with just the necessary, base files. However, the command kept getting longer and more complicated as we added more files and subfolders to the project. While you could create a shell-script or a *.bat file with the zip command in it, and run that every time, a better option is to use a makefile.

Linux comes built-in with a command line tool called make. On Windows, there are a bunch of routes to get it working. So I never run zip on my doom modding projects. After I make additions/changes to the code and assets, I run the make command from the project-folder location. The tool then looks for a file named makefile at that location, and executes the instructions therein. To illustrate with a real-world example, take a look at the project files in one of my publicly shared mods, an IWAD template for isometric-viewpoint games in GZDoom. Its makefile has the following contents:

BUILDDIR=build
ZIP=zip
ZIPFLAGS=-r -FS
ZIPEXCLUDES=-x '**~' 'build/*' '.*' 'makefile' '**.dbs' '**.backup*' '**.bak' '**.autosave*'
ZIPTARGET=$(BUILDDIR)/$(notdir $(CURDIR)).ipk3

TARGETS=$(BUILDDIR) $(ZIPTARGET)

.phony: all debug clean
all: $(TARGETS)

debug:
  @echo $(BUILDDIR)
  @echo $(ZIPTARGET)
  @echo $(ZIPEXCLUDES)

$(BUILDDIR):
  mkdir $(BUILDDIR)

$(ZIPTARGET) : *.* */*.obj */*.lmp */*.png */*.wav */*/*.png maps/*.wad *.zc */*/*.zc | $(BUILDDIR)
  $(ZIP) $(ZIPFLAGS) $(ZIPTARGET) * $(ZIPEXCLUDES)

Without knowing much about makefile syntax, you can kind-of guess what it is doing. There are a whole bunch of variables (all capitalized) at the start. The variable BUILDDIR is set to the text-string “build”. And there is a variable named ZIPEXCLUDES that lists a bunch of regular expressions for files to exclude from the archive (including makefile and the contents of the build subfolder). And there is a variable named ZIPTARGET which is using some internal make variables like CURDIR (stands for “current directory”). With this makefile in my project’s root folder, I can type make or make all to have the build subfolder automatically created if it didn’t exist, and execute the zip command for all of those file-types in the second-to-last line (with the excludes). make also keeps track of what files have changed since it last ran, so running make twice will not regenerate the archive (unlike a zip command), and only files that have been changed will get added to the archive. I can run make clean to delete the archive in case I need a fresh one built, and I use make debug to make sure that I have assigned the variables correctly (by having them printed out). I always start with a simple makefile in all of my projects and slowly grow its complexity with the project over time. If you want, you can even have make launch GZDoom and run your archive after it builds it. Some work will be needed to hide your local file structure from the makefile if you end up sharing your source code.

Git and version control

Imagine that you were maintaining a big modding project. You’ve realized that the way you’ve set some coding structure up was inefficient. You want to rewrite it but this would require changing a whole bunch of things that could break the mod. So you would probably make backup copies of the files that you want to start changing (naming them <FILENAME>.backup01.<DATE> or something like that). Or maybe you will zip-up the whole project in its current state into a backup archive. Half-way into your modifications, your friend sends you a custom class in a ZScript file that he wants you to try. So you zip-up your current, half-finished, totally-not-working state into another backup zip-archive, unzip the old backup, and add your friend’s ZScript code to start tinkering with it. A few hours later the artist you commissioned sends you a spritesheet as promised. So you create a third backup archive and replace your placeholder sprites in the current project directory with the one the artist sent over. You will need to modify the offsets in your TEXTURES lump to make it work, so you back that file up in case you’ll need to revert it later. And pretty soon, you are juggling nearly full-sized copies of your project in various branched stages of development, with no tracking information regarding what came before what and for what reason. Now imagine if more than one person were working on the project. Do they make independent copies of each other’s work and email zip files over with strange names every day? What if they work on the same file at the same time? How do you imagine that Microsoft is developing Windows?

This is a very old problem in software development, and people have figured out how to programmatically track these things. The technique is called “version control”. There are many version-control software to choose from, but the most popular one these days is called git. It was written by Linus Torvalds (the guy who invented the Linux kernel). Though once it became public, lots of people have since worked on it. The software runs on your computer and can manage your project’s development versions over time.

You can install a GUI version of git and learn to use that, but since I am old-school, I use the command line on Linux systems. So I will be showing the commands that I execute to use git. When I start a new project, be it a GZDoom mod, or a tutorial/guide like this website, I first create a project folder. And inside this empty folder, I run the command:

git init

This creates a hidden subfolder named .git (the “.” prefix is deliberate and not a typo). This is where git will store all of its content for this project. The contents of .git should never be messed with manually (in fact, don’t even look into that folder!). Only the program git is allowed to look inside and change things. After this, I create a hidden file named .gitignore. This file contains regular expressions for the types of files that git should not consider as part of the project (i.e. to be saved). Since I use Emacs, I know that any file I work on gets a temporary backup file created with the same name and a tilde “~” suffix. So the first line in my .gitignore is always:

**~

with more lines added later over time. The double “**” handles recursive subfolders. After this I begin working on the project proper like normal. I create new files and modify old ones. When I am done for the day, or I just want to save the progress made so far before trying some crazy idea, from the terminal I run:

git status

This will print a report of any new files that aren’t being track by git (i.e. newly created) and are also not matching any expression in .gitignore. It will also print a list of files that have been modified since the last “commit”. I use:

git add <FILENAME>

to add the new files if there are any, and then I commit the changes with the command:

git commit -a -m "<SOME MESSAGE ABOUT THIS COMMIT>"

The flag “-m” lets me enter the message for this commit in the command line (otherwise, git will open a text editor and ask me to type the commit message there). Technically, I also add the flag “-S” to the commit command to cryptographically sign my commits, but that is beyond the scope of this guide. This saves the current state of the project.

The next day, I run the command:

git checkout

to “checkout” the latest commit into the project folder before I start working on it. At the end, I check git status again and commit before calling it a night. At any time, if I am not happy with the changes that I have made but don’t know how to safely undo all the changes that have been accumulating, I can use git checkout -f to checkout the latest commit again (the “-f” means “forced, as in allow git to overwrite files). This will replace all the files in the project folder with the ones from that commit. I can checkout an earlier commit instead of the latest one too. To view older commits (and their commit messages) I type:

git log

This shows the entire commit history with hashes and commit messages (I type “Q” to exit the mode). Then I can use:

git checkout <LAST-FEW-CHARACTERS-OF-THE-COMMIT-HASH>

to checkout a specific commit from the past (throw in a “-f” to overwrite current changes). To view all the changes (additions and deletions) that I have made to all the text files in the project since the last commit, I run the git diff commmand. I can even check the difference between any two commits using their hashes that way.

So far, we have seen a linear, chronological chain of commits. But I can even branch my commits to try crazy experiments. And I can even merge branches back into main (and that branching and merging history is all stored by git). A long-running, complex project may have several abandoned/stale branches that had been created for various experiments and tests over time.

All of this might seem like too much to memorize, but it becomes second nature after a while. Most people starting out use a git cheatsheet. Much like learning the hotkeys for UDB, you start with a small set of 5-6 commands, practice them a bunch until you master them, and then learn one new command at a time and incorporate it into your workflow. Or you could just use the GUI. But it is good to get familiar with terms like commit, checkout, status, branch, merge, etc. just to be aware of what is possible to do.

Remote repositories

The commit history (which is stored in a very efficient, differences-only format by git) is collectively called a “repository” (or “repo” for short). The real power of version control starts to shine when you start using remote repositories. Meaning storing your project on a remote server that is accessible from any computer connected to the internet. That doesn’t necessarily mean that it is public, as you can have a private remote repo that requires a password or a cryptographic key. Famous public git repo servers that offer free accounts are Bitbucket, Gitlab, and the one this website is hosted on: Github. These are private companies (Github was acquired by Microsoft a while ago) that don’t have anything to do with the open-source software called git, though their employees might be developers. You could get a private repo going on Github if you get a paid account.

You can view the repo hosting this website by visiting https://github.com/dileepvr/gzdoom_modding_101. It is a barebones repo with very few files (the README.org file is a symbolic link to index.org) and one subfolder for images. The .gitignore file has only one line in it. To view the commit history with the dates, messages, and the last few characters of the hashes, click on “commits” below the green “code” button, or simply visit this link. This is just a linear chain with only the “master” branch, but you can checkout this repository and view earlier versions of the guide to see how it has evolved over time.

So to work on this website, my workflow is only slightly different from the one above. When I start, I run:

git pull
git checkout

before I begin. The git pull command pulls all the changes that the remote repo as accrued (very useful if you have multiple collaborators). After I am done for the day, I run the commit command, and then run:

git push

to push my latest commits to the remote repo. When I start working on it from, say, one of my other computers (like the laptop while traveling), I can git pull the changes that I pushed from my desktop earlier and continue working. I just need to remember to git push after I am done! If I want to start working in it on an entirely new computer that didn’t have the project repo to begin with, I run:

git clone git@github.com:dileepvr/gzdoom_modding_101.git

to clone the repo on my local machine, and then start cracking. You can clone any publicly available repo, by the way. You just won’t be able to push changes to it if you don’t have write-access.

The Github docs page contains instructions for starting a new repo. You can either create it on the website and clone locally, or start git init on the local machine and then push the first commit to your Github account. You may either use the website’s GUI, the git program’s GUI, or the command line to manage remote repos. You don’t have to push everything you commit either. You can have local branches that don’t get pushed to the remote repo (useful for clean, collaborative projects). When trying to merge two branches that have diverged too much, git can report a “merge conflict” and will create diffs inside files, giving you the opportunity to resolve the conflicts by making choices about which conflicting blocks of text to keep or discard. But that is an advanced topic.

The isometric-camera IWAD template that I mentioned earlier is also hosted on Github. This one has a “license.txt” file in addition to a “readme.org” file. Its commit history is also linear with no branches, but it has a couple of “tags” named “v1.0” and “v1.1” that I’ve designated as “Release candidates” using Github’s facility for the same. These were particularly stable commits that I was happy with. There is a “Releases” tab in the right-panel on the repo’s home page. The .gitignore file in this repo has a few more lines to cover the backup files created by programs like UDB and Slade.

Lastly, GZDoom itself is hosted on and actively developed on Github. It has over a hundred branches and tags (some of which are release versions). You can click on the “master” pull-down button to the top left in the home page to see some of the branch names. In the right-panel, you can see a list of the major contributors, some of whom can push to the repo directly. They have the keys to the engine source and make the major decisions. In the top, there is a tab for “Issues.” This is where anyone can report bugs or make feature requests that is visible to the public (and anyone can contribute to the discussion). Right next to it, there is a tab called “Pull Requests”, where contributions from third-party people like myself (who can’t push to the repo directly) are staged before they are either accepted or rejected. This is a Github feature (not a git feature), and other repo servers have similar collaborative tools. The work flow is:

  1. “Fork” the GZDoom repo.
  2. Clone the fork on my local machine.
  3. Start a new branch and commit some work.
  4. Push the branch to my forked repo.
  5. Create a “pull request” to the main GZDoom repo on Github’s page.

At the time of writing, there are 581 forks of GZDoom. Most of them are likely just experiments from the past that have been abandoned by various people. But engine dev is sort of irrelevant to modding. I caught the GZDoom repo home page a few hours after one of my minor pull requests (“PR” for short) had been accepted. So the latest commit briefly showed my name:

gzdoom_github.jpg

Seeking help and forum/discord etiquette

There is little point in getting into GZDoom modding if you are not also going to get involved with the modding community. The GZDoom community is only a subset of the greater Doom community. But you can ask for help with general concepts (like mapping) pretty much anywhere. GZDoom-specific queries (like anything to do with modern ZScript) should be saved for specific circles. For general help where the wiki falls short, you can try the Doomworld forums, or the subreddit r/DoomModDevs. Naturally, these communities have their own set of rules. Forums will enforce strict subforum topic relevance. Subreddits will have a flair-tagging mechanism for the same. This is meant to keep all records neatly organized and searchable for future help-seekers. Doomworld will very likely have a “make five or ten replies to other threads before being allowed to start your own” rule.

For GZDoom circles, at the time of writing, the two active places are the ZDoom forums and the ZDoom Discord. The Discord server is way more live and active, but you will find the same people (including me, come say hi!) in both places. Discord can be such a time-sink, in fact, that Graf Zahl (the creator and lead developer of GZDoom) has deleted his Discord account, and only posts on the forum (and occasionally comments on Github). The forum is where you would post stuff that you want the search-engines like Google to crawl. So if you are posting some helpful assets or templates (more on that in the next section), that would go in the Resources subforum. But you can’t create a post until after you’ve made over five replies in other threads, to prove that you are sincere. You can be banned from the forum, so don’t be a dick! And do a thorough search for the issue you are looking to get help on. It is possible that someone else has had the same problem years ago and has already made a post about it. The forums have their own Tutorials subforum.

The Discord ban hammer sees a lot more action. They get scammers, bots, and people stalking or harassing other community members by creating multiple accounts. This screenshot was from a few days ago on a GZDoom-adjacent Discord server that gets the sentiment across. Do you see the problem here:

discord_ban_saga.jpg

Leaving or muting Discord servers is trivial. And a returning user would open by identifying themselves by their old account. In fact, you can peruse the thread about the Discord on the forum to see a whole bunch of people banned for various reasons, begging to be let back in.

If you are new to Discord, do not repeatedly join and leave the same servers. That is seen as suspicious behavior. As soon as you enter the ZDoom Discord, you will be asked to introduce yourself in the #welcome channel. There, a moderator will give you access to all of the other channels, once they are convinced that you are not a bot. For the first few days, your name will have a green symbol next to it, so every post you make will advertise your n00b status to everyone. Here is a non-exhaustive list of channels on the GZDoom Discord:

discord_channels.jpg

There are more off screen, including some private ones that only special people (who have proven themselves) get access to. For example, you start doing engine development and making PRs, you might be invited to a private channel meant for discussing those. Assume that it is always turtles all the way up.

When you first join, it is generally a good idea to just lurk without making any posts. Observe the behavior to get an idea of the culture. Figure out who the jesters are, since it is impossible to guess the correct tone from text, and someone’s banter can easily be mistaken for an offensive retort if you aren’t familiar with their personality. Some people yap a lot and just want to have the last word. It is best to just let them have it, as long as you got the help that you were looking for. Restrain your ego, and leave others to their own. Figure out who the veterans are (it’s the people who are doing all the helping, answering questions, etc.). I’ve seen too many instances of someone new lecturing an actual engine dev about some historical Doom factoid. Often, if you need help with a specific mod, you can find that mod’s author hanging out on Discord. And be sure to thank the people who help you out.

Just make sure to not ping-spam anyone (that is when you use the @ to mention their name, or reply to their posts incessantly). And try not to send direct messages without seeking permission in one of the public channels. Be careful about asking for commissioned help on either the forum or Discord. You become a magnet for scammers. Don’t accept friend-requests from strangers, as this gives them legitimacy when interacting with others who see the friendship status (think ring of trust). If you need commissioned help, seek out recommendations and references for commissions from other trusted people or approach artists using the contact details on their portfolio page (making sure that the page is old enough and the work on it hasn’t been stolen or AI generated). All of these places have some rules about posting AI generated assets or code.

Publishing stuff and licenses

The thing you would want to publish would be the *.pk3 file, which is under-the-hood a zip archive. If your mod is very big (with lots of assets, say) and you want to avoid waiting time during loading, you could find the appropriate flags for the zip command that will create the archive without compressing it. Regardless, literally any file-sharing service (like mediafire or Google Drive) can be used to generate downloadable links. If your project is already on Github, you can tag your current/desired commit with a version number (via a git command) and use Github’s “Releases” feature in the repo website’s right panel. That is a great way to continuously update your work with patches, improvements, and bugfixes, while maintaining version history. GZDoom itself does it like this.

If you want to share with the GZDoom community specifically and want it to be find-able via a Google search (or duckduckgo or what have you), creating a thread on the ZDoom forums is a great way to go. Here, you can receive public feedback and commentary in the thread and update the main post as and when newer versions become available. People on Github generally create “Release Candidates” over there and link to it in a forum post. ModDB.com is another popular avenue for sharing, although you can’t post links there and need to upload the file to their server. Uploading your mod to multiple places instead of linking makes updating versions much more difficult. If you are taking the indie-developer persona seriously, you could create a portfolio page on itch.io and host your mods there. For example, the weapons and movement mod that I mentioned earlier when introducing launcher programs (Hellrider: Vengeful) is hosted on the current project-lead’s itch.io page. But the original mod that it is based on was announced and updated on a forum post by its former author (Endie).

If you are using another mod-author’s work in your mod, naturally you need to have their permission; either personally, or via a license file in their mod. Speaking of which, you need to include a license file (or several) in your own mod. You also need to make it clear which parts of your mod are your work and covered by the license. Creative Commons has been suggested as a good option for artwork and assets like sound/music, but is terrible for code and map files.

A permissive choice for code, if you are stuck, is The MIT License. This is short, and covers most cases. It grants permission to others to reuse your work in theirs, even for commercial profit (and they won’t owe anything back to you). And their reuse can be published by them in any newer license of their choice.

A more restrictive option for code is the GNU General Public License (GPL). You will have to choose between GPLv2 and GPLv3 (though for most purposes, the differences won’t matter to you). This is an infectious license that allows reuse and modification but doesn’t allow closing the source (in theory). Additionally, it requires anyone else using your work to publish that work under a GPL license too. This has gotten a little tricky since GZDoom itself uses a GPLv3 license. Many dev teams have released commercial, total-conversion IWADs (standalone games) and have bundled them with some modified version of GZDoom (see Selaco and Supplice (steam link), for recent examples). If non-GPL, then licensing for such projects is in a legal grey area untested by case law. Note that this wouldn’t have been a problem if they didn’t bundle a modified GZDoom executable file with their game and just released it as an IWAD/IPK3 mod instead. Be careful, especially when using other people’s work, to check if their license restricts your licensing options like GPL does. Often, you might need to package the original author’s license file with your mod and specify what all it applies to.

If you don’t want to make things this official, just use a Beerware license. It is always worth it to include a license file (and a readme file with information on what all stuff the license covers in the project) so that people who want to use your work don’t have to track you down and ask for permission directly.