Preprocessor¶
Preprocessor directives are, as the name hints at, things the compiler reads and does before processing the file.
These are a series of keywords that all start with #, are all lowercase, and must be at the very start of the line (seriously, not even whitespace before them!), that the driver reads and it indicates to the compiler certain actions it needs to take, certain things that are going to be defined, the rules for how you want it to process the file, and even conditional language.
As a coder on 3Kingdoms, there is one of these you will use in every single file, one you will use in a good number of files, a handful you might never use, and a few you probably won’t ever use.
Note
Unlike actual code lines, you do not end any preprocessor directives with a semi colon.
Pragma¶
#pragma <option>
#pragma <option1>,<option2>,...,<optionN>
Pragma tells the compiler which options you wish to have it utilize when compiling. It can tell it how to handle type checking, if it should allow cloning, inheriting, or shadowing, etc.
As most of the options are not used regularly, or even frequently, we are going to focus on the important ones:
#pragma strong_types is the single most used pragma on 3Kingdoms. Every single file you write that ends in the .c extension should have this as the very first code line (comment blocks above it are fine). This tells the compiler that all functions have to be declared with a type of return and parameters.
Forgetting to do this can make your ability to debug a major headache because the compiler won’t tell you if you accidentally assign a string to an int and then your code to modify it breaks holy hell.
#pragma strict_types can be used instead of strong_types but this will turn on the highest level of type checking and you will be forced to typecast all your call_other()/ -> function calls. Its generally overkill and strong_types is sufficient.
#pragma weak_types is the opposite of strict_types. It does no type checking at all. This should really never be used as you’re just asking for trouble, but sometimes when we’re debugging old code that didn’t have any #pragma set, we’ll do this until we can get around to fixing it.
#pragma no_clone tells the compiler this file cannot be cloned, it can only be loaded as a blueprint. Useful if you want to make sure nobody ever accidentally clones a daemon for example.
#pragma no_inherit tells the compiler this file cannot be inherited. Not a lot of reasons I can think of where this would be super useful.
#pragma no_shadow tells the compiler this file cannot be shadowed. Useful on a few key system objects, but otherwise not very useful at all as 3Kingdoms generally doesn’t use shadowing.
The rest of the #pragma options are mostly used to turn on/off certain data types, runtime checks, compiler warnings, or things that are only available if the driver is compiled in debug mode. As they aren’t used, we can ignore them and you can always view the wizard man file for them if you are curious.
Include¶
#include <file>
Include tells the compiler to read a file, generally ending in ‘.h’ for ‘header file’, into this file. The compiler treats it as if that file was literally copy pasted where the #include line is, verbatim. This is useful if you have a definitions header file (often called defs.h) with common macros/routines/defines (as outlined below) that you want to copy into multiple files. You can also use this if you have a large block of text or a huge array of data that you want included in the file, but you don’t want to have to read through 500 extra lines when you look at the actual code. By using include you can separate the files physically but have the driver and compiler treat them as if they were a single file.
There are two formats for include, one which references a filepath based on the local position (meaning ./blah.h is in this folder, ../blah.h is a folder up, /inc/blah.h is in /inc/, etc), and the second is a shortcut to include any file in the global include directory.
#include "filepath_from_local_position.h"
#include <file_in_global_include_dir.h>
Generally the files in the global include dir are mostly system defined information that might be needed by several systems, but a few files in there do contain helpful macros for shortening some of your code or data that can be changed by driver configuration, and using those include macros ensures that you’re getting the valid data based on what is being used.
As mentioned above, include is useful for header files that you want to include in many places. Most wizards have a inc/ or include/ directory inside their home directory, and inside that is a defs.h file which has a ton of #define statements to shorten up how much typing they have to do, by making short defines (like TO, TP, ETP in the example above). Then there is usually a defs.h in their home directory that does nothing but #include ./inc/defs.h, and then every folder in their home dir, and every subfolder inside of those, also all have a defs.h which does #include ../defs.h. They may also contain #define code that pertains only to the folder they are in and the ones below it, like if its the top folder for an area they are coding, it might have defines that says #define MY_PATH (AREAS+"this_area/") along with #define MY_OBJS (MY_PATH+"obj/") and similar for monsters, rooms, etc.
By doing so, you can house your global defines in your inc/ dir and ensure all your code has them included, and that anything in a given folder or below has any defines that are specifically useful for just that set of code. It’s a very powerful tool.
It would look like:
./
├── inc/
│ └── defs.h //This file has all your *global* definitions for *all* your code.
├── defs.h //This file does #include ./inc/defs.h to get the global ones, nothing else.
├── areas/
│ ├── defs.h //This file does #include ../defs.h to get the global ones, and then also might define things for all your areas, like a base path for each area name.
│ ├── area1/
│ │ ├── defs.h //This file does #include ../defs.h to get the area ones, and then also might define things for just area1, like a base path for its monsters/objects/rooms.
│ │ ├── mons/
│ │ | ├── defs.h //This file does #include ../defs.h to get the area1 ones, and then also might define things for just area1 monsters.
| | ├── obj/
│ │ | ├── defs.h //This file does #include ../defs.h to get the area1 ones, and then also might define things for just area1 objects.
│ │ └── rooms/
│ │ | ├── defs.h //This file does #include ../defs.h to get the area1 ones, and then also might define things for just area1 rooms.
Define¶
#define <what> <as what>
Using #define allows you to create what is called a macro, often stylized as MACRO since they are almost always done in capital letters.
You can think of a define as a copy/paste the driver will do at compile time. If you define something called PLAYER_NAME with the value “Bob”, anywhere you type PLAYER_NAME in your code, the compiler will automatically replace it with “Bob” when it loads the file. This can also make it so you can define a macro to store the filepath of something, and then if you ever decide to move it to an entirely different folder you can change one #define and all the things that use that define for the filepath will automatically update on reload.
Where macros get really powerful is they can also represent functions and take arguments, which can allow you to really shorten down a long function call into something managable.
Here’s an example of some common define uses:
#define MY_DIR "/adalius/"
//As we are doing an operator, to ensure order of operations its always
//a good idea to wrap the right side in ().
#define MY_AREAS (MY_DIR + "areas/")
#define TO this_object()
#define TP this_player()
//Notice we can use a define if its defined ABOVE when we use it.
#define ETP environment(TP)
#define CAP(XX) capitalize(XX)
//Notice we can go over a line by using the \ operator just like strings.
#define NWRAP(XX) (XX ? (to_int(XX->query_property_args("COLS")) ? \
to_int(XX->query_property_arg("COLS"))-5:75):75)
//Ensures a number passed (XX) is between (YY) and (ZZ)
#define CLAMP(XX,YY,ZZ) ((XX)>(ZZ)?(ZZ):((XX)<(YY)?(YY):(XX)))
//You can also define something with nothing after it. The compiler will
//keep track that it was created and has no value which is useful
//in conditionals...
#define TOP_LEVEL_DEF
There are a few caveats to define, you can only reference another define inside a define if its declared before you use it. You cannot define something that is already defined as it will throw an error (how to avoid that is below in Conditionals). Defines are set at compile time so they cannot do anything dynamically other than a direct find/replace at compile time.
Preprocessor Conditionals¶
Preprocessor directives include a limited form of if/else in the form of conditionals
They are:
#if <expr>
#else <expr>
#elif <expr>
#endif <expr>
#ifdef <macro name>
#ifndef <macro name>
Unlike if/else in regular code, in preprocessor you do not encapsulate the sections with {}. Instead you start the block with one of the #if/#ifdef/#ifndef directives, and end the entire thing with #endif. Everything between those two is essentially inside a giant {}.
All the <expr> bits must be available to the driver at compile time, and whatever they evaluate to at compile time is how the if will be processed until the file is unloaded and reloaded. Consequently these conditionals are often used with empty or simply defined macros as the expression.
#ifdef and #ifndef are directly tied to #define and check whether the <macro name> is defined or not. They can be used instead of #if. They most commonly are used with something defined like TOP_LEVEL_DEF above where it just checks if its defined or not, as it doesn’t care what it is defined as.
A common problem with preprocessor directives and include files is that its easy to include a file twice in the same file by accident which can cause all the #define lines to break as they’re trying to define the same macro name twice. To get around that, include files frequently have something that looks like this:
#ifndef ___MY_INCLUDE_FILE___
#define ___MY_INCLUDE_FILE___
#define MY_MACRO1 <stuff>
#define MY_MACRO2 <stuff>
...
#endif
What will happen now is the first time the include hits that file, it will define an empty macro ___MY_INCLUDE_FILE___ (we use a weird name to ensure nobody else accidentally uses it in a different file), then it will define the other macros since #ifndef was true at the start. When the code accidentally tries to include the same file later on, ___MY_INCLUDE_FILE___ is already defined, so the entire block is skipped and nothing is defined twice by accident.
The code between the #if/ifdef/ifndef does not need to be preprocessor code however. It can be regular code. For instance, if we have a file that we want to print debug information but only when we’re in debug mode, we could do this:
#define DEBUG 1
int my_func(int a)
{
#if DEBUG
print("The value of a is: " + a + "\n");
#endif
a *= 10;
#if DEBUG
print("The value of a is: " + a + "\n");
#endif
return a;
}
When you run the program, you will see the value before and after it multiplies it. Once the code is ready for live, you simply change #define DEBUG 1 to #define DEBUG 0 and those print statements will be completely ignored by the compiler as if they were never there.
Real examples of these are all over the mudlib, but a familiar example would be priest code. When a holiday occurs, a macro is toggled from 0 to 1 indicating which holiday it is, for example XMAS, and then the spell emotes properly load the Christmas ones when the file is reloaded, and will continue to do so until XMAS is set back to 0. This is more efficient than using an actual variable because the compiler skips all the non-relevant code, so it is never even loaded into memory. Using a regular variable and if() would mean all possible emote strings would be loaded into memory even though only one set would be used.
Predefined¶
The driver used by 3Kingdoms also defines a bunch of things at the driver level which can be access by any file in the mudlib, including ones you write. Most of these are not useful for any standard coder as they generally relate to very high level things (like is support for a certain feature turned on, is TLS supporting OPENSSL, what version is the driver, etc).
That said, there are a few that sometimes can be helpful.
If you write your own debugging code inside your file, __FILE__ will always give the name of the compiled file, __LINE__ will give the current line, __FUNCTION__ will report the function name it is located in, __DIR__ will give the directory of the file, __PATH__(n) will give the directory path without <n> trailing elements.
Complex code sometime will also reference __MAX_EVAL_COST__ which is the maximum cycle time of any running code thread, __RESET_TIME__ is the default number of seconds between object resets, __CLEANUP_TIME__ is the same for object cleanup time.
There’s also __INT_MAX__, __INT_MIN__, __FLOAT_MAX__, __FLOAT_MIN__ which are values tied to exactly what they say.
There’s a whole bunch more and you’ll likely never use any of these unless you’re really digging deep in something, but the important part is they are there.