Introduction to Tasks and Workers - X-Hax/SADXModdingGuide GitHub Wiki
What are Tasks?
Almost everything that happens or exists in the game (menus, characters, interactable objects, bosses etc.) is set up using structures called tasks (known previously as ObjectMaster
). As the game runs, it loops through a list of tasks that tell it what should happen every frame. Every time a new object is created, the game adds a task for it that remains on the task list until the object is deleted, either manually or automatically (e.g. when you leave the level where the object is). Tasks can also add or remove their own sub-tasks (child tasks), which execute together with the parent task and are removed when the parent task is removed.
Although there are several variations of the task structure, the overwhelming majority of the game's objects use the so called Elemental Task. The structure for the Elemental Task consists of the following pointers:
struct task
{
task *next; // Next task on the list (reserved)
task *last; // Previous task on the list (reserved)
task *ptp; // Parent task (reserved)
task *ctp; // Child task (reserved)
void (__cdecl *exec)(task *); // Main ("Executor") function (runs every frame when the game is not paused)
void (__cdecl *disp)(task *); // Display function (runs every frame when the game is paused)
void (__cdecl *dest)(task *); // Destructor function (runs once when the task is destroyed)
OBJ_CONDITION *ocp; // Used by SET objects
taskwk *twp; // Task Worker (see below)
motionwk *mwp; // Motion Worker (see below)
forcewk *fwp; // Force Worker (see below)
anywk *awp; // Any Worker (see below)
void *thp; // Thread (reserved)
};
When a new task is created, the game allocates memory to store the structure. There is also an option to allocate memory for additional structures used to store various kinds of data for the task. For example, a boss task might need a timer, and a task that renders a model would need to store a pointer to the model and its coordinates and rotation. The structures storing this data are called workers. The game uses a variety of workers for different purposes. Here are some examples of workers used by different tasks:
Name | Old name | Description |
---|---|---|
taskwk | EntityData1/DataPtr1 | The most common worker. Used to store position, rotation, scale, timers etc. |
motionwk | EntityData2/DataPtr2 | Stores values such as speed, acceleration, friction, angular velocity etc. |
forcewk | ObjUnknownA/UnknownA_ptr | Stores a callback function pointer, angular velocity and X/Y/Z speed. |
anywk | ObjUnknownB/UnknownB_ptr | A general purpose worker that stores 16 bytes of data. |
bosswk | EntityData1/DataPtr1 | Used by bosses, can store X/Y/Z speed/acceleration and vertex welding data. |
playerwk | CharObjData2/DataPtr2 | Stores various parameters used by playable characters. |
chaoswk | Stores camera and collision-related data, a texture list, a boss worker, timers etc. | |
colliwk | CollisionInfo | Stores additional collision data if the object uses shape (CCL) based collision. |
eventwk | Stores face morph data, shadow size and other data for characters and cutscenes. | |
shadowwk | Stores position, rotation and size for character shadow. |
Some workers can store pointers to workers of other types. There are also unique workers for some specific things - for example, Egg Hornet's jet fumes have a dedicated worker that stores texture animation data and current animation frame, Egg Viper has its own worker, Adventure Field NPCs have their workers per level etc. However, the most commonly used worker is taskwk
. The structure is set up as follows:
struct taskwk
{
char mode; // Generally used to switch between "actions" (e.g. walk, attack, disappear and so on)
char smode; // Same as above but the value is signed
char id; // Internal index
char btimer; // 1-byte timer (0 to 255)
__int16 flag; // Status flags stored in 2 bytes (may be reserved if the object has collision)
unsigned __int16 wtimer; // 2-byte timer (0 to 65535)
taskwk_counter counter; // 4-byte union field that can store a float, a pointer, an array of four bytes, two shorts etc.
taskwk_timer timer; // Same as above but all values are signed
taskwk_timer value; // Same as above
Angle3 ang; // X, Y and Z rotation
NJS_POINT3 pos; // X, Y and Z position
NJS_POINT3 scl; // X, Y and Z scale
colliwk *cwp; // Collision worker pointer
eventwk *ewp; // Event worker pointer
};
Creating Tasks
To create a new task, you need to call the function CreateElementalTask
like this:
task* obj = CreateElementalTask(LoadObjFlags, index, YourObjectLoadFunction);
LoadObjFlags
is an 8-bit integer that specifies flags which control memory allocation for workers. If you want your object to have a certain worker type, you need to include the flag bit for that worker.
enum LoadObj : __int8
{
LoadObj_MotionWk = 0x1,
LoadObj_TaskWk = 0x2,
LoadObj_ForceWk = 0x4,
LoadObj_AnyWk = 0x8,
};
So if you need your task to have taskwk
and motionwk
, your LoadObjFlags
would be 0x3, and if you want to load all workers, it would be 0xF.
For a basic interactable object you only need to set up a taskwk
, so 0x2 is sufficient in most cases.
index
is an integer value from 0 to 7. It is used to control the order in which the game processes tasks, since they are split in 8 linked lists. Here are some examples of how the game uses them:
Index | Used by |
---|---|
0 | "Master" objects that control levels, Eggman race AI in Tails' levels |
1 | "Master" level object for Perfect Chaos, skybox objects, bosses |
2 | Level objects |
3 | "Stage clear" sound effect/player action, level objects |
4 | Explosion effects, lens flare |
5 | Cutscenes, level results screen |
6 | Spindash trail and other effects |
7 | Child tasks |
In general, 2 or 3 should work just fine.