Lua is a lightweight, high-level programming language that is used, among other purposes, to create XDRV modfiles. For this purpose, Lua is a great pick for several reasons; it is easy to learn, requires minimal structuring, and is familiar to many charters in the custom chart scene. Many other rhythm games, such as NotITG, use Lua to handle mods and events. Additionally, Trackmaker is built with Lua (specifically via the LÖVE game engine).
In XDRV, modfiles work by invoking the XDRV API, sending instructions to the game to execute mods or background events. XDRV modfiles can use the full suite of Lua functionality, but only a small subset of Lua is necessary to make modfiles that are optimized, clean, and expressive. This passage will go over those basic functionalities sequentially.
In EX-XDRiVER, Lua modfiles are able to reference xdrv, a data structure containing instructions that hook into XDRV itself. These instructions are xdrv.Set(), xdrv.Ease(), and xdrv.RunEvent(), and they can be written like so to change mod values or initiate background events. For each of these instructions, different parameters must be inserted between the parenthesis, telling EX-XDRiVER exactly how to execute the mod you want. Below is a description of each instruction and the parameters that need to be passed with it.
xdrv.Set
xdrv.Set(modName, value, beatOrTime, time)
Sets the value of a moddable field at a given time.
Parameters:
modName: string
The name of the moddable field to set.
value: number
The value to set that field to.
beatOrTime: string
Whether to measure timeValue as "beat"s or "time" in seconds.
timeValue: number
The beat or time in seconds to apply the mod at, depending on what beatOrTime is.
Runs a background event. Note that each background event has different parameters, so you’ll need to check the XDRV Chart Documentation for information on background events.
Parameters:
eventName: string
The name of event to run. Available events depend on which background your chart is using!
beatOrTime: string
Whether to measure timeValue as "beat"s or "time" in seconds.
timeValue: number
The beat or time in seconds to start the background event at, depending on what beatOrTime is.
data…
Additional parameters, based on the background event you are invoking.
The ability to write and reference instructions is not exclusive to the XDRV data structure. In Lua, the actual name for these instructions is functions. Invoking one of these instructions is known as a function call. You can create your own functions within your modfile, but doing so requires a better understanding of the Lua language.
While technically, you could make wholly functional mods with just the information above, your modfile would not be very clean or optimized. Understanding a bit more Lua functionality can making writing modfiles faster, editing modfiles easier, and creating complicated mods far simpler.
In Lua, variables are declared and assigned a value using the local keyword, a variable name, the assignment operator =, and a value. There are a lot of parts to variable declaration, although most parts besides the local keyword are relatively straightforward.
In Lua, variable names can be composed of lowercase letters, uppercase letters, digits, and underscores, although the first character of a variable name cannot be a digit. Variable names are case-sensitive, meaning that beat, Beat, and BEAT are all different variables. Good convention is to make your variable names descriptive of the value they store, using camelCase or snake_case to separate words.
The assignment operator = tells the program to assign the value on its right to the variable on its left. Bear in mind that some similar symbols, such as ==, operate differently and cannot be used interchangeably.
A variable can store different types of values, including numbers, strings, booleans, and more. Variables can also be declared without being assigned a value, causing the variable to have a value of nil. Variables can also be assigned the value of nil manually. Below is a table with examples of each type of value:
Value Type
Written Examples
Number
1, 0, 7.27, -7
String
"Hello world!", "OutCirc", " "
Boolean
true, false
Nil
nil
Once a variable is defined, it’s value can be re-assigned later. Just drop the local keyword from the front of the variable assignment. Variables are not type cast in Lua. This means that a variable holding a boolean can be changed later to hold a number or string instead.
localhasUnlocked=false
-- Variable declared and assigned to false.
hasUnlocked=true
-- Variable already declared, value re-assigned to true!
hasUnlocked="fish"
-- Variable already declared, value re-assigned to "fish"!
Another useful value type in Lua is the table. A table is a collection of values that can be referred to with indices. To create a table definition, all parts to the left of = stay the same. To the right of the assignment operator, however, your table must be defined with a set of curly brackets ({} exclusively). Then, the contents of your table go inside, where each entry is separated by a comma. Tables in Lua do not need to be declared with a size; you can add as many values as you want to a table after declaration.
To reference a specific term in a table, write the table’s name followed by square brackets ([]) and the index you want to reference in the table. You can also refer to an index to add a new value to a table. Note that Lua uses 1-based indexing, which means that the first element in a numerically-indexed table has an index of 1. Like variables, referencing a non-existent index will result in a value of nil.
localfirstSnare=snareBeats[1] -- = 9
localsecondSnare=snareBeats[3] -- = 13
localnonExistantSnare=snareBeats[10] -- = nil
snareBeats[10] =81-- snareBeats[10] now exists
Tables are not limited to indexes in numerical order. You can also index elements in a table with random numbers, strings, booleans, and any other valid data types.
Operators take the values on their left and right sides (known as operands) and perform operations on them to get some new value. Lines composed of variable references, values, and operators are called expressions. Expressions are a fundamental part of coding. but do nothing on their own. When included within some action, such as a variable assignment, they can be very powerful.
In Lua, arithmetic operators allow you to do math within your code. Arithmetic operators can operate on predefined variables or new values that you define. Arithmetic expressions follow PEMDAS order of operations. You can use parenthesis to force operations happen first. The following are valid math operators in Lua:
Symbol
Name
Code Example
+
Addition
sum = 10 + 4 sum = 14
-
Subtraction
diff = 10 - 4 diff = 6
-
Negation (Urnary)
neg = -(10+4) neg = - 14
*
Multiplication
prod = 10 * 4 prod = 40
/
Division
quot = 10 / 4 quot = 2.5
%
Modulus (Remainder)
mod = 10 % 4 mod = 2 (since 10/4 = 2r2)
^
Exponentiation
pow = 10^4 pow = 10000
When using any operators, including arithmetic operators, you can use a variable as both an operand in an expression and the reference for variable assignment, allowing you increment, decrement, or scale the value.
Relational operators are operators that compare two different numbers, outputting true or false based on the values being compared. Relational expressions are evaluated from left to right, so again, parenthesis can be used to force certain operations to happen first. With that said, putting parenthesis around relational expressions typically makes them much more legible.
Symbol
Name
Code Example
<
Less Than
ans = (10 < 4) ans = false
>
Greater Than
ans = (10 > 4) ans = true
<=
Less Than or Equal To
ans = (10 <= 4) ans = false
>=
Greater Than or Equal To
ans = (10 >= 4) ans = true
==
Equal
ans = 10 == 10 ans = true
~=
Not Equal
ans = 10 == 10 ans = false
Additionally, there are 3 keywords that function as operators on booleans: and, or, and not. The and operator evaluates to true only if both of its operands are true. The or operator evaluates to true if either of its operands are true. Lastly, the not operator negates a boolean operand so that true becomes false and false becomes true. Errors can arise when order of operations is not carefully considered.
To perform string concatenation, the process of connecting two strings into one string, Lua uses the ’..’ operator. This operator can also be used to concatenate some non-string types of values, such as numbers, converting them to a string automatically.
In all languages, loops are useful because they allow programmers to apply a line of code multiple times. Rather than copy and pasting the same line over and over and adjusting the values for each iteration, a programmer can use a loop to have values be applied and adjusted automatically. For XDRV modfiles, the most useful type of loop to use is the for loop.
In Lua, for loops start with the keyword for and then are followed by 3 parameters: a variable declaration with a start value, an end value, and an increment. The for loop executes the code for each value within the range, stepping by the provided increment. If no increment is provided, then the loop defaults to incrementing by one. Lastly, the loop header is completed with the do keyword.
The contents of your for loop should be indented, and you must mark the end of your for loop’s content with the end keyword. As you see other control structures like if-else statements and functions, you will see the need to intent and add a end keyword repeat.
In Lua, you can use for loops to increment through a table! Rather than a numeric for loop structure, iterating through a table requires a for i, v in loop. This creates two variables that can be referenced within the loop: the current index i and the value at that index v.
Additionally, looping through a table requires the table be passed through an ipairs() or pairs() statement. If your table’s indexes are sequential numbers, than ipairs() can be used. Otherwise, if your table’s indexes are non-numerical or non-sequential, you need to use pairs() to ensure that no indexes are missed.
If-else statements are useful for handling logic within your modfile. Your if statement must be composed of oneif (condition) then` clause at a minimum. When the condition is true, the code contained within the block below is executed.
Following your if statement, you can have any number of elseif statements, which allow you to evaluate another condition and execute a different block of code. Note that only one code block in a if-else statement can execute.
The final statement you can add to a statement is an else statement, which functions as a catch-all. If none of the former conditions evaluate as true, the code block in your else statement will execute. In practice, if statements can be combined with other control structures to create useful logic structures. Again, all contained blocks of code should be indented, and an end statement should mark the end of the if-else. If you have multiple control structures nested within each other, you will need to indent and add an end keyword for each layer.
Writing functions is a great way to optimize your modfile by allowing code to be reused. The first line of a function definition consists of four parts: the local keyword, the function keyword, the name of your function, and parenthesis containing your function’s parameters, which are variables that you want to be able to pass to the function (or just parenthesis if no parameters are needed). This line is known as the function signature. Functions follow similar naming conventions to variables and can be composed of the same characters (lowercase letters, uppercase letters, underscores, and digits). The same naming conventions also apply to the parameters of the function.
The line following the function signature is the body of the function, which contains a block of code that is executed whenever the function is called. The body of the function can contain any variable declarations, expressions, and control structures that the programmer needs. The body of the function can also refer to the parameters defined in the signature as variables. Once again, the body of the function is indented, and its end is signified with the end keyword.
To call a function that you have previously defined, simply write the name of the function with closed parentheses after. If your function requires parameters, values for those parameters must be written inside of the parentheses. Variables can also be referenced inside of a function call’s parenthesis, in which the value corresponding to the variable will be passed to the function.
-- Checks if an element exists in a list (sequential numerical table)
-- Returns true if the element is, false if the element isn't
localfunctionCheckList(elt,list)
fori, vinipairs(list) do
ifv==eltthen
returntrue-- Return statement 1
end
end
returnfalse-- Return statement 2, catch-all
end
localhasValue=CheckList(1,{1,2,3}) -- = true
In Lua, functions can use the keyword return to return a value to the position of the function call, ending execution of the function’s code immediately. It’s typically good practice for your function to either always return a value or never return a value. If you want your function to return a value, make sure that the function returns something for all inputs.
--- Single line comment, reserved for documentation
--[[
Block comment!
]]
Throughout this passage, you probably noticed little messages in the code snippets that were prefaced by either two dashes or contained within two dashes and two square brackets. These elements are called comments, and they are very useful for annotating your code. Although EX-XDRiVER will not read your comments when it runs your modfile, comments allow you to annotate your mods with information like functionality, notes, warnings, and even checkpoints. Comments not only make your code more readable to yourself, but they also make your code more readable to others, enabling easier collaboration and cross-referencing.
Both function definitions and variable assignments use the local keyword, but the truth is that they don’t have to. The local keyword means that the scope of the defined variable or function is limited to the block it was defined in. A block is a section of code that is indented out and contained by some control structure (ex. for loop, if-else statement, and function). Blocks layer on top of each other so that code in a deeper block cannot be accessed by code in a more shallow block. Additionally, a local variable cannot be accessed by a reference that appears before its declaration.
localx=1
do
localy=2
do
localz=3
--[[ Accessible values:
x = 1
y = 2
z = 3
]]
end
--[[ Accessible values:
x = 1
y = 2
z = nil
]]
end
--[[ Accessible values:
x = 1
y = nil
z = nil
]]
In Lua, variables without the local keyword are called global variables. Global variables can be defined on any block depth and still be accessed anywhere else within the script. In practice, using global variables can result in messy, unoptimized code. Typically, you should try to avoid using global variables and instead declare local variables at the scopes they need.
That might not seem like a lot (or it might, depending on your familiarity with programming), but with that functionality alone, you can now write modfiles for EX-XDRiVER that are powerful and efficient. Lua has a lot more functionality than that described in this, however. If you’d like to learn more about Lua’s functionality, it’s best that you read through the official Lua documentation.