Handbook
Handbook
Handbook
Page 1
Contents
Hotkeys....................................................................................................3 Preprocessor Directives.........................................................................21
Window Mode.......................................................................................... 3 Default Preprocessor Definitions...........................................................23
Rendering................................................................................................ 3 Engine Script Functions.........................................................................25
Simulation Time Control...........................................................................3 A general note on fixed point numbers..................................................25
Audio Volume.......................................................................................... 4 Basics & math functions........................................................................26
Game Resolution.....................................................................................4 String Access / Manipulation..................................................................32
Miscellaneous functions on the F keys....................................................4 Flag Registers.......................................................................................33
Save States............................................................................................. 5 Stack Access......................................................................................... 34
Performance............................................................................................ 5 Memory Data Processing......................................................................34
Memory View........................................................................................... 5 Persistent Data Access..........................................................................35
Side Panel............................................................................................... 5 Raw Data Loading.................................................................................36
Debug Keys............................................................................................. 5 Game Resolution...................................................................................37
Built-in Debugging Tools..........................................................................6 Input....................................................................................................... 37
Memory View........................................................................................... 6 Miscellaneous........................................................................................ 39
Side Panel............................................................................................... 6 Debugging............................................................................................. 40
Lemon Script............................................................................................8 Control Flow.......................................................................................... 41
Direct VRAM Access..............................................................................42
About the Lemon Script Language..........................................................8
Emulated VDP (Video Display Processor).............................................42
Script Feature Level.................................................................................8
Color operations....................................................................................45
Identifiers................................................................................................. 9
Rendering.............................................................................................. 47
Data Types.............................................................................................. 9
Sprite Rendering (new functions)..........................................................52
Variables................................................................................................11
Sprite Rendering (legacy functions).......................................................57
Constants.............................................................................................. 12
Audio Playback......................................................................................58
Constant Arrays.....................................................................................12
Modding................................................................................................. 60
Control Flow..........................................................................................14
Undocumented......................................................................................61
Memory Access.....................................................................................16
Registers...............................................................................................17 Rendering...............................................................................................62
Functions...............................................................................................18 Render Queues Overview.....................................................................62
Includes.................................................................................................20 Palette Sprites vs. Component Sprites..................................................62
Page 2
HOTKEYS
Window Mode
• Alt + Enter = Toggle fullscreen / window mode
• Shift + Alt + Enter = Switch between different full-integer multiples of game resolution
Rendering
• Alt + F = Previous filtering method
• Alt + G = Next filtering method
• Alt + H = Next frame sync method
• Alt + B = Change background blur setting
• Alt + 1 … Alt + 8 = Toggle rendering of layer:
◦ Alt + 1 = Plane B, no priority
◦ Alt + 2 = Plane A, no priority
◦ Alt + 3 = VDP sprites, no priority
◦ Alt + 4 = Custom sprites, no priority
◦ Alt + 5 = Plane B, with priority
◦ Alt + 6 = Plane A, with priority
◦ Alt + 7 = VDP sprites, with priority
◦ Alt + 8 = Custom sprites, with priority
◦ Note that certain rendered objects like custom planes and debug draws can‘t be toggled
Page 3
• Numpad-1 = Normal game speed
• Numpad-2 = Fast-forward 3x game speed (with Ctrl: 2x game speed)
• Numpad-3 = Fast-forward 5x game speed
• Numpad-4 = Fast-forward 10x game speed
• Numpad-7 = Fast-forward at maximum speed
• Numpad-5 = Slowmotion 0.2x game speed (with Ctrl: 0.5x game speed)
• Numpad-6 = Slowmotion 0.05x game speed (with Ctrl: 0.01x game speed)
• Numpad-9 = Rewind one frame
• Numpad-0 = Pause simulation
• Numpad-Period = Single-frame step
• Ctrl + Numpad-Period = Continue at normal speed until the next watch triggers or something gets logged (also Shift + Numpad-Period)
Audio Volume
Increase or reduce the global audio volume:
• Numpad-Plus = Increase global volume
• Numpad-Minus = Reduce global volume
Game Resolution
Change the horizontal game resolution:
• Numpad-Divide = Reduce width
• Numpad-Multiply = Increase width
Page 4
• F3 = Rescan for connected controllers
• F4 = Switch controls for player 1 and player 2
• F10 = Reload resources (sprites etc.)
• F11 = Reload scripts
Save States
• F5 = Go to the Save State menu
• F8 = Go to the Load State menu
• F7 = Quick load last loaded state - without changing current speed (so this can be used while single-stepping with Numpad-Period)
Performance
• Alt + P = Switch between performance displays
◦ Warning: Don‘t leave the first one (with the flickering box) on for a longer period of time - like more than some minutes -, as it isn‘t
too healthy for some LCD screens
Memory View
Keys I, O and L control the Memory View, see section on this Tool.
Side Panel
Keys Home and End control which Side Panel tab is shown.
Debug Keys
Numbers 1, 2 …, 9 and 0 on the main keyboard can be read as Debug Keys by the scripts, see script binding "Key0".
E.g. for Sonic 3 A.I.R., key 1 disables camera bounds, and key 0 toggles a debug output for certain collision boxes.
Page 5
BUILT-IN DEBUGGING TOOLS
Memory View
The Memory View is a simple output of a part of memory. It allows you to investigate both ROM and RAM data, as well as the extended RAM.
The data is shown in hexadecimal form, like in a hex editor. The leftmost text in each line is the address of the first byte shown.
Side Panel
The Side Panel displays various types of debug information arranged in several tabs.
Note that many items in there act as clickable buttons, to show or hide additional information.
Also, the side panel can be made smaller or larger by dragging its left border.
C = Call Frames
• Lists all functions called in the last simulated frame.
• They are ordered by their call hierarchy by default, but you can also sort them alphabetically or by source file name.
• For doing optimizations, profiling samples can be shown as a rough estimate how much script code got executed in each function.
Page 6
U = Unknown Addresses
• In case simulation encountered jumps or calls to unknown addresses, these are listed here.
W = Watches
• Here you can find the active debug watches, i.e. memory locations whose changes are tracked during script execution.
• Watches with one or more changes in the last frame are highlighted. You can click on each change to get a call stack that shows where
exactly the change happened.
• Note that you can add watches by using the "Global Defines" tab, or with the "debugWatch" script function.
G = Global Defines
• Shows all script defines with a fixed memory address, i.e. all global variables in memory.
• You can investigate on their values and set debug watches on them here.
R = Rendered Geometry
• Gives an overview over all planes, sprites, etc. that was rendered in the last frame.
• In most (but not all) cases, hovering your mouse over an entry highlights its bounding box in the viewport.
V = VRAM Writes
• This is some kind of persistent debug watch, specifically for the emulated video memory.
• Lists all writes to VRAM that got triggered in the last frame by the scripts.
L = Log
• In addition to the usual log output on screen, all logged events are displayed here again.
• Click on a log entry to get a call stack showing where it was logged.
Additional tabs
• More tabs can be defined by scripts (see "OxygenCallback.setupCustomSidePanelEntries" in Sonic 3 A.I.R. scripts)
Page 7
LEMON SCRIPT
If you want to engage in scripting, make yourself familiar with the language first. It is somewhat similar in syntax to languages like C (or C++,
C#, Java, whatever) - but with some differences, most notably:
1. Line break handling
◦ Line breaks are regarded as end of a code line. That implies that you can‘t split expressions like function calls into multiple lines.
◦ For that reason, there‘s no need to end a line with a semi-colon. Doing so results in a compile error.
◦ Curly braces { } always have to be placed in their own lines.
2. There is only a limited number of data types that can be used, and currently no way to define additional data types.
3. Definitions of functions and global variables have to start with keywords function and global, respectively. Other definitions like
constants etc. start with their respective keywords.
Page 8
◦ The compiler checks for the correct syntax for if-clauses, namely you need to place parentheses around the condition (this was
optional before).
Identifiers
Identifiers for variables, functions, defines, etc. consist of the following characters:
'A'..'Z', 'a'..'z', '0'..'9', '_', '.'
So unlike other languages, the dot can be a normal part part of an identifier.
Lemon script is case-sensitive.
Data Types
There are only a handful of basic data types that can be used for variables and constants:
Integer types
• u8 is a 8-bit / 1-byte unsigned integer, values ranging from 0 to 2^8-1 (0 to 255)
• u16 is a 16-bit / 2-byte unsigned integer, values ranging from 0 to 2^16-1
• u32 is a 32-bit / 4-byte unsigned integer, values ranging from 0 to 2^32-1
• u64 is a 64-bit / 8-byte unsigned integer, values ranging from 0 to 2^64-1
• s8 is a 8-bit / 1-byte signed integer, values ranging from -2^7 to 2^7-1 (-128 to 127)
• s16 is a 16-bit / 2-byte signed integer, values ranging from -2^15 to 2^15-1
• s32 is a 32-bit / 4-byte signed integer, values ranging from -2^31 to 2^31-1
• s64 is a 64-bit / 8-byte signed integer, values ranging from -2^63 to 2^63-1
Page 9
Conversion from integer to floating point types can be done implicitly like this:
u32 integerValue = 123
float floatValue = integerValue // Implicit cast
For the other way round, from floating point types to integers use specialzed functions like Math.roundToInt:
float floatValue = 16.175f
s16 integerValue = Math.roundToInt(floatValue) // Round to an integer value, in this case the result is 16
Other types
• For boolean values, you can use the type bool, but note that this not an actual data type, instead only a synonym for u8. When
evaluating a value to an actual bool (e.g. in an if-clause condition), all values != 0 are regarded as true, and 0 is regarded as false.
• The void type exists, but only as return value type for functions that don‘t return anything.
Strings
Starting with script feature level 2, string is added as a proper data type for strings (before that, strings were referenced using u64
variables). A string can be casted to a u64 integer, giving you the 64-bit hash of the string, but you can't convert such a u64 hash value back to
a string.
To create a string, use string literals like "Hello World" for fixed strings, and the stringformat function to build new strings if needed.
Note that strings are always 8-bits per character, usually interpreted as ASCII.
With script feature level 2, there's a few more features available for strings:
• operator + concatenates two strings (with level 1, this only results in a garbage value)
• operators ==, !=, <, <=, >, >= compare two strings, using their ASCII representation (case sensitive)
• method-like calls like .length()
Examples:
Page 10
string part1 = "This is "
string part2 = "a full sentence."
string text = part1 + part2 // text = "This is a full sentence."
if (part1 < part2) // That's true here, as character 'T' has a smaller ASCII value than character 'a'
{
u8 length = text.length() // Returns the length of text, which is 24 characters in this case
}
Variables
You can define variables to store data. There's two kinds of variables:
• Global variables need to be defined outside of functions, using the global keyword. As the name implies, they can be accessed from
everywhere in your own scripts and also in modules compiled on top (i.e. mods using a higher mod priority).
• Local variables only exist inside the scope where they are created. This can be a whole function, or a block { } like the if-clause's if-
block and else-block.
Examples:
global u8 winCounter // Defines a global variable named "winCounter" of type u8; needs to be placed outside a function
s32 offset // Defines a local variable named "winCounter" of type s32; needs to be placed inside a function
string characterName // Defines a local variable named "characterName" of type string; to be placed inside a function
Variables by default have a value of 0 for integer data types, false for bool variables, or an empty string in case of string variables.
Page 11
In case of local variables you can directly assign them as value like this:
s32 offset = 60 // Define one variable with a fixed initial value
s32 anotherOffset = offset * 2 // Define another one whose initial value is going to be twice of whatever offset is
// when that line gets executed
Constants
Unlike variables, which can be assigned new values all the time, constants are meant to be fixed values that can't change at run-time at all.
To declare a constant, use this syntax:
constant <type> <name> = <value>
With these placeholders:
• <type> is a data type, which can be any integer type, bool and string
• <name> is the name if the constant and must be a valid identifier (the same rules as for variable names apply)
• <value> is the value of the constant, which needs to fit with <type>.
Examples:
constant u16 SCREEN_HEIGHT = 224
constant string WELCOME_MESSAGE = "Hey diddly ho neighborino!"
Constant Arrays
For creating a fixed (in terms of size and content) lookup table or a similar list of constant values, you can make use of constant arrays:
constant array<<type>> <name> =
{
<values>
}
Page 12
Here, placeholders <type>, <name> and <values> have these meanings:
• <type> is the data type of the values in the array; allowed are all the same types as for constants
• <name> is the name of the array, following the usual rules for identifiers
• <values> is a comma-separated list of values, which can be multi-line if needed
• Note that in the syntax definition above, the <> characters around the type are literally the characters < and > and not part of
placeholders like <type> etc.
For small constant arrays, the definition may optionally be written a one-line, too:
constant array<<type>> <name> = { <values> }
Some examples:
constant array<u32> PRIMES = // A lookup of the first 10 prime numbers, no idea where that's useful
{
2, 3, 5, 7, 11, 13, 17, 19, 23, 29
}
Just like with constants, you're free to define constant arrays either in global scope, or locally inside a function if needed only in there.
Page 13
To get the number of values in a constant array, you can use the array's .length() method right after the constant array name, like this:
u32 numCharacterNames = CHARACTER_NAMES.length()
string lastCharacterName = CHARACTER_NAMES[CHARACTER_NAMES.length()-1] // Now index 3 is not hard-coded any more
Control Flow
Lemon script allows for the following control flow statements.
Note that the inner statements there can themselves be control flow statements like an if clause or one of the loop statements below.
Also note that in the syntax examples, <single-statement> / <statements> , <condition>, <initialization> and <incrementation> are just
placeholder to be replaced with your own code.
If-clause
Executes a statement or block only if a given boolean condition is met.
The syntax is similar to many C-like programming languages. For a single conditional statement:
if (<condition>)
<single-statement> // This must not be exactly one statement; if you place more, only the first is executed conditionally
You can add one or more statements to be executed if the condition is not met by adding an else part:
if (<condition>)
{
<statements>
}
else
Page 14
{
<statements>
}
While-loop
Executes a statement or block an arbitrary number of times, as long as a given condition is met.
Here again, a C-like syntax is used:
while (<condition>)
{
<statements>
}
Just like for the if-clause, you can also have a single statement instead of the block.
Inside a loop, you can use the break and continue statements:
• break exits the loop immediately and continues script executing after it
• continue restarts the loop, including an evaluation of the condition
Page 15
For-loop
Executes a statement or block, as long as a given condition is met, and does an increment step after each loop.
for (<initialization>; <condition>; <incrementation>)
{
<statements>
}
Note that:
• Both break and continue are possible in there as well, with continue executing the incrementation before doing the condition
evaluation.
• Each one of <initialization>, <condition> and <incrementation> can either be a single statement or just empty. If the condition is
empty, the for-loop will loop without condition and can only be exited using break.
Memory Access
To access a piece of memory (including ROM and RAM), use a basic data type followed by the address in square brackets. This can be used for
both reading and writing data.
Example: u16[0x012345] would access the bytes at addresses 0x012345 and 0x012346.
Some notes:
• Memory access is always little endian, just like the Mega Drive's / Genesis' M68K processor.
• Unlike with the M68K, it's allowed to access odd memory addresses with all data sizes, not just u8 and s8.
• The address can be constant or calculated at runtime like in u32[myAddressVariable + 4]
Page 16
• Memory access can use only the following data types: u8, s8, u16, s16, u32, s32, u64, s64 - but not string
The total mapped memory space is limited to 24-bit addresses (0x000000 to 0xffffff), memory accesses only use the lower 24-bits. This
means that addresses 0xffff1234 and 0xff1234 are identical.
Inside the memory space, not all addresses are valid to access. The following are:
• 0x000000 .. 0x3fffff = ROM -- this is where the game ROM content is stored
• 0x800000 .. 0x8fffff = "Shared memory", actually additional RAM
• 0xff0000 .. 0xffffff = RAM – for game ports like Sonic 3 A.I.R., this is mostly used the same way as by the original game
Registers
The registers a0 .. d7 of the M68K are represented in lemon script by the global variables A0 .. D7.
These have some special sub-variables that allow for partial access. Here‘s an overview using D0 as example:
• D0 -> This is the full 32-bit register, interpreted as unsigned by default
• D0.u8 -> Lowest 8 bits, interpreted as unsigned 8-bit value
• D0.s8 -> Lowest 8 bits, interpreted as signed 8-bit value
• D0.u16 -> Lowest 16 bits, interpreted as unsigned 16-bit value
• D0.s16 -> Lowest 16 bits, interpreted as signed 16-bit value
• D0.u32 -> Lowest 32 bits, interpreted as unsigned 32-bit value (same as D0)
• D0.s32 -> Lowest 32 bits, interpreted as signed 32-bit value
Page 17
Functions
Function definitions follow this syntax:
function <return-type> <name>(<parameters>)
{
// Here comes the code to be executed when the function is called
}
The function keyword always goes first, followed by the return value - which can be void if the function should not return anything.
Next is the function name, which needs to follow the rules for identifiers (see above).
And finally a list of parameters. This can be empty () if no parameters are needed, otherwise it's a comma-separated list of parameters, each
consisting of the parameter data type and the parameter variable name.
Examples:
function void myTestFunc() // Defines a function named "myTestFunc" with no return value and no parameters
{
// Do stuff here
}
function s16 getNiceNumber(bool negative) // This function returns a u16 integer and takes a boolean as input
{
// Just an example; returning either -42 or 42 depending on the input
if (negative)
return -42
else
return 42
}
function void addPlayer(string name, u16 age, u32 highscore) // An example with multiple parameters
{
// This example makes use of the functions "debugLog" that prints a text on the screen, and "stringformat" to build a string
Page 18
debugLog(stringformat("Player %s has highscore %d", name, highscore))
}
Functions with actual return types (not void) need to return a value on each possible path of execution.
Function Overloading I
It's possible to create multiple versions of the same function with different parameters. When resolving calls, the compiler will choose the
most fitting version of the function.
Function Overloading II
You can even overload a function with the very same parameters and return type. This is particularly useful if you're making a mod that wants
to replace a function from the vanilla game with one that does things differently.
In such a case, your overloaded function might want to call the original version of the function at some point of execution. Maybe because you
don't need to replace it all, but only add some code before or after the original version is executed. For such cases, just call the original
function by its name with an additional prefix base. - including the dot.
function void helloWorld() // Overloaded version - usually not directly below, but somewhere in your mod
Page 19
{
debugLog("Doing stuff before the original")
base.helloWorld()
debugLog("This is done after the original")
}
Address Hooks
Functions can have so-called address hooks. This is an associated ROM address for the function, to allow for indirect calls using addresses
with the "call" keyword, like in call 0x123456 + D0.u16 .
Whenever such a call is executed, the function with the resulting address's hook will get called. Or none at all, if there is no function with that
hook, leading to the call having no effect.
Address hooks are declared with a line like this preceding the function header:
//# address-hook(0x123456) end(0x123478)
Here, the first curved brackets include the address for this function. The "end" part afterwards is optional and has only informative meaning
for functions directly representing original M68K code.
Includes
There‘s usually only a single script file that gets loaded as a starting point, e.g. a "main.lemon" file.
In order to add more script files to compilation, include lines like the following examples are used:
include myfunctions → includes the contents of a script file named "myfunctions.lemon"
include utils/helpers → includes the contents of a single script file "helpers .lemon" in a subdirectory named "utils"
include entities/player/? → includes the contents of all script files in a nested subdirectory "entities/player"
The paths used in includes are always relative to the including script file‘s location.
Note that the wildcard ? can only be used to include all *.lemon files in a directory, but not for additional filtering or recursive includes of
subdirectories.
Page 20
Preprocessor Directives
Before evaluating the actual contents of a script, the compiler performs some preprocessing on the script - especially filtering out parts of
the code that aren't meant to be compiled, e.g. because they require a newer or older game version.
There's a few preprocessor directives you can use to control that.
#define <name>
#define <name> = <value>
Creates a new preprocessor definition, or if one with that name already exists, overwrites its value.
If no value is explicitly assigned, the value will be 1.
Examples:
#define HAS_EUKAS_AWESOME_MOD // Sets the definition HAS_EUKAS_AWESOME_MOD to value 1
#define AWESOME_MOD_VERSION = 0x105 // Sets the definition AWESOME_MOD_VERSION to value 0x105
Note: Do not confuse the #define preprocessor directive with the define keyword (without #) that is used in the Sonic 3 A.I.R. scripts to
declare shorthands for ceratin expressions. These are really two entirely different things (though both are derived from preprocessor
defines / macros in the C programming language, hence the same naming).
#if <condition>
#else
#endif
Checks whether the condition evaluates to anything different from 0. If so, the scripts between #if and #endif (or #if and #else) are included
in the compilation. If the condition evaluates to 0, the scripts in between are filtered out and handled as if they were empty lines.
The condition may include:
• Preprocessor definitions. These include the ones listed under "Default Preprocessor Definitions" below, as well as custom ones
defined by the script module itself or other modules that were compiled before (via #define). If a preprocessor definition is unknown,
it evaluates to 0 automatically.
Page 21
• Operations between preprocessor definitions and constants.
Examples:
#if HAS_EUKAS_AWESOME_MOD
// Code in between here is compiled if the HAS_EUKAS_AWESOME_MOD preprocessor definition is set
#endif
Note: You can also filter out other preprocessor directives, e.g. create a preprocessor definition via #define only when a certain condition is
fulfilled.
However, this only works in game versions from February 2022 on. For compatibility with older game versions, you can resort to including a
different script file conditionally.
For example:
#if GAMEAPP >= 0x22020100 // This encodes the date of Feb 1, 2022
include my_new_definitions_file
#endif
where a different script file - in this case named "my_new_definitions_file.lemon" would have the #define directives (which by the way also
require a game version from at least February 2022).
#error "<error-message>"
Makes script compilation fail intentionally and print a message to the player.
Example:
#error "Something is wrong! Also, this message is not helpful :(" // Let's hope your error message is better than that
Page 22
This can be useful if you're making a script mod that depends on a different script mod to be loaded first. Given that the other mod created a
preprocessor definition that you can check against, you can output an error if that definition was not found.
For example:
#if !HAS_EUKAS_AWESOME_MOD
#error "Euka's Awesome Mod is required. Make sure to activate it, using a lower mod priority."
#endif
STANDALONE
In the Sonic 3 A.I.R. game scripts, you might stumble across usage of the STANDALONE preprocessor definition. This one is always set to 1.
GAMEAPP
The GAMEAPP preprocessor definition makes the difference between the Oxygen Development Application ("OxygenApp.exe") and the actual
game application ("Sonic3AIR.exe").
Unlike STANDALONE, its value is not 1, but a version code of the game executable, so you can do comparisons like #if GAMEAPP >= 0x21010800 if
e.g. you need to include a piece of script code for only the newer game versions that support it.
For Sonic 3 A.I.R. the value of GAMEAPP is something like 0x21010800 (this example representing version 21.01.08.0) – you can get the actual
value from the "GameAppBuild" entry in the game‘s metadata.json.
Page 23
In conclusion:
• Game application: GAMEAPP = 0x21010800 STANDALONE = 1
• Oxygen dev app: GAMEAPP = 0 STANDALONE = 1
Page 24
ENGINE SCRIPT FUNCTIONS
Page 25
Basics & math functions
s8 min(s8 a, s8 b)
u8 min(u8 a, u8 b)
s16 min(s16 a, s16 b)
u16 min(u16 a, u16 b)
s32 min(s32 a, s32 b)
u32 min(u32 a, u32 b)
s64 min(s64 a, s64 b)
u64 min(u64 a, u64 b)
float min(float a, float b)
double min(double a, double b)
• Returns the smaller value of inputs a and b.
s8 max(s8 a, s8 b)
• Returns the larger value of inputs a and b.
• Also exists for types u8, s16, u16, s32, u32, s64, u64, float, double.
s8 clamp(s8 a, s8 b, s8 c)
• Limits the value in a into the range defined by b and c; the result is:
◦ b if a <= b
◦ c if a >= c
◦ a if it‘s between b and c
• Also exists for types u8, s16, u16, s32, u32, s64, u64, float, double.
u8 abs(s8 value)
• Returns absolute value of the input, i.e. negates it if it‘s negative.
• Also exists for types s16, s32, s64, float, double.
Page 26
u32 sqrt(u32 value)
• Calculates the square root of the input value, rounded down to the next integer value.
Page 27
float Math.exp(float x)
double Math.exp(double x)
• Evaluates the exponential function for the input value x.
float Math.log(float x)
double Math.log(double x)
• Evaluates the natural logarithm for the input value x.
Page 28
float Math.atan2(float y, float x)
double Math.atan2(double y, double x)
• Evaluates the atan2 function, which can be used to get an angle value out of a 2D vector.
• The output is in radians.
Page 29
float Math.round(float value)
double Math.round(double value)
• Round the input value to the closest integer (a fraction of .5 is rounded up), but return it as a floating point value.
Page 30
◦ If factor is 1.0, this returns b
◦ If factor is between 0.0 and 1.0, it returns the interpolated value between a and b
◦ factor can also be outside the 0.0 to 1.0 range, for linear extrapolation
Page 31
bool Math.isNaN(float value)
bool Math.isNaN(double value)
• Returns whether a floating point value is NaN, which is the result of diving zero by zero.
Page 32
◦ %d for integer parameter, output as decimal number
◦ %x for integer parameter, output as (unsigned) hexadecimal number; note that this will not add any prefix like "0x"
◦ %b for integer parameter, output as (unsigned) binary number; note that this will not add any prefix
◦ %02d or %02x for integer output (decimal or hexadecimal, respectively), and an enforced minimum number of digits to be filled up
with zeroes; replace the 2 with any other single digit
◦ Use %% if you like to output an actual percent character %
• This function supports up to 8 additional value parameters.
Flag Registers
Note: The internal flag registers of the engine – Zero and Negative Flag – only exist for easier compatibility of scripts with original M68K code.
Use the following functions only where needed, and avoid using them in your own new script code.
bool _equal()
• Returns true if the Zero Flag is set.
bool _negative()
• Returns true if the Negative Flag is set.
Page 33
void _setNegativeFlagByValue(s8 value)
void _setNegativeFlagByValue(s16 value)
void _setNegativeFlagByValue(s32 value)
• Set the Negative Flag by the given value. It will be set if the signed input value is below zero, and cleared if it is zero or positive.
Stack Access
Similar to the flag register functions, this is mostly for compatibility with M68K code.
Avoid using these, as there are usually better ways of temporarily saving values (namely using local variables) or passing values between
functions.
u32 pop()
• Returns the topmost value from the stack and removes it there.
Page 34
void fillMemory_u8(u32 startAddress, u32 bytes, u8 value)
void fillMemory_u16(u32 startAddress, u32 bytes, u16 value)
void fillMemory_u32(u32 startAddress, u32 bytes, u32 value)
• Fills a part of memory by writing multiple copies of the given value there.
Page 35
Raw Data Loading
bool System.hasExternalRawData(string key)
• Returns true if there is a raw data file registered under the given key string.
u32 System.loadExternalRawData(string key, u32 targetAddress, u32 offset, u32 maxBytes, bool loadOriginalData, bool loadModdedData)
• Same as above, but with more parameters.
• Parameters offset and maxBytes determine which part of the raw data to load. You can set maxBytes to zero to load everything
starting at offset.
• Parameters loadOriginalData and loadModdedData can limit raw data loading to keys defined as part of the game itself or a mod for
that game, respectively.
Page 36
Game Resolution
u16 getScreenWidth()
• Returns the width in pixels of the simulated game screen.
u16 getScreenHeight()
• Returns the height in pixels of the simulated game screen.
u16 getScreenExtend()
• Returns the additional pixels on each side of the screen for widescreen display.
• This is calculated as:
(getScreenWidth() - 320) / 2
Input
u16 Input.getController(u8 controllerIndex)
• Returns a controller's input bitmask, with each bit representing a button or d-pad direction.
u8 Input.buttonDown(u8 index)
• Returns true if the button with the given index is held down at the moment.
• Index ranges from 0x00 to 0x0f for player 1‘s input. Add 0x10 to query player 2‘s input instead.
u8 Input.buttonPressed(u8 index)
• Returns true if the button with the given index was just pressed this frame, i.e. is now held down but wasn‘t before.
Page 37
void Input.setTouchInputMode(u8 mode)
• Enables a specific handling for touch input on mobile devices, depending on the input value:
◦ mode = 0: Hide touch controls overlay, no touch input is possible
◦ mode = 1: Hide touch controls overlay, but when the next touch gets released, this creates a button press of the Start button
◦ mode = 2: Default behavior, touch controls overlay is shown if no controller / keyboard is used
• Warning: Use modes 0 and 1 only if needed and only very temporarily, as especially mode 0 can easily softlock the game
Page 38
Miscellaneous
u32 System.rand()
• Returns a random u32 number.
• Usage of this function breaks determinism of scripts, which can make debugging much harder. E.g. game recordings including a call to
„System.rand“ won‘t work properly and input recordings won‘t produce the same results each time.
• For that reason, if you need random number, better rely on a script-implemented pseudo-random number generator.
Page 39
Debugging
Log = ...
• Not a function actually, but some kind of global variable. Though it can only be written to.
• This is a quick way of logging an integer value (up to 64-bit) onto the screen.
• Usage example: Log = D0.u16
• Note that one one logged value per script line that uses Log will stay visible on the screen. So when using this, you might want to step
frame by frame, or run until a logging occurs.
Page 40
void debugDumpToFile(string filename, u32 startAddress, u32 bytes)
• Save parts of memory to a file. The filename parameter must be a string.
• The file will be created in the game‘s working directory, which is usually the executable directory.
Control Flow
void yieldExecution()
• Signals the end of the current frame. The script will continue after this call in the next frame.
• Best practise: Call this only in a single script function that acts as a general function ending a frame. When modding a game, you
usually wouldn‘t use this at all.
Page 41
void System.setupCallFrame(string functionName, string labelName)
• Creates a new entry on the call stack, at a label inside the given script function.
• The parameters must both be a strings, the function name (see above), and the name of the label.
Page 42
u16 VDP.readData16()
• Read a u16 (i.e. 2 bytes) from VRAM / VSRAM / CRAM at the address previously defined with one of the calls above.
• The address will be increased by the current write increment in each call, so multiple calls to this function read from different
addresses each time.
u32 VDP.readData32()
• Same as VDP.readData16, but reads a u32 (i.e. 4 bytes).
• Note that internally, this is two calls to VDP.readData16, so write increment is applied once in between and once afterwards.
Page 43
void VDP.copyToCRAMbyDMA(u32 sourceAddress, u16 vramAddress, u16 bytes)
• Copy memory from the given address (in RAM, extended RAM, or ROM) into CRAM.
• This is a more handy version of the VDP.setupVRAMWrite + VDP.copyToCRAM combination.
Page 44
void VDP.Config.setPlayfieldSizeInPixels(u16 width, u16 height)
• Defines the size in pixels of plane A and plane B.
• Use only powers of two, between 256 and 1024 pixels for width and height.
void VDP.Config.setPlaneWScrollOffset(u16 x, u8 y)
• Set plane W scroll offset.
Color operations
The following functions are used to convert between the usual RGB (red, green, blue) color representation and the HSV (hue, saturation, value)
color space. (Not to confuse with the slightly different HSL color space.)
Page 45
u32 Color.fromHSV(float hue, float saturation, float value)
• Builds a RGB color value from hue, saturation, lightness
◦ hue is between 0.0f and 360.0f (it repeats automatically if outside that range), and is basically an angle on a circle of colors:
▪ 0.0f is red
▪ 120.0f is green
▪ 240.0f is blue
▪ 360.0f is red again, after a full rotation
◦ saturation can be anywhere between 0.0f and 1.0f and defines the ratio between gray and color
▪ 0.0f is just a gray value between black and white, depending on the value parameter (hue has no influence)
▪ 1.0f is the color as defined by hue
◦ value can be anywhere between 0.0f and 1.0f and defines how much color / brightness is used
▪ 0.0f is black
▪ 1.0f is the full mix between color and gray value, depending on hue and saturation
Page 46
Rendering
Page 47
• This function needs to be called in each frame again, otherwise will only have an effect for a single frame.
void Renderer.setupPlane(s16 px, s16 py, s16 width, s16 height, u8 planeIndex, u8 scrollOffsets, u16 renderQueue)
• Define a custom plane for a particular rectangle on the screen.
• Parameters:
◦ px, py, width, height define the rectangle on screen
◦ planeIndex refers to: 0x00 = plane B non-prio layer, 0x01 = plane A non-prio layer, 0x02 and 0x03 = plane B / A priority layers
◦ scrollOffsets is the index of one of the scroll offset set to use, usually 0x00 (plane B offsets) or 0x01 (plane A offsets), but can as
well be a custom set (0x02 and 0x03)
◦ renderQueue defines when this plane will get rendered in relation to other planes and sprites
• This function mainly exists to allow for more obscure things like e.g. rendering plane A with different scroll offsets on the upper and
lower parts of the screen.
Page 48
void Renderer.resetCustomPlaneConfigurations()
• Removed previously defined custom planes and restores the default planes again.
void Renderer.resetSprites()
• Clears the list of sprites to be rendered.
• This is meant to be done once per frame before starting to draw sprites again. However, there might be situations where the last
sprites shown should just remain, like just fading out the last image – in this situation, do not call this function and do not draw any
new sprites.
void Renderer.drawVdpSprite(s16 px, s16 py, u8 encodedSize, u16 patternIndex, u16 renderQueue)
• Draws a sprite like the VDP does on original Mega Drive / Genesis hardware (VDP using its data in the sprite attributes table = SAT).
• Parameters:
◦ px and py is the position on screen. Note that this is using actual screen coordinates (without the 0x80 offset preset in the SAT)
◦ encodedSize is a byte of the format 00WW00HH, where WW is the width and HH is the height between 00 (1 pattern = 8 pixels) and
11 (4 patterns = 32 pixels)
◦ patternIndex is an encoded reference to the patterns to use for drawing; same format as in SAT
◦ renderQueue defines when this sprite will get rendered in relation to other sprites, rectangles, texts, and the planes
void Renderer.drawVdpSpriteWithAlpha(s16 px, s16 py, u8 encodedSize, u16 patternIndex, u16 renderQueue, u8 alpha)
• Same as Renderer.drawVdpSprite, but supporting an additional alpha transparency value (0xff for full opacity).
void Renderer.drawVdpSpriteTinted(s16 px, s16 py, u8 encodedSize, u16 patternIndex, u16 renderQueue, u32 tintColor, u32
addedColor)
• Same as Renderer.drawVdpSprite, but supporting a multiplicative tint color and an additive color
• Both color parameters use the RGBA format, as a hexadecimal number this looks like 0xRRGGBBAA (red, green, blue, alpha)
Page 49
u64 Renderer.setupCustomUncompressedSprite(u32 sourceBase, u16 words, u32 mappingOffset, u8 animationSprite, u8 atex)
u64 Renderer.setupCustomCharacterSprite(u32 sourceBase, u32 tableAddress, u32 mappingOffset, u8 animationSprite, u8 atex)
u64 Renderer.setupCustomObjectSprite(u32 sourceBase, u32 tableAddress, u32 mappingOffset, u8 animationSprite, u8 atex)
u64 Renderer.setupKosinskiCompressedSprite(u32 sourceAddress, u32 mappingOffset, u8 animationSprite, u8 atex)
• Creates a custom sprite from the ROM or RAM.
• These functions are somewhat game-specific, see the existing scripts for examples.
void Renderer.addSpriteMask(s16 px, s16 py, s16 width, s16 height, u16 renderQueue, u8 priorityFlag)
• Add a mask that hides all sprites with a lower render queue.
void Renderer.addSpriteMaskWorld(s16 px, s16 py, s16 width, s16 height, u16 renderQueue, u8 priorityFlag)
• Same as Renderer.addSpriteMask, but uses world space coordinates.
void Renderer.setViewport(s16 px, s16 py, s16 width, s16 height, u16 renderQueue)
• Reduce the viewport to a smaller portion of the screen.
void Renderer.setGlobalComponentTint(s16 tintR, s16 tintG, s16 tintB, s16 addedR, s16 addedG, s16 addedB)
• Define the global tint color and added color, which will get applied to component sprites.
• For tintR/G/B, a value of 0x100 would be a neutral value that does not introduce any change to the colors.
• For addedR/G/B, these are values to be added to the respective 8-bit pixel channel value, after applying tint. You can make use of
negative values here as well, to darken component sprites on the screen.
void Renderer.drawRect(s16 px, s16 py, s16 width, s16 height, u32 color, u16 renderQueue, bool useWorldSpace)
• Draw a colored rectangle to the screen.
• Parameters:
◦ px and py define the upper left position of the rectangle.
Page 50
◦ width and height make up the rectangle's size.
◦ color is the color of the rectangle, using 0xRRGGBBAA (red, green, blue, alpha) format. Using an alpha value smaller than 0xff
makes the rectangle semi-transparent.
◦ renderQueue defines when this rectangle will get rendered in relation to other sprites, rectangles, texts, and the planes.
◦ useWorldSpace = true interprets px/py as coordinates in the world, otherwise screen coordinates (in pixels from the upper left
corner) are used.
void Renderer.drawRect(s16 px, s16 py, s16 width, s16 height, u32 color, u16 renderQueue, bool useWorldSpace, bool
useGlobalComponentTint)
• Same as above, but allows for automatic fading of the rectangle (using the global component tint color) if useGlobalComponentTint is
true.
void Renderer.drawText(string fontKey, s32 px, s32 py, string text, u32 tintColor, u8 alignment, s8 spacing, u16 renderQueue, bool
useWorldSpace)
• Draw text to the screen.
• Parameters:
◦ fontKey is the name of the font to use, e.g. "smallfont". This is the font's file name without path or extension.
◦ px and py define the anchor position where to draw the text.
◦ text is the actual text string to draw.
◦ tintColor is the color to apply when rendering the text, using 0xRRGGBBAA (red, green, blue, alpha) format.
◦ alignment can be one of the following:
▪ 1: Left / top aligned text (i.e. the anchor position at px, py is the upper left corner of the text)
▪ 2: Horizontally centered / top aligned text
▪ 3: Right / top aligned text
▪ 4: Left aligned / vertically centered text
▪ 5: Horizontally and vertically centered text
▪ 6: Right aligned / vertically centered text
▪ 7: Left / bottom aligned text
▪ 8: Horizontally centered / bottom aligned text
Page 51
▪ 9: Right / bottom aligned text
◦ spacing can be used to add additional space between characters. It's measured in pixels and can also be negative. Use the default
value of 0 for standard spacing.
◦ renderQueue defines when this text will get rendered in relation to other sprites, rectangles, texts, and the planes.
◦ useWorldSpace = true interprets px/py as coordinates in the world, otherwise screen coordinates (in pixels from the upper left
corner) are used.
void Renderer.drawText(string fontKey, s32 px, s32 py, string text, u32 tintColor, u8 alignment, s8 spacing, u16 renderQueue, bool
useWorldSpace, bool useGlobalComponentTint)
• Same as above, but allows for automatic fading of the text (using the global component tint color) if useGlobalComponentTint is true.
Here's an example:
// First create the sprite handle, assign it to a variable (here named spr)
SpriteHandle spr = Renderer.addSpriteHandle(key, px, py, renderQueue)
Page 52
// Now set different properties of sprite rendering
spr.setFlipX(true) // Render the sprite mirrored horizontally
spr.setRotation(45.0f) // Rotate the sprite by 45° (around its center point)
spr.setOpacity(0.75f) // Render the sprite slightly transparent, with only 75% opacity
Page 53
SpriteHandle: void setFlipY(bool flipY)
• Draw the sprite mirrored vertically if the parameter flipY is true
SpriteHandle: void setTransform(float transform11, float transform12, float transform21, float transform22)
• Set a custom transformation matrix instead of using the setRotation / setScale variants
Page 54
◦ space == 0: Use screen coordinates (this is the default)
◦ space == 1: Use world coordinates (see setWorldSpaceOffset)
Page 55
SpriteHandle: void setTintColor(float red, float green, float blue, float alpha)
◦ Set a tint color, or RGB component multipliers
▪ Having all multiplier values set to 1.0f has no effect
▪ Values in the range 0.0f to 1.0f for red, green, blue darken the respective channel, effectively tinting the sprite
▪ Values in the range 0.0f to 1.0f for alpha will draw your sprite in a semi-transparent way (you can also use setOpacity for that)
▪ For red, green, blue, you can also use multiplier values outside of that range, to intensify them with values above 1.0f, or even
inversing them with values below 0.0f
▪ In the latter case, consider using the setAddedColor to bring the resulting RGB values back into the usual valid 0.0f to 1.0f range of
color component values
Page 56
Sprite Rendering (legacy functions)
void Renderer.drawSprite(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue)
void Renderer.drawSprite(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, u8 angle, u8 alpha)
void Renderer.drawSpriteTinted(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, u8 angle, u32 tintColor, s32 scale)
void Renderer.drawSpriteTinted(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, float angle, u32 tintColor, float scale)
void Renderer.drawSpriteTinted(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, u8 angle, u32 tintColor, s32 scaleX, s32 scaleY)
void Renderer.drawSpriteTinted(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, float angle, u32 tintColor, float scaleX, float scaleY)
• Draw a custom / modded sprite.
• Parameters:
◦ key is a string referencing a custom sprite loaded from a PNG or BMP file, or it‘s the result of a Renderer.setupCustom*Sprite call
◦ px and py denote the position where to draw the sprite on the screen
◦ atex is an offset added to palette indices inside palette sprites (not used for component sprites loaded from PNGs)
◦ flags is a combination of:
▪ 0x01 = flip horizontally
▪ 0x02 = flip vertically
▪ 0x08 = use pixel upscaling, instead of the default Scale3x
▪ 0x10 = draw fully opaque (only meant for performance optimization, when the sprite has no transparent pixels at all)
▪ 0x20 = interpret px/py as world space coordinates (see setWorldSpaceOffset) instead of a screen position
▪ 0x40 = draw sprite with priority flag
▪ 0x80 = ignore global tint and added color, even if this is a component sprite, where those would usually be applied automatically
◦ renderQueue defines when this sprite will get rendered in relation to other sprites and the planes
◦ angle is the rotation angle to draw and behaves differently depending on the used type:
▪ u8 angle is covering the full u8 range for one rotation (0 is 0°, and 0xff is just below 360°)
▪ float angle is in radians
◦ alpha is an alpha transparency value between 0 (completely invisible) and 0xff (fully opaque)
◦ tintColor can be used to change the sprites colors by channel-wise multiplication
▪ the format as hexadecimal number is 0xRRGGBBAA (red, green, blue, alpha)
Page 57
▪ a value of 0xffffffff is white with full opacity, i.e. keeping the original colors
◦ scale is a scaling factor for the sprite
▪ parameters scaleX and scaleY lead to different scaling in horizontal (scaleX) and vertical (scaleY) direction
▪ if a function variant with float inputs is used, a scale parameter of 1.0f simply is the normal size
▪ if a function variant with s32 inputs is used, they are 16.16 fixed point numbers – i.e. a value of 0x10000 would draw the sprite in
normal size
void Renderer.drawSpriteWithTransform(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, u32 tintColor, s32 m11, s32 m12, s32 m21, s32 m22)
void Renderer.drawSpriteWithTransform(u64 key, s16 px, s16 py, u16 atex, u8 flags, u16 renderQueue, u32 tintColor, float m11, float m12, float m21, float m22)
• Draw a custom / modded sprite, this time with a custom 2x2 transformation matrix.
• Parameters are the same as for the third Renderer.drawSprite variant, except:
◦ angle and scale are replaced by the four entries of a 2x2 matrix (m11, m12, m21, m22 in row-first order)
◦ when using the first variant with s32 parameters for the matrix, these are interpreted as 16.16 fixed point values
Audio Playback
Page 58
• Some sound IDs use a defined channel, which means it can only be played once at a time. Starting a new sound of a channel will stop
playback of other sounds using the same channel.
u8 Audio.getAudioKeyType(u64 sfxId)
• Checks the sfxId (audio key) for existence and whether it's modded.
• Returns either of:
◦ 0 if the audio key does not exist
◦ 1 if the audio key is emulated music and not changed by any active mod
◦ 2 if the audio key is remastered music and not changed by any active mod
◦ 3 if it's modded
Page 59
• For information on the seconds / length parameter, see Audio.fadeInChannel just above.
Modding
Page 60
Undocumented
There exist some more functions that can be used, but there‘s no documention. You can find the full list of available functions in the Sonic 3
A.I.R. scripts under "_reference/cpp_core_functions.lemon".
To get idea what they're for and how they are used, you might find results by searching for their usage in scripts.
Page 61
RENDERING
Page 62
Component sprites
• These are loaded from 32-bit PNGs.
• Component sprites are internally stored in 32-bit ABGR format, i.e. with 8 bits per red, green and blue channel and an 8-bit alpha
channel that usually will get alpha blended when being rendered.
• The game automatically applies a global tint color and an added color when rendering a component sprites. This is used for effects like
fading from / to a black or white screen. This behavior can be disabled in the draw script call, see Renderer.drawSprite.
Page 63