-
Notifications
You must be signed in to change notification settings - Fork 3
Simple Program
Previous Section: Getting Started
- Overview
- Background Knowledge
- Basic Structure
- Declaring the AI
- Adding Logic
- AI structure
- Configuration File
- Creating the Program
- Next Section
In this tutorial we will create a simple AI for SSBM. This provides a good example that incorporates most elements of the library and produces something semi-useful in the end.
To start we will create an AI that drops down from the re-spawn platform. This is pretty much the most basic move the AI could do, so it's a good introduction to this tutorial.
Note that this AI will not do anything else. Our AI will completely overwrite the default cpu player, so if no moves were given to the AI nothing would happen at all.
It is extremely important to understand variable scope and the static keyword.
Other Important Topics:
First step is to create a file called SimpleProgram.c
. This will be the source file that contains _main()
(equivalent to main
in a normal C program - see FAQ). The basic structure of this file looks like:
static bool init_run = false;
static void init()
{
...
}
void _main()
{
if (!init_run) { init(); }
...
}
Most of the mods you ever create will start with this basic structure. Initialization is primarily used for creating the heap, but there could be many other things that need to be done the first time _main
is called. In our case we just need to initialize the heap. Our initialization function will be:
#include <mml/system.h>
#include <mml/gctypes.h>
static char heap[2000];
static bool init_run = false;
static void init()
{
initHeap(heap, heap + sizeof(heap));
init_run = true;
}
initHeap takes a pointer to the lower and uppper limit of the heap. There are several ways of reserving enough space for your heap, but the simplest is just to declare your own variable in the global scope. In this case, heap
is 2000 bytes long and we pass a pointer to the beginning and end of heap
to the initHeap
function. Now any calls to malloc, calloc or realloc will allocate space in this region.
Even if you never call malloc
there are a few functions in the library that call malloc
. If you ever call one of these functions you will need to initialize the heap first.
We want the AI to live in the global scope, otherwise the AI would not be able to "remember" information from previous frames and would have to make an independent decision every frame. The declaration looks like:
#include <mml/ai.h>
static AI cpuPlayer = INIT_AI(2, FALCO | FOX | MARTH | FALCON);
The INIT_AI macro allows for simple initialization. In this case, our AI will play as port 2 and only use characters Falco, Fox, Marth, and Falcon. If the game is loaded with one of these characters on a legal stage, the AI will take over.
This is the core mechanism by which the AI makes decisions. By design, the library wraps up all functionality required to make an AI, but the user still needs to provide the rules by which the AI makes decisions. A Logic struct is a simple way to encode if-then logic. Our example looks like:
static Logic respawnLogic =
{
{&actionStateEq, .arg1.u = 2, .arg2.u = _AS_RebirthWait},
{&addMove, .arg1.p = &cpuPlayer, .arg2.p = &_mv_shortHop}
};
In order to understand that, let's break down what exactly is in a Logic
struct.
typedef struct
{
FunctionCall condition;
FunctionCall action;
} Logic;
This is the definition of the struct. Don't worry about the specifics of what a FunctionCall
is yet. Basically, Logic
is a struct that encodes if-then logic. condition
is a call to a function that returns a bool
value. action
is a call to a function that happens if condition
is true. Basically this is equivalent to the statement:
if (condition())
{
action()
}
However, encoding that in a struct provides much more flexibility and much smaller code.
The definition of FunctionCall
is:
typedef struct
{
void* function;
FunctionArg arg1;
FunctionArg arg2;
} FunctionCall;
Basically, it is a function pointer along with 2 arguments. FunctionArg
is a union of 32-bit data types that allows for multiple different data types to be the argument of a function.
typedef union
{
u32 u;
s32 s;
f32 f;
void* p;
} FunctionArg
Now that we know what is in a Logic
struct, let's re-examine our example:
static Logic respawnLogic =
{
{&actionStateEq, .arg1.u = 2, .arg2.u = _AS_RebirthWait},
{&addMove, .arg1.p = &cpuPlayer, .arg2.p = &_mv_shortHop}
};
The condition
function of respawnLogic
checks if the action state of player 2 is equal to RebirthWait
(aka waiting on the platform). The action
function of respawnLogic
adds the move "short hop" to the cpuPlayer
.
This means that every frame the following if-then statement is called:
if (actionStateEq(2, _AS_RebirthWait))
{
addMove(&cpuPlayer, &_mv_shortHop);
}
The basic structure of any program that uses an AI
is:
static void init()
{
...
}
static void loadDefaultLogic()
{
...
}
void _main()
{
if (!init_run) { init(); }
if (needLogic(&cpuPlayer)) { loadDefaultLogic(); }
updateAI(&cpuPlayer);
}
Whenever _main
is called, it first checks if in needs to run the initialization function. Then it checks if the AI
needs instructions (happens whenever the AI has no logic or moves queued up). If the AI
does need logic, it calls the loadDefaultLogic
function that gives the AI
its basic instructions. Finally, the AI
is updated - all its logic is processed and it's inputs for that frame are written to the controller.
In our case, the default logic looks like:
static Logic respawnLogic =
{
{&actionStateEq, .arg1.u = 2, .arg2.u = _AS_RebirthWait},
{&addMove, .arg1.p = &cpuPlayer, .arg2.p = &_mv_shortHop}
};
static void loadDefaultLogic()
{
addLogic(&cpuPlayer, &respawnLogic);
}
It is important to declare Logic
in the global scope, for more info see Data is Smaller than Code.
Now that our source file is complete (see here for complete file) it is time to create the configuration file for wiimake
. This file will basically be the same for each program in this tutorial. First create a file called SimpleProgram.ini
.
The first variable we need to define is our available memory regions. This tells wiimake
which addresses in can overwrite.
REGIONS =
8022887c-80228920 ; unused code
8032c848-8032c87c ; unused code
8032dcb0-8032ddb8 ; unused code
8032ed8c-8032ee8c ; unused code
80393a5c-80393c0c ; unused code
803fa3e8-803fc2e8 ; debug menu tables/strings
803fc420-803fdc18 ; debug menu tables/strings
803001dc-80301e40 ; debug menu functions
801910e0-8019af4c ; tournament mode
8040a950-8040bf4c ; unknown
This is a standard list of regions in SSBM v1.02. The debug menu data is overwritten along with tournament mode, but this gives enough room for us to work with.
Next we want to tell wiimake
about any addresses we want to overwrite (equivalent to DOL modding).
; debug menu replaces tournament mode
8022d638 = 38000006
; unlock all characters and stages, random stage select
801648c8 = 38a007ff
801644bc = 38a007ff
80173580 = 38a007ff
; default settings (pause on)
803d4a48 = 00340100
803d4a4c = 04000a00
803d4a50 = 08010100
803d4a60 = ff000000
803d4a70 = 00000000
803d4a78 = e70000b0
; spoof controllers
80376bd4 = 38000000
80376bdc = 38000000
80376bf0 = 380000d8
80376c04 = 38000001
; boot to CSS
801bfa20 = 38600002
Note that the debug menu replaces the tournament mode option, so now it is safe to overwrite tournament mode as it is no longer accessible to the user. It is also neccesary to have a "spoofed" controller plugged in to port 2 or else the AI has no way of sending input. Any other DOL mods are just for convenience.
Next we need to tell wiimake
how to compile and link our code:
SOURCES = SimpleProgram.c
LIBRARIES = ../../libmml.a
INCLUDE_PATHS = ../mml-1.0.0/include/
COMPILER_FLAGS = -std=c99 -fno-builtin -fmerge-constants -Werror
LINKER_FLAGS =
This says that are only source file is SimpleProgram.c
. It also gives wiimake
the path to the library for linking, and the path to the include
folder of the library for compiling. Finally, we can pass flags to the compiler and linker. -fno-builtin
is essential since the library overwrites many built-in functions. -fmerge-constants
helps with large constants in the .data
section of the executable (especially useful with strings). -Werror
is important since warnings are more serious when programming in this kind of environment.
The last piece of the config file is for the fixed symbols. This tells wiimake
which functions in our code we want set up branches to from the original melee source code.
FIXED_SYMBOLS =
_main 80377998 7ee3bb78
endGame 801b15cc 38800000
display 801a633c 7c7f1b78
First, _main
is injected at 80377998. This address is in between the game reading the controller and processing the input. It allows for controller overwrites to work. 7ee3bb78 is the instruction originally at 80377998.
endGame
is a function in the library. This must be placed at a point in the code which is executed when a game ends. It sets important global variables that tell the AI when it is in or out of a game. The instruction at 801b15cc tells Melee to branch to the results screen. We overwrite it with 38800000 which tells Melee to skip the results screen. Thus, we accomplish two things here.
display
is the function that loads in our output stream to the address the debug menu looks at. This way we can overwrite the debug menu strings/tables since they will never be called, and it sets up the debug menu (called by clicking tournament mode) to be replaced with our output stream. Any calls to print will put text here.
The final config file is here.
Now that the config file is complete we simply run:
wiimake [MELEE.ISO] SimpleProgram.ini
and we have created out first program! Load up this iso and enter a game on a valid stage and set player 2 to cpu Falco/Fox/Marth/Falcon. This cpu should do nothing except jump down from the re-spawn platform. See iso reference to verify that your iso was built correctly.
Now that you've gotten the hang of writing a mod, let's make something useful. The next section creates an AI that can tech.