General Programming Guide
This guide is meant for complete beginners without prior knowledge or experience of programming in C.
Note: Some information here is only relevant in the context of creating mods, and may not apply to other programming languages or contexts. Some details and accuracy have been sacrificed for the sake of simplicity.
Comments
Comments in C are preceded by //. Everything after // on the same line will be regarded as a comment, and ignored during compilation.
You can also use /* and */ for multi-line comment blocks like this:
// This is a single-line comment. /* This is a multi-line comment. This is the second line of the multi-line comment. This is the last line of the multi-line comment. */
You can disable lines of code by putting // at the start. With /* and */ you can comment out entire code blocks.
Bits and Bytes
Bits are the most basic units of information that can store a binary state: 0 (true) or 1 (false). A byte consists of eight bits, so it can store eight independent binary states.
Common Data Types
Data and values can be represented in many different ways (types) that determine how much space the data takes. The Sonic Adventure games were built with the Ninja SDK, which has its own names for common types in C. When you write your code, it's recommended to use the Ninja types.
Integer values can be signed or unsigned. Unsigned values have a larger range but cannot be below zero.
Non-integer values are stored with single (around 7 digits) or double (around 16-17 digits) precision.
Below is a table of commonly used types that you can use for reference.
| C type | Ninja type | Size in bits | Size in bytes | Range | Description | Example declaration |
|---|---|---|---|---|---|---|
| signed char | Sint8 | 8 | 1 | -128 to 127 | Signed 8-bit integer. | Sint8 myvalue = -5; |
| unsigned char | Uint8 | 8 | 1 | 0 to 255 | Unsigned 8-bit integer. | Uint8 myvalue = 255; |
| __int16 | Sint16 | 16 | 2 | -32768 to 32767 | Signed 16-bit integer. | Sint16 myvalue = -1000; |
| unsigned __int16 | Uint16 | 16 | 2 | 0 to 65535 | Unsigned 16-bit integer. | Uint16 myvalue = 33000; |
| int | Sint32 | 32 | 4 | -2147483648 to 2147483647 | Signed 32-bit integer. | Sint16 myvalue = -80000; |
| unsigned int | Uint32 | 32 | 4 | 0 to 4294967295 | Unsigned 32-bit integer. | Uint16 myvalue = 900000; |
| bool | Bool | 8 or 32 | 1 or 4 | Either True or False | A boolean value can only be true or false. Note that the size of the boolean type can be different depending on the environment.
For modding purposes, the C |
Bool myflag = true; |
| float | Float | 32 | 4 | 1.2E-38 to 3.4E+38 | 32-bit floating point with single precision. Can accommodate up to 7 digits after or before a decimal point.
Make sure to put |
Float myvalue = 100.0f; |
| double | Double | 64 | 8 | 1.7E-308 to 1.7E+308 | 64-bit floating point with double precision. Can accommodate up to 16 to 17 digits after or before a decimal point. | Double myvalue = 123.123456789; |
| void | Void | 32 | 4 | None | Indicates absence of a value. Used for pointers or to declare functions that don't return a value. | void MyFunction()
{ // Code } |
Note: Because of the way floating point values are stored, they cannot always be converted exactly, and you should not use exact comparisons involving float and double types. For example, 0.1f + 0.2f would not be equal to 0.3f, often you would see values such as 0.699999... instead of 0.7 etc.
Decimal and Hexadecimal Representations
When working with data, it is often useful to represent values in the hexadecimal, rather than decimal, format. In the hexadecimal format, a single digit can be from 0 to 15, with A~F representing 10-15.
| Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hexadecimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | 10 |
In the hexadecimal format, the decimal value 22 would be 16, and the decimal value 16 would be 10. To help distinguish hexadecimal values, the 0x prefix is used:
Uint32 value1 = 0xE; // 14
Uint32 value2 = 0x6E; // 110
For integer values, the decimal conversion of a hexadecimal value can be different depending on whether the value is supposed to be signed or not. For example, 0xFF00 would be 65280 if unsigned, and -256 if signed.
Floating point values of the float type consist of the sign, the exponent and the significand (mantissa) precision, which can all be stored in four bytes. It is hard to tell from the hexadecimal representation what the float's value is, so you can use an online converter such as this one, or a tool like a hex editor. However, some floats are easy to recognize when you're looking at hexadecimal data. For example, 0x00000000 is 0.0 (if it's a float), and 0x3F800000 is 1.0. Likewise, -1.0 is 0xBF800000.
Pointers
A pointer is a variable that stores the memory address of another variable. To put it simply, "pointer to myvalue" means "the location of myvalue".
Pointers are declared with the * operator:
Uint32 value = 80000; // Define a variable with an unsigned 32-bit integer value.
Uint32* ptr_value = &value; // Set the pointer 'ptr_value' to the memory address of 'value'.
To retrieve or modify the value referenced (pointed to) by a pointer, you need to dereference it using the * operator.
To retrieve the address of the value, you can use the & operator.
Uint32 value = 80000; // Define the variable.
Uint32* ptr_value = &value; // Set the pointer 'ptr_value' to the memory address of 'value'.
*ptr_value = 75000; // Change 'value' from 80000 to 75000 by dereferencing the pointer.
Uint32 newvalue = *ptr_value; // Declare a new variable and set it to the value referenced by the pointer.
Uint32 address = &ptr_value; // Declare a new variable and set it to the address of the value referenced by the pointer.
In the example above, the type of the value referenced by the pointer (Uint32) is known, so the pointer is declared as Uint32*. You can also declare pointers with void* without specifying the data type. To manipulate data referenced by void pointers, you will need to cast them to other types (see below).
A pointer with a null value is nullptr. Usually pointers are checked if they are nullptr before trying to access data they point to, because trying to access data pointed to by a null pointer would result in an error.
Arrays
Arrays are groups of items of the same type. Use square brackets to define an array of a specified type, and curly brackets for its contents:
Uint32 myarray[] = { 1000, 2000, 3000, 4000, 5000 }; // Defines an array of five unsigned integers.
Uint32 cleararray[20] = { }; // Defines an array of 20 unsigned integers that are all 0.
To access individual items (members) of an array, use the item index in square brackets after the array's name. Array indices are 0-based, which means the first item will have index 0, the second item will be 1 and so on.
Uint32 myarray[] = { 1000, 2000, 3000, 4000, 5000 }; // Defines an array of five unsigned integers.
Uint32 myitem = myarray[3]; // The unsigned integer 'myitem' will be the fourth member of the array (4000).
myarray[1] = 0; // Sets the second item of the array (2000) to 0.
When you pass an array as an argument to a function, it is treated as a pointer type.
void Function(Uint32* MyArray)
{
MyArray[2] = 0;
}
Structures
Structures (structs) are custom data types. They can consist of common data types mentioned above, or other data structs.
Let's have a look at a commonly used Ninja struct NJS_POINT3. A point consists of three floats for its X, Y and Z.
struct NJS_POINT3
{
Float x;
Float y;
Float z;
};
NJS_POINT3 mypoint = { 1.0f, 0.0f, -1.0f }; // Defines a point located at X = 1.0, Y = 0, Z = -1.
Individual items inside the struct are called members or fields. You can access individual members of a struct using the . operator.
NJS_POINT3 mypoint = { 1.0f, 0.0f, -1.0f }; // Defines a point located at X = 1.0, Y = 0, Z = -1.
mypoint.z = 2.0f; // Sets the point's Z value to 2.0.
Float x_value = mypoint.x; // Defines a separate value that equals the point's X value.
If you are accessing members of a struct via its pointer, you need to use -> instead of ..
NJS_POINT3* mypoint = (NJS_POINT3*)0x021DAC90; // Point the 'mypoint' pointer to memory at 0x021DAC90.
mypoint->z = 2.0f; // Sets the point's Z value to 2.0.
Float x_value = mypoint->x; // Defines a separate value that equals the point's X value.
Casts
Type casting means converting between data types. In mods, sometimes you need to access certain data types as if they were a different type. For example, you can divide two integers to get a result with a decimal point like so:
int value1 = 5;
int value2 = 2;
float nocast = value1/value2 + 1.0f;
float cast = (float)value1/value2 + 1.0f;
// The value of 'nocast' will be 3.0.
// The value of 'cast' will be 3.5.
Casts are often used with pointers:
void* mymem = MAlloc(4)); // Calls the function MAlloc to allocate 4 bytes of memory and set the 'mymem' pointer to the location of those bytes.
Uint32* myvalptr = (Uint32*)mymem; // Declares a pointer 'myvalptr' as a pointer to an unsigned 32-bit (4-byte) integer.
Defines
You can use #define for constants (values that don't change). For example, if you have a "maximum number" of something that is often used in comparisons, you can define it like this:
#define MAX_NUMBER 32
With this, instead of writing 32 every time you do a comparison, you can compare to MAX_NUMBER. This is also convenient when you want to change this constant without rewriting all comparisons - just change the #define line and it will reflect in all of your code.
Macros
A macro is a snippet of code that has a name. You can use macros if you have a lot of repeating code to make it look cleaner and to avoid excessive rewriting when you make edits to it. Macros are created with #define.
For example, let's look at the following code:
if (angle_x != 0)
njRotateX(NULL, angle_x);
if (angle_y != 0)
njRotateY(NULL, angle_y);
if (angle_z != 0)
njRotateZ(NULL, angle_z);
This code compares three angle variables (X, Y and Z) against zero, and if they are not zero, it calls one of the Ninja njRotate functions for either X, Y or Z. This is often used in model rendering code. However, if you have to render a lot of models and perform these rotations for each model, it quickly becomes convoluted. To simplify this code, we can set up macros to perform the check against zero and rotate accodringly:
// SHORT_ANG is another macro used in Ninja defines. It just makes sure the rotation value doesn't go over 65535 (0xFFFF).
#define SHORT_ANG(ang) ((ang) & 0xFFFF)
#define ROTATEX(m, ang) if (ang != 0) njRotateX(m, SHORT_ANG(ang));
#define ROTATEY(m, ang) if (ang != 0) njRotateY(m, SHORT_ANG(ang));
#define ROTATEZ(m, ang) if (ang != 0) njRotateZ(m, SHORT_ANG(ang));
With the use of these three macros the code becomes cleaner:
ROTATEX(NULL, angle_x);
ROTATEY(NULL, angle_y);
ROTATEZ(NULL, angle_z);
Enums
Enums are a custom data type that contains named integer constants. You can use enums for lists of different states, such as stopped, moving, finished etc. A commonly used enum is for "modes" used to program behavior of objects and enemies in SADX.
enum
{
MODE_INIT = 0,
MODE_MAIN, // = 1
MODE_DEBUG, // = 2
MODE_END // = 3
};
With this, instead of using 0, 1, 2 and 3 you can use MODE_INIT, MODE_MAIN etc.
Uint32 mode = MODE_INIT;
if (something_happened)
mode = MODE_MAIN;
Using const
You can use const to declare variables that never change once they are defined. The most common use of const is for ASCII strings, which are usually defined as arrays of const char.
const char TextName[] = "Test";
void LoadFile(const char* filename)
{
LoadText(filename);
}
LoadFile("MYFILE.TXT"); // Will call LoadText("MYFILE.TXT");
LoadFile(TextName); // Will call LoadText("Test");
Functions
A function is what makes things happen in your mod. Functions can have arguments (input) and return values (output). The function below returns a sum of two values. Its arguments are value1 and value2, both Uint32, and the return value is also Uint32.
Uint32 CalculateTwoNumbers(Uint32 value1, Uint32 value2)
{
return value1 + value2;
}
Uint32 result = CalculateTwoNumbers(3, 5); // The value of 'result' will be 8.
Functions can call other functions. There can also be functions without arguments and/or without return values. If a function doesn't return a value, void is used, and the return statement is not required. Below is a function that doesn't take any arguments, calls another function and doesn't return anything.
void PrintSomething()
{
PrintDebug("This is a test");
}
A fully specified function with code like in examples above is called a function prototype. If you want to reuse the function in a different source file (see below), you can use shortened definitions known as function declarations:
Uint32 CalculateTwoNumbers(Uint32 value1, Uint32 value2);
void PrintSomething();
Conditions (Using if and else)
For conditions, if, else if and else statements are used. The condition itself is enclosed in brackets. Here is a simple function that compares values and outputs different text based on the result of comparison.
void CompareTwoValues(Uint32 value1, Uint32 value2)
{
if (value1 == value2)
PrintDebug("Values are equal");
else if (value1 > value2)
PrintDebug("Value 1 is bigger");
else
PrintDebug("Value 2 is bigger");
}
Apart from the operators in the function above, for arithmetic comparisons you can also use "greater or equal" >=, "lower" <, "lower or equal" <= and "not equal" != .
Note that == is used instead of = to check if the values are equal. Do not use = for comparisons. Also, do not use == or != for comparing floating point values.
For boolean and integer types, you can use (!value) to check if it's false (0), and (value) to check if it's true or not zero. This can also be used with function return values:
bool value1 = true;
if (value1)
PrintDebug("Value 1 is true");
bool value2 = false;
if (!value2)
PrintDebug("Value 2 is false");
if (Function())
PrintDebug("Function returned true");
if (!Function())
PrintDebug("Function returned false");
For conditions with multiple comparisons, you can use the AND && and OR || operators:
if (value1 > 5 && value2 < 3) // The condition is true if value1 is bigger than 5 and value2 is less than 3
if (value1 > 5 || value2 < 3) // The condition is true if either value1 is bigger than 5 or value2 is less than 3
if (value1 > value2 || value3 < value4) // The condition is true if value1 is bigger than value2 OR value3 is bigger than value4
if (value1 > value2 && value3 < value4) // The condition is true if value1 is bigger than value2 AND value3 is bigger than value4
Switch Statements
If you have to compare the same variable against different values multiple times, switch statements are a great way to improve readability of your code.
Let's look at the following function:
Uint32 test_a(Uint32 a)
{
if (a == 0)
return 1;
else if (a == 1)
return 2;
else if (a == 2)
return 5;
else if (a == 10)
return 7;
else
return 0;
}
There are many comparisons of a. Instead of repeating else if, we can use a switch. Each possible value has the corresponding code in each case, and the default case is for values that have not been covered in other cases.
Uint32 test_a(Uint32 a)
{
switch (a)
{
case 0:
return 1;
case 1:
return 2;
case 2:
return 5;
case 10:
return 7;
default:
return 0;
}
}
Using switches together with enums makes the code much easier to read:
switch (mode)
{
case MODE_INIT:
return RESULT_NOTMOVING;
case MODE_MOVE:
return RESULT_MOVING;
case MOVE_STOP:
return RESULT_STOPPED;
case MOVE_END:
return RESULT_ENDED;
default:
return RESULT_NONE;
}
Using break and Fall Through Cases
In the previous examples, the function returned at the end of each case. However, often you need to continue executing the function after the switch statement. To finish the case, you add a break; statement. If you don't add it, the case will fall through, and the code will continue executing for the next case. You can combine break and fall through cases to create branching code.
Uint32 value = RESULT_NONE;
switch (mode)
{
case MODE_INIT:
case MOVE_END:
case MOVE_STOP:
value = RESULT_NOTMOVING;
break;
case MODE_MOVE:
value = RESULT_MOVING;
break;
default:
value = RESULT_NONE;
break;
}
if (value == RESULT_MOVING)
PrintDebug("Moving!");
In the example above, when mode is MODE_INIT, MODE_END and MODE_STOP the value of value will be RESULT_NOTMOVING, but when mode is MODE_MOVING the value will be RESULT_MOVING.
Using for and do-while Loops
To perform the same action on multiple variables or array members, you can use a for loop like this:
Uint32 array[10] = { };
void MyFunction()
{
for (int i = 0; i < 10; i++)
{
array[i] = i + 1; // Set the value of the array item with the index i to i + 1
}
}
The temporary i variable is used as a loop index that increases by 1 every loop (the ++ operator means +=1) until it reaches 10, which is when the loop stops. After calling the function the array members will be 1, 2, 3, 4, 5, 6, 7, 8, 9 and 10.
Instead of the ++ operator you can use -- to subtract by 1, or use +=2 to add 2, -=5 to subtract 5 etc.
The same function can be rewritten using do and while:
void MyFunction()
{
int i = 0;
do
{
array[i] = i + 1;
i++;
} while (i < 10);
}
Using Bitwise Operators
Setting and Checking Flags
The Sonic Adventure games often use 16-bit (2 byte) or 32-bit (4 byte) values to store flags. Since a flag can only be on or off, it only requires one bit of information, and a 32-bit value can store 32 independent flags. This becomes useful because you can pass all flags to functions as a single value and check or set them independently within the function.
To understand how flags work, let's have a look at the Programming mode in Windows Calculator:
The rows of 0000 represent bit fields of the current value. The value 0xF (15) has the first four bits (bottom right) set to true. If we change the value to 0xB (11), it will have bits 0, 1 and 3 as true, and bit 2 as false. If we toggle more bits on the left, the value will grow. For example, if you set the two bottom right rows of bits to 1100 1011, the value will turn into 0xCB (203). That value has bits 0, 1, 3, 6 and 7 as true, and bits 2, 4 and 5 as false. The calculator shows 64 bits, so the entire bottom row can be used to set and check bit flags in 32-bit values.
When combined with enums, bit flags are extremely useful in modding because you can pack 32 individual flags in a single 32-bit value. To use bit flags in enums, define them like this (example from the Dreamcast Conversion mod):
#define NJD_CUSTOMFLAG_UVANIM1 (BIT_2)
#define NJD_CUSTOMFLAG_UVANIM2 (BIT_3)
#define NJD_CUSTOMFLAG_WHITE (BIT_4)
#define NJD_CUSTOMFLAG_NIGHT (BIT_5)
#define NJD_CUSTOMFLAG_RESERVED (BIT_6)
#define NJD_CUSTOMFLAG_NO_REJECT (BIT_7)
To check if your value contains the specified flag, you can use the bitwise AND operator &. To set a flag, use the bitwise OR operator | with =. To remove a flag, use &= ~.
Uint32 materialflags = 0xFFFFFFFF; // All 32 bits are set to true
materialflags |= NJD_FLAG_USE_ALPHA; // Add BIT_20
materialflags &= ~NJD_FLAG_FLIP_U; // Remove BIT_18
if (materialflags & NJD_FLAG_IGNORE_LIGHT) // Check BIT_25
PrintDebug("Has flag!");
// NJD_FLAG_USE_ALPHA, NJD_FLAG_FLIP_U and NJD_FLAG_IGNORE_LIGHT are some of the common material flags used in Ninja SDK. They are defined in Ninja defines.
Bitwise Operators
Sometimes you need to combine the bits of two variables in a single value, or split a single variable's bits into multiple variables. To do that, you use bitwise operators XOR ^, AND &, OR |, or bit shifts << and >>.
Note the difference between bitwise AND & and logical AND &&, which is used in comparisons. Same for bitwise OR | and logical OR ||.
XOR
Bitwise XOR ^ is used to compare bits. If the bits are the same, the result is false (0), and if they are different, the result is true (1).
Let's say we have values a = 1 and b = 0. In that case, a ^ b would be 1. If a and b were both 0 or 1, a ^ b would be 0.
For comparing multiple bits, let's try a = 5 and b = 9. The result of a ^ b would be 12 (0xC), which is 0000 1100 in binary representation.
In binary, 5 is 0000 0101, and 9 is 0000 1001. The first four bits are 0 in both, so the comparison result for them is 0000. The following two bits are different, so the result is for them is 11. The last two bits are 0 and 1 in both, so the result is again 00. Combining the results, we get 0000 1100, or 0xC.
AND
Bitwise AND & compares bits and returns true if they are the same, and false if they are not. It is useful for checking flags as seen in the examples above, or for retrieving specific bits from a value.
The result of 0xFF0F & 0xF0 would be 0. The result of 0xFF0F & 0xF would be 0xF. ANDs with 0xF or 0xFF are often used to get the lower 4 or 8 bits of a value, e.g. 0x5AC0 & 0xFF would be 0xC0.
OR
Bitwise OR | returns true if either of the compared bits is true. It is often used to add flags or combine variables with non-overlapping bits into one.
0xAA00 | 0x00BB = 0xAABB
0xA0C0 | 0x0B01 = 0xABC1
Bit Shifts
Bit shifts are used to move bits of a variable. Bits can be shifted left << or right >>. The number after << or >> indicates the number of bits to shift.
0xFF << 8 = 0xFF00
0xAA00 >> 8 = 0xAA
You can use bit shifts to combine multiple variables into one:
Uint8 a = 0xFF;
Uint8 r = 0xAA;
Uint8 g = 0xBB;
Uint8 b = 0xCC;
Uint32 color = (a << 24) | (r << 16) | (g << 8) | b); // The value of 'color' will be 0xFFAABBCC
You can use an online bitwise calculator such as this one to check the results of bitwise operations.
Source and Header Files
Source files have a .c or .cpp extension. They contain definitions of variables and function prototypes.
Header files have an .h extension. They contain declarations of variables and functions.
In other words, source files have the actual implementation of your code, while header files contain the interface to that implementation. As such, source and header files often come in pairs sharing the same name, e.g. mod.cpp and mod.h.
Using Headers
To use the interface from one source file in a different source file, you need to #include the header. For example, to enable the use of SADX Mod Loader types, functions etc. in your mod, you need to add the line #include <SADXModLoader.h> at the top of your .cpp file.
#include can be used with angle brackets (#include <file.h>) or quotation marks (#include "file.h"). Angle brackets mean the compiler will search for the header in the include path list for your project (set up in project properties in Visual Studio). Quotation marks mean the compiler will start looking for the header in the same folder as your source file, and if the header isn't there it will look in the include path list.
You can use relative paths with #include. For example, #include "../header.h" will look for the header header.h in the parent folder of the source file, and #include "test/header2.h" will look for header2.h in the "test" subfolder.
Using #pragma once
Depending on the contents of your header, including the same header in multiple source files can lead to a compilation error. To avoid such errors, add #pragma once at the top of the header. This directive tells the compiler to only include that header once in a single compilation.
Using static Declarations
If you put static before a variable or a function, it will only be usable in the current source file. Static variables also retain their values between function calls and last until quitting the game.
static void StaticFunction();
static Uint32 myvalue = 100;
Using extern
If you define a variable in one source file and want to use it in another source, you use the extern keyword. Basically extern is the opposite of static.
Let's say your mod.cpp has a variable Uint32 myVariable = 100;. To use this variable in another source file while maintaining its value, you put the following in mod.h:
extern Uint32 myVariable;
After that, just #include mod.h in your other source file and you will be able to use the variable.
For functions, the extern keyword is not necessary because this behavior is default for functions. You can mark a function as static to change it.
Using extern "C" and __declspec(dllexport)
When your mod has a function or variable that you want to be used outside the mod itself, you need to export it.
All mod DLLs need to have Mod Loader exports in order to be recognized as mods.
For a mod to be recognized by the Mod Loader, it needs to have the SADXModInfo or SA2ModInfo exports as follows:
SADX
extern "C" __declspec(dllexport) ModInfo SADXModInfo = { ModLoaderVer };
SA2
extern "C" __declspec(dllexport) ModInfo SA2ModInfo = { ModLoaderVer };
The "C" after extern is necessary for compatibility with both C and C++ code. It allows calling C functions from C++ code.
Apart from that, your mod must export the mod's initialization (Init) function. This function is what makes your mod code start working when the Mod Loader loads it.
extern "C" __declspec(dllexport) void Init(const char* path, HelperFunctions& helperFunctions, const unsigned int index)
{
// Your code here
}
Other exports are optional. The Mod Loaders page has a list of all functions you can export. Exported functions will be picked up by the Mod Loader and executed at appropriate moments in-game.
To reduce clutter, you can use only one extern "C" and encapsulate the exported functions in curly brackets. For example, if besides the Init function you only need the OnFrame function, you can format it like this:
extern "C"
{
__declspec(dllexport) void __cdecl Init(const char *path, const HelperFunctions &helperFunctions, const unsigned int index)
{
// Executed at startup.
}
__declspec(dllexport) void __cdecl OnFrame()
{
// Executed every frame while the game is running.
}
__declspec(dllexport) ModInfo SADXModInfo = { ModLoaderVer }; // This is needed for the Mod Loader to recognize the DLL.
}
Mod Loader exports are used only once for the whole mod. You only need to put the Mod Loader exports in your mod's main source file (e.g. mod.cpp).