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: on (1) or off (0). A byte consists of eight bits, so it can store eight independent binary states.
Data Types
Values can be represented in many different ways. The Sonic Adventure games were built with 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.
| 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; |
| 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(Uint8 argument)
{ // 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.
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; // Declare the 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; // Declare the value
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).
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.0f
// The value of 'cast' will be 3.5f
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.
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 CalculateTwoNumber(Uint32 value1, Uint32 value2)
{
return value1 + value2;
}
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 CalculateTwoNumber(Uint32 value1, Uint32 value2);
void PrintSomething();
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 use of SADX Mod Loader features 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.