* Rename outputs * Makefile target renames * Add run target * yeet z64compress * venv * baserom_uncompressed -> baserom-decompressed * input rom name to baserom.z64 * Add BUILD_DIR makefile variable * Move built roms to build dir * Move baserom to baseroms folder * Add version to map file name * Makefile cleanup * Rename ldscript to include version * Multiversion build * n64-us version name * Remove venv as dependency of setup * Readme wording * extract_baserom.py suggestion * Readd checksums * Make .venv work with windows * missed an endif * Cleaner windows venv implementation * Remove duplciate process * Build process steps * Move make_options back * Fix schedule build directory * Fix schedule includes * Makefile NON_MATCHING check -> != 0 * OOT 1704 changes * Small cleanups * Missed 1 thing * UNSET -> SYMS * Update extract_baserom.py * dmadata.py * Small cleanup * dmadata_start * Format * dmadata files * Fix makefile comment * Jenkins report fix * extracted dir * Python dependencies order in readme
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.
- Schedule scripting language
- Features of the schedule system
- How a schedule script looks like
- Syntax
- Compiling
- Commands
- Formal grammar
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:
- Current in-game day.
- If a WeekEventReg flag is set or not.
- Current time is in a specified time range.
- Current time is before/after a specified time.
- Some other miscellaneous checks (see the
ScheduleCheckMisc
enum).
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_FLAG_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 anelse
, 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
#include
d 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_FLAG_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 goto
s. 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
branch
es.
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 [^\(\)]+