mm/docs/schedule_scripting_language.md
Derek Hensley 0b375cadb0
Msgevent OK (#1637)
* Decompiled, some commands still unnamed

* Use full weekevent reg names

* Fix func_8094E69C

* Fix Player_GetMask

* Eventinf, mask worn, and item action argument formatting

* decimal rupees

* Times (day, hours, minutes) as decimal

* msgEventScriptPos

* Callbacks

* Name final commands

* Enum

* Arrays

* functions

* Small cleanups

* Clean up actors

* Remove event from some variable names to make them shorter

* Some more function renames

* newline

* Missed a few unks in commands

* Update functions.txt

* PlayerMask

* MSGMODE_PAUSED

* Script time macros

* EnSob1_GetWelcome default

* Whole bunch of renames

* Format

* Add CMD to commands

* Update code_functions

* structs for size

* z64scripts.h

* MsgEventCallback -> MsgScriptCallback

* format

* MsgEvent_RunScript stop

* Remove cmd handler comment

* cmd -> script in MsgEvent_RunScript

---------

Co-authored-by: Thar0 <17233964+Thar0@users.noreply.github.com>
2024-06-14 21:35:22 -07:00

27 KiB

Schedule scripting language

The Schedule scripting language is a high level language that was made to help reading and modifying the schedule scripts used by various actors.

It should be noted that the language discussed on this document was invented and made up by the community. There's no presence of higher level abstraction on the game ROM, neither it was used by the original development team at Nintendo.

Features of the schedule system

The Schedule system is a very simple scripting language, it is composed of a series of commands that can check the state of the game and return a value depending on said state. This system can't declare variables, hold its own state or modify the game's state.

A schedule script can check the state of the following:

All of those checks act as conditional branches into some other command. The schedule system also supports unconditional branches.

The schedule script is run until a return command is executed. There are various return commands that allow different things:

  • Return none: The schedule finished without returning a value.
  • Return empty: The schedule finished without changing the previous value.
  • Return s: The script returns a value that's 1 byte long.
  • Return l: The script returns a value that's 2 bytes long (Note this is bugged and will be truncated to 1 byte).
  • Return time: Returns a time range and a 1 byte value.

Running an unknown command or branching to the middle of a command is undefined behaviour.

A low level schedule script can be compared to a multi byte assembly language, on which each command occupies 1 byte plus a variable number of arguments.

Short and long commands

Due to the commands themselves using a variable amount of bytes it is possible to do some small optimizations, like if a branch distance (which is the amount of bytes the interpreter should skip if a check evaluates to true) can fit on a signed byte then a short (noted with the _S suffix) command is used, otherwise a long version (noted with the _L suffix) of the command is used instead.

A long command uses two bytes to store the branch distance, meaning the command itself will be one byte longer.

There are no commands that use more than 2 bytes to store the branch distance.

The short and long distinction also exists for the returned value, in case the user wants to return a value that wouldn't fit on a single unsigned byte and requires an unsigned short (two bytes) instead. Please note that the vanilla built-in system interpreter has a bug on which the upper byte of a long returned value will be discarded, so please ensure your returned values always fit on the 0-255 range.

How a schedule script looks like

An extracted schedule script is just an array of raw data. A macroified version looks like this:

static ScheduleScript D_80BD3DB0[] = {
    /* 0x00 */ SCHEDULE_CMD_CHECK_NOT_IN_SCENE_S(SCENE_YADOYA, 0x21 - 0x04),
    /* 0x04 */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(3, 0x0B - 0x08),
    /* 0x08 */ SCHEDULE_CMD_RET_VAL_L(1),
    /* 0x0B */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(2, 0x20 - 0x0F),
    /* 0x0F */ SCHEDULE_CMD_CHECK_TIME_RANGE_S(21, 0, 23, 0, 0x1D - 0x15),
    /* 0x15 */ SCHEDULE_CMD_CHECK_WEEK_EVENT_REG_S(WEEKEVENTREG_HAD_MIDNIGHT_MEETING, 0x1C - 0x19),
    /* 0x19 */ SCHEDULE_CMD_RET_VAL_L(1),
    /* 0x1C */ SCHEDULE_CMD_RET_NONE(),
    /* 0x1D */ SCHEDULE_CMD_RET_VAL_L(3),
    /* 0x20 */ SCHEDULE_CMD_RET_NONE(),
    /* 0x21 */ SCHEDULE_CMD_CHECK_NOT_IN_SCENE_S(SCENE_OMOYA, 0x37 - 0x25),
    /* 0x25 */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(3, 0x36 - 0x29),
    /* 0x29 */ SCHEDULE_CMD_CHECK_TIME_RANGE_S(18, 0, 6, 0, 0x30 - 0x2F),
    /* 0x2F */ SCHEDULE_CMD_RET_NONE(),
    /* 0x30 */ SCHEDULE_CMD_RET_TIME(18, 0, 6, 0, 2),
    /* 0x36 */ SCHEDULE_CMD_RET_NONE(),
    /* 0x37 */ SCHEDULE_CMD_RET_NONE(),
};

Having these scripts as arrays like this has two major flaws:

  • The control flow is not clear at a first glance (this is especially problematic for larger scripts).
  • The branch distances are hardcoded into each command, making the script itself hard to modify.

As a solution, the high level Schedule script language uses a C-like syntax to better represent the control flow. The above script can be written as the following:

D_80BD3DB0 {
    if_scene (SCENE_YADOYA) {
        if_day (3) {
            return_l (1)
        } else if_day (2) {
            if_time_range (21, 0, 23, 0) {
                return_l (3)
            } else if_week_event_reg (WEEKEVENTREG_HAD_MIDNIGHT_MEETING) {
                return_none
            } else {
                return_l (1)
            }
        } else {
            return_none
        }
    } else if_scene (SCENE_OMOYA) {
        if_day (3) {
            if_time_range (18, 0, 6, 0) {
                return_time (18, 0, 6, 0, 2)
            } else {
                return_none
            }
        } else {
            return_none
        }
    } else {
        return_none
    }
}

Syntax

The syntax is simple, it consists on the name of the schedule script followed by a succession of commands. Some commands require arguments (by using parentheses) and some (conditional checks) can have bodies with subcommands and an optional else with its corresponding body with subcommands.

Like in C, both whitespace and newlines are not part of the language and they get ignored during compilation. At the same time, this language accepts both C and C++ styles of comments, known as block comments (/**/) and line comments (//).

Each top-level schedule script consists on its name, followed by a list of commands delimited by braces ({ and }). A schedule script must always have at least one command on its body.

A command's body is delimited by braces ({ and }). A body must follow either a conditional check command or an else, meaning top-level bodies are not allowed. At the same time a conditional check must be followed by a body. An else must be followed by either a body or another conditional check command.

Even if this language's syntax is inspired by C, there are a few key differences:

  • Commands are not separated by semicolons (;). Instead whitespace is used as separation.
  • Instead of having a single if conditional of which its parameters should evaluate to non-zero, this language uses conditional checks. These are commands that receive parameters and have a body with parameters that would be executed if the command itself evaluated to true. This commands may be followed by an else, which has its own body.
  • There's no concept of functions, variables, loops, scopes, etc.
    • Each schedule script could be seen as a function itself, but said script can't "call" another scripts.

Compiling

Compiling a high level schedule script should produce an array for each script in the file. Each array should contain a series of macros that can be #included by a C preprocessor, like in the following example:

File: build/src/overlays/actors/ovl_En_Ah/scheduleScripts.schl.inc

static ScheduleScript D_80BD3DB0[] = {
    /* 0x00 */ SCHEDULE_CMD_CHECK_NOT_IN_SCENE_S(SCENE_YADOYA, 0x21 - 0x04),
    /* 0x04 */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(3, 0x0B - 0x08),
    /* 0x08 */ SCHEDULE_CMD_RET_VAL_L(1),
    /* 0x0B */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(2, 0x20 - 0x0F),
    /* 0x0F */ SCHEDULE_CMD_CHECK_TIME_RANGE_S(21, 0, 23, 0, 0x1D - 0x15),
    /* 0x15 */ SCHEDULE_CMD_CHECK_WEEK_EVENT_REG_S(WEEKEVENTREG_HAD_MIDNIGHT_MEETING, 0x1C - 0x19),
    /* 0x19 */ SCHEDULE_CMD_RET_VAL_L(1),
    /* 0x1C */ SCHEDULE_CMD_RET_NONE(),
    /* 0x1D */ SCHEDULE_CMD_RET_VAL_L(3),
    /* 0x20 */ SCHEDULE_CMD_RET_NONE(),
    /* 0x21 */ SCHEDULE_CMD_CHECK_NOT_IN_SCENE_S(SCENE_OMOYA, 0x37 - 0x25),
    /* 0x25 */ SCHEDULE_CMD_CHECK_NOT_IN_DAY_S(3, 0x36 - 0x29),
    /* 0x29 */ SCHEDULE_CMD_CHECK_TIME_RANGE_S(18, 0, 6, 0, 0x30 - 0x2F),
    /* 0x2F */ SCHEDULE_CMD_RET_NONE(),
    /* 0x30 */ SCHEDULE_CMD_RET_TIME(18, 0, 6, 0, 2),
    /* 0x36 */ SCHEDULE_CMD_RET_NONE(),
    /* 0x37 */ SCHEDULE_CMD_RET_NONE(),
};

In the actor's C code:

#include "src/overlays/actors/ovl_En_Ah/scheduleScripts.schl.inc"

Commands

Commands are the fundamental (and almost only) building block of this language. A schedule script file must always contain at least one script, and a schedule script must always have at least one command. It's undefined behaviour if the script's control flow doesn't always lead to a return command.

To see how the command arguments work, please see the corresponding section.

Commands can be categorized in 4 major types: Conditional checks, unconditional branches, return commands and other commands.

Generics and non-generics

Schedule scripts use both short and long commands for most of the conditional checks and unconditional branches commands, making it optimal space-wise, but terrible to worry about when actually writing a schedule script from the user's point of view.

Due to this, this high level language allows for generic versions of those commands (a.k.a. suffix-less versions) that allow not having to worry about how many commands the body of a check has (and the byte length of them).

The rest of this section will mostly refer to the generic versions of the commands. The non-generic versions of each command are available to be used too, but their use is not recommended.

Command arguments

Some commands require arguments. Arguments are used by checking commands to check something specific of the state of the game (what's the current day? what's the current scene? etc.) and take a decision based on it (branch to another command).

Arguments are enclosed in parentheses (( and )) and passed verbatim to the generated output, allowing a compiler for this language to not need to recognize any identifier used in arguments, allowing to use any custom identifier (specially useful for schedule result enums). Note this behaviour may change in a future version of the language.

Conditional checks

Conditional checks commands are commands that take a decision based on the state of the game, allowing it to decide which set of subcommands to execute.

All conditional checks require an argument and a body of subcommands. A conditional check can optionally be followed by an else.

If the condition of a command is not satisfied and that command has an else, then control jumps to the body of that else. If there's no else and the condition isn't satisfied then control just falls through into the next command.

if_week_event_reg

Checks if the passed WeekEventReg flag is set, and execute the body of the command if it is set.

if_week_event_reg arguments
  • Argument 0: A WeekEventReg flag. WEEKEVENTREG_ macros are preferred.
if_week_event_reg example
if_week_event_reg (WEEKEVENTREG_61_02) {
    return_s (31)
}
return_s (30)
if_week_event_reg non-generics

if_week_event_reg_s and if_week_event_reg_l

if_time_range

Checks if the current time is between the passed time range.

if_time_range arguments
  • Argument 0: Hour component of the start time
  • Argument 1: Minute component of the start time
  • Argument 2: Hour component of the end time
  • Argument 3: Minute component of the end time
if_time_range example
// Checks if the current time is between 18:00 ~ 6:00
if_time_range (18, 0, 6, 0) {
    return_time (18, 0, 6, 0, 2)
} else {
    return_none
}
if_time_range non-generics

if_time_range_s and if_time_range_l

if_misc

Checks if the passed miscellaneous argument is true.

if_misc arguments
  • Argument 0: A value of the ScheduleCheckMisc enum.
if_misc example
if_misc (SCHEDULE_CHECK_MISC_MASK_ROMANI) {
    return_s (33)
} else {
    return_s (34)
}
if_misc non-generics

if_misc_s. Note there's no long version of this command. It is advised to minimize the amount of commands used on a ìf_misc body and else's body.

if_scene

Checks if the current scene matches the one passed as argument.

if_scene arguments
  • Argument 0: A value of the SceneId enum.
if_scene example
if_scene (SCENE_SECOM) {
    return_s (7)
}
if_scene non-generics

if_scene_s and if_scene_l

if_day

Checks if the current day matches the passed argument.

if_day arguments
  • Argument 0: The day to check. For example, passing 1 will check for day 1.
if_day example
if_day (3) {
    return_l (1)
}
if_day non-generics

if_day_s and if_day_l

if_before_time

Checks if the current time is before the time passed as argument.

if_before_time arguments
  • Argument 0: The hour component of the time.
  • Argument 1: The minute component of the time.
if_before_time example
if_before_time (8, 0) {
    return_s (19)
} else {
    return_none
}
if_before_time non-generics

if_before_time_s and if_before_time_l

if_before_time notes

This command performs the opposite check of if_since_time.

if_since_time

Checks if the current time is after or equal to the time passed as argument.

if_since_time arguments
  • Argument 0: The hour component of the time.
  • Argument 1: The minute component of the time.
if_since_time example
if_since_time (13, 0) {
    return_s (9)
} else {
    return_none
}
if_since_time non-generics

if_since_time_s and if_since_time_l

if_since_time notes

This command performs the contrary check to if_before_time.

Unconditional branches

Unconditional branches are basically the equivalent of C's gotos. They require a label to know where to branch to.

branch

Unconditionally transfer control to the passed label.

branch arguments
  • Argument 0: The label to branch to.
branch example
if_day (3) {
    if_since_time (10, 0) {
        label_0x8:
        return_none
    } else {
        branch (label_0xF)
    }
} else {
    if_time_range (10, 0, 20, 0) {
        branch (label_0x8)
    } else {
        label_0xF:
        return_s (21)
    }
}
branch non-generics

branch_s and branch_l

else

An else signals which commands should be executed in case a conditional check does not evaluates to true. The else signals this by either having those instructions inside its own body (marked by braces) or by not having those braces but being immediately followed by another conditional check command, similar to C's else if.

else example

The following two scripts are equivalent:

if_time_range (9, 50, 18, 1) {
    return_time (9, 50, 18, 1, 1)
} else {
    if_time_range (18, 0, 6, 0) {
        return_time (18, 0, 6, 0, 2)
    } else {
        return_l (0)
    }
}
if_time_range (9, 50, 18, 1) {
    return_time (9, 50, 18, 1, 1)
} else if_time_range (18, 0, 6, 0) {
    return_time (18, 0, 6, 0, 2)
} else {
    return_l (0)
}

Return commands

A return command signals the end of the script and halts its execution.

A return command allows to optionally return a value and/or a time range to the calling actor so it can change behavior accordingly.

A schedule script with a control flow that leads to no return command is undefined behaviour.

return_s

Allows to return a short (one byte) value.

return_s arguments
  • Argument 0: The value to return. It must fit in a u8.
return_s example
if_time_range (8, 0, 12, 0) {
    return_s (EN_NB_SCH_1)
}

return_l

Allows to return a long (two bytes) value.

Please note that the vanilla interpreter for the schedule scripts has a bug on which the upper byte of a long returned value will be discarded (will be truncated to a u8), so please ensure your returned values always fit in the 0-255 range.

return_l arguments
  • Argument 0: The value to return. It must fit in a u16.
return_l example
if_scene (SCENE_TOWN) {
    return_l (1)
} else {
    return_none
}

return_none

The schedule finished without returning a value. The internal hasResult member of the ScheduleOutput struct will be set to false.

return_none arguments

No arguments.

return_none example
if_week_event_reg (WEEKEVENTREG_HAD_MIDNIGHT_MEETING) {
    return_none
}

return_empty

The schedule finished without changing the previous value. The internal hasResult member of the ScheduleOutput struct will be set to true.

return_empty arguments

No arguments.

return_empty example
if_time_range (15, 50, 16, 15) {
    return_empty
}

return_time

Returns a time range and a 1 byte value.

return_time arguments
  • Argument 0: Hour component of the start time
  • Argument 1: Minute component of the start time
  • Argument 2: Hour component of the end time
  • Argument 3: Minute component of the end time
  • Argument 4: A value to return. It must fit in a u8
return_time example
if_time_range (0, 0, 6, 0) {
    return_time (0, 0, 6, 0, TOILET_HAND_SCH_AVAILABLE)
} else {
    return_none
}

Other commands

nop

No operation. Doesn't perform an action or a check.

nop arguments
  • Argument 0: Unknown meaning.
  • Argument 1: Unknown meaning.
  • Argument 2: Unknown meaning.
nop example
nop (0, 1, 2)

Operators

Operators can be applied to some commands.

Currently only one operator is allowed on the language, the not operator.

not

A not is a special kind of command that doesn't get compiled into the actual low level script, instead it changes the meaning of the check that's right next to it by inverting the logic of the check. In other words, the subcommands of a conditional check command will be executed if the check of said command evaluates to false instead of true.

A not must always be followed by a conditional check command.

not example

The following two scripts are equivalent:

if_week_event_reg (WEEKEVENTREG_51_08) {
    if_since_time (22, 0) {
        return_s (12)
    } else {
        return_none
    }
} else {
    return_s (12)
}
not if_week_event_reg (WEEKEVENTREG_51_08) {
    return_s (12)
} else {
    if_since_time (22, 0) {
        return_s (12)
    } else {
        return_none
    }
}

Labels

A label is a special kind of command that doesn't get compiled into the actual low level script, instead it is used as a marker to be used for branches.

A label is defined as an alphanumeric identifier (a to z, digits and underscores) followed by a colon (:). The colon is not considered part of the label's name.

A label must always be followed by another command that isn't another label.

Formal grammar

This section presents the formal grammar for the Schedule scripting language.

<scriptFile>            : <functionList>
                        ;

<functionList>          : <functionScript>
                        | <functionList> <functionScript>
                        ;

<functionScript>        : IDENTIFIER '{' <cmdList> '}'
                        ;

<cmdList>               : <labeledCmd>
                        | <cmdList> <labeledCmd>
                        ;

<labeledCmd>            : <command>
                        | IDENTIFIER ':' <command>
                        ;

<command>               : <conditionalCmd>
                        | <unconditionalCmd>
                        | <returnCmd>
                        | <miscCmd>
                        ;

<conditionalCmd>        : <conditionalExprBody> <else>
                        | <conditionalExprBody>
                        ;

<unconditionalCmd>      : <tokenBranch> <args>
                        ;

<returnCmd>             : RETURN_S <args>
                        | RETURN_L <args>
                        | RETURN_NONE
                        | RETURN_EMPTY
                        | RETURN_TIME <args>
                        ;

<miscCmd>               : NOP <args>
                        ;

<conditionalExprBody>   : NOT <conditionalExpr> <body>
                        | <conditionalExpr> <body>
                        ;

<conditionalExpr>       : <tokenIfWeekEventReg> <args>
                        | <tokenIfTimeRange> <args>
                        | <tokenIfMisc> <args>
                        | <tokenIfScene> <args>
                        | <tokenIfDay> <args>
                        | <tokenIfBeforeTime> <args>
                        | <tokenIfSinceTime> <args>
                        ;

<else>                  : ELSE <body>
                        | ELSE <conditionalCmd>
                        ;

<tokenIfWeekEventReg>   : IF_WEEK_EVENT_REG
                        | IF_WEEK_EVENT_REG_S
                        | IF_WEEK_EVENT_REG_L
                        ;

<tokenIfTimeRange>      : IF_TIME_RANGE
                        | IF_TIME_RANGE_S
                        | IF_TIME_RANGE_L
                        ;

<tokenIfMisc>           : IF_MISC
                        | IF_MISC_S
                        ;

<tokenIfScene>          : IF_SCENE
                        | IF_SCENE_S
                        | IF_SCENE_L
                        ;

<tokenIfDay>            : IF_DAY
                        | IF_DAY_S
                        | IF_DAY_L
                        ;

<tokenIfBeforeTime>     : IF_BEFORE_TIME
                        | IF_BEFORE_TIME_S
                        | IF_BEFORE_TIME_L
                        ;

<tokenIfSinceTime>      : IF_SINCE_TIME
                        | IF_SINCE_TIME_S
                        | IF_SINCE_TIME_L
                        ;

<tokenBranch>           : BRANCH
                        | BRANCH_S
                        | BRANCH_L
                        ;

<body>                  : '{' '}'
                        | '{' <cmdList> '}'
                        ;

<args>                  : '(' ')'
                        | '(' <args_list> ')'
                        ;

<args_list>             : <arg_elem>
                        | <args_list> ',' <arg_elem>
                        ;

<arg_elem>              : NO_PARENTHESIS
                        | <args>
                        ;

Tokens

The presented grammar expects a few tokens.

First column is the corresponding token and right is a regular expression to match said token.

IDENTIFIER              [a-zA-Z0-9_]+

IF_WEEK_EVENT_REG       if_week_event_reg
IF_WEEK_EVENT_REG_S     if_week_event_reg_s
IF_WEEK_EVENT_REG_L     if_week_event_reg_l
IF_TIME_RANGE           if_time_range
IF_TIME_RANGE_S         if_time_range_s
IF_TIME_RANGE_L         if_time_range_l
IF_MISC                 if_misc
IF_MISC_S               if_misc_s
IF_SCENE                if_scene
IF_SCENE_S              if_scene_s
IF_SCENE_L              if_scene_l
IF_DAY                  if_day
IF_DAY_S                if_day_s
IF_DAY_L                if_day_l
IF_BEFORE_TIME          if_before_time
IF_BEFORE_TIME_S        if_before_time_s
IF_BEFORE_TIME_L        if_before_time_l
IF_SINCE_TIME           if_since_time
IF_SINCE_TIME_S         if_since_time_s
IF_SINCE_TIME_L         if_since_time_l

BRANCH                  branch
BRANCH_S                branch_s
BRANCH_L                branch_l

RETURN_S                return_s
RETURN_L                return_l
RETURN_NONE             return_none
RETURN_EMPTY            return_empty
RETURN_TIME             return_time

NOP                     nop

ELSE                    else
NOT                     not

NO_PARENTHESIS          [^\(\)]+