Introduction

godot-luau-script is a GDExtension adding support for Roblox’s Luau, a variant of Lua 5.1, to Godot 4.x as a scripting language.

Priorities

This project has a few specific goals:

  • Parity with Godot’s official scripting languages (GDScript, C#)
  • Allow for (relatively) safe loading of user-generated content (UGC) including scripts
    • Sandbox “core” and “user” scripts from each other (separate Lua VMs)
    • Enforce permission levels for restricted APIs, such as networking or filesystem access
    • Limit breakage of the game by user scripts
    • Scan PCKs for unsafe script or resource access
  • Support typechecking of source files

Note that godot-luau-script was specifically built for loading untrusted UGC. As such, other languages like GDScript may be more suitable if UGC support is not needed (that is, unless you are partial to Luau as a language).

Disclaimer

This project is still in relatively early stages. Please keep in mind:

  • (Potentially major) breaking changes will happen often and whenever necessary.
  • This project will usually track the latest Godot version (including preview releases), and support for older versions is not planned.
  • This documentation may be incomplete or out-of-date. Please report an issue if this is the case.
  • Some security issues may remain. Please report an issue if you encounter one.

Building and Installation

Download

Prebuilt binaries for each tagged release can be found on the releases page.

Installation

All necessary GDExtension files are provided in bin/. It should suffice to copy the entire folder into your Godot project. After reloading the project, the extension should be loaded and you should be able to create a Luau script through the script creation dialog.

Building yourself

godot-luau-script uses SConstruct to build.

Cloning

The three dependencies this project has, godot-cpp, Luau, and Catch2, are added as submodules in extern/. As such, make sure to clone submodules when cloning this project:

  • git clone https://github.com/Fumohouse/godot-luau-script --recurse-submodules, or
  • git submodule update --recursive if the repository is already cloned.

Supported platforms

godot-luau-script supports compilation using recent versions of Clang, GCC, MinGW, and MSVC, and building for Windows, macOS, and Linux. LLVM (clang++, clangd, clang-format) utilities are preferred where possible.

Building

Ensure that a supported toolchain is installed. You will also need to install Python 3 and run python -m pip install scons to install SConstruct. If on Linux, SConstruct is also available through some package repositories.

If you are running a Python version older than 3.11, you will need to python -m pip install tomli for TOML support.

Build the project by running scons at the project root. The following flags are supported:

  • tests=yes: Build with Catch2 tests included.
  • iwyu=yes: Run include-what-you-use instead of the compiler to find potential #include mistakes.
  • generate_luau_bindings=yes: Force regeneration of any auto-generated files (see bindgen/).

Additionally, you may want to use the following flags from godot-cpp:

  • target=editor: Build for the editor.
  • target=template_release: Build for release templates.
  • use_llvm=yes: Force usage of LLVM for compilation (over GCC).
  • debug_symbols=yes: Build the project with debug symbols.
  • Run scons compiledb target=editor tests=yes to generate a compilation commands database, typically for use with a language server like clangd.

After building, the output will be present in the bin/ folder.

Build containers

podman build containers are provided in build_containers/ for reproducible build environments targeting Linux, Windows, and macOS.

Usage

Scripts should not be run outside the directory they are in.

  1. Ensure podman is installed
  2. Build images with build_images.sh
  3. Build the project with build_gls.sh - use arguments linux, windows, and macos to select which platform(s) to build

Preparing osxcross

Go to tpoechtrager/osxcross and follow an appropriate procedure for downloading Xcode and packaging the SDK on your system.

Place the output SDK (expected name: MacOSX13.3.sdk.tar.xz) in build_containers/files/.

Build was tested on Xcode 14.3.1 (SDK: macOS 13.3).

Accessing Godot APIs

The majority of builtin and Object classes are bound from Godot to Luau. A selection of utility functions from Godot are also available.

To find out what APIs are exposed, you can refer to the definitions/luauScriptTypes.gen.d.lua file after building (beware: this file is large!) or rely on autocomplete to tell you.

For the most part, you can refer to Godot’s documentation to discover the API. After learning the rules below, this should become relatively intuitive.

Renaming rules

Most names are adapted from their Godot counterparts according to the following convention:

TypeGodot CaseLuau Case
Classes and enumsPascalCaseunchanged
Utility functionssnake_caseunchanged with few exceptions
Enum valuesPREFIX_UPPER_SNAKE_CASEUPPER_SNAKE_CASE with exceptions *
ConstantsUPPER_SNAKE_CASEunchanged
Methodssnake_casePascalCase
Properties/signalssnake_casecamelCase

*: e.g. for the PropertyUsage enum, the prefix on all values is PROPERTY_USAGE_; this is removed. Additionally, if an enum name begins with a number after renaming (e.g. KEY_9 with prefix KEY_ -> 9), an N will be prepended to the name -> N9.

Accessible APIs

Globals

TypeGodot AccessLuau AccessGDScript ExampleLuau Example
Global enum@GlobalScopeEnum. namespaceMARGIN_LEFTEnum.Margin.LEFT
Global constant@GlobalScopeConstants. namespaceN/AN/A
Utility function@GlobalScope_Glerpf(0.0, 1.0, 0.5)lerp(0, 1, 0.5) *

*: This is one of the renamed functions mentioned above.

Variant and Object classes

TypeGodot AccessLuau AccessGDScript ExampleLuau Example
Variant constructors<ClassName><ClassName>.new()Vector3(0, 1, 0)Vector3.new(0, 1, 0)
Object constructors<ClassName>.new()unchangedAESContext.new()unchanged
Object singleton<ClassName><ClassName>.singleton
Static methods<ClassName>.<Method>unchangedVector2.from_angle(x)Vector2.FromAngle(x)
Instance methods<Instance>.<Method><Instance>:<Method>v1.dot(v2)v1:Dot(v2)
Member/property/signal access<Instance>.<Property>unchanged *vector.xunchanged
Keyed/indexed set<Instance>[<Key>] = <Value><Instance>:Set(<Key>, <Value>)dictionary["key"] = 1dictionary:Set("key", 1)
Keyed/indexed get<Instance>[<Key>]<Instance>:Get(<Key>)dictionary["key"]dictionary:Get("key")
Array length<Array>.size()<Array>:Size() OR #<Array>array.size()array:Size() OR #array
Array iterationfor item in <Array>:for index, item in <Array> do **
Dictionary iterationfor key in <Dictionary>:for key, value in <Dictionary> do **
Variant type operators<A> <Op> <B>/<Unary><A>unchanged ***v1 == v2unchanged
Variant/Object to stringstr(<Instance>)tostring(<Instance>)

*: Variant type properties (e.g. Vector2.x) cannot be set because Luau does not support copy on assign (as C++ and GDScript do). You must construct a new object instead.
**: Iterators do not support modification during iteration. Doing so may cause errors or for items to be skipped.
***: Some Godot operators are not supported as they do not exist in Luau. Also, == comparison is not allowed between two different types in Luau, so these operators do not work.

Notes

  • For security reasons, the only supported Callable constructor is Callable.new(object: Object, methodName: string | StringName).
  • String, StringName, and NodePath are not bound to Luau as Luau’s builtin string suffices in the majority of cases. StringName and NodePath can be constructed manually if needed (e.g. if a String type would be inferred over the other two types) by using the SN or NP global functions respectively.
    • The intention with these constructors is to use them like a prefix you would find in other languages, e.g. SN"testStringName". This is valid because of Lua’s parsing rules.
  • Some Lua types are automatically converted to Godot types when necessary:
    • {Variant} to Array
    • {T} to their corresponding Packed[T]Array types
    • {[Variant]: Variant} to Dictionary
    • string to String, NodePath, or StringName

Class Registration

Class registration allows creating custom script classes derived from Godot classes or other script classes.

Annotations

Class registration in godot-luau-script is primarily done through annotations, an extension of Luau’s syntax expressed with comments.

Annotation comments begin with three dashes in a row, and must have no code on the same line. A block of annotation comments is delimited by a completely blank line, and the code the annotation refers to is on the line after the last comment line.

Annotations begin with an @ followed by a camelCase name. Where there are multiple arguments, they are separated by spaces.

An optional text comment can precede the annotation comments. This comment is currently parsed but not used for anything.

--- This is a comment. Documentation can go here.
---
--- The empty comment above indicates a line break.
--- @annotation1 arg1 arg2 arg3
--- @annotation2 arg1 arg2 arg3
local x = 1 -- The above comment block is attached to this line.

--- This is a comment.

--- This is a different comment since the above line is empty.

-- This is not an annotation comment.

--[[
    This is not an annotation comment.
]]

print("hello world!") --- This is not an annotation comment.

Defining a Class

godot-luau-script classes are essentially a special table created using gdclass.

Both the original table and the output of gdclass are expected to be stored in locals, typically at the start of the file.

Annotations used for class registration, such as @class, should be placed on the original table local and not the local storing the output of gdclass.

The output of gdclass should be returned at the end of the file.

Valid example:

--- @class
local MyClass = {}
local MyClassC = gdclass(MyClass)

return MyClassC

Invalid example:

--- @class
MyClass = {} -- INVALID: The class parser ignores globals

return gdclass(MyClass) -- INVALID: The class parser will not trace this return

Annotation Reference

Key for documentation:

  • [] denotes an optional argument
  • <> denotes a required argument
  • ... means an argument can be supplied multiple times
  • | means ‘or’
  • '' denotes a literal argument supplied to the annotation without quotes
  • {} denotes multiple choices at any given location

Class Definition

These annotations should be attached to the local defining the original class table (in this example, MyClass).

@class [globalClassName]

Required to define a class.

  • globalClassName: If supplied, the class will be globally visible to Godot.

@extends <base>

Defines a script’s base class.

  • base: Either a valid Godot class name, or the name of a local containing a directly required script.

require example:

-- Your require call must directly contain a path.
-- It must appear before the @extends annotation.
local BaseClass = require("BaseClass.lua")

--- @class
--- @extends BaseClass
local MyClass = {}

-- <truncated>

@tool

Indicates the script is instantiable/runnable in the editor.

@permissions <...permissionsFlags>

Declares the script’s permissions (see “Core Scripts”, VMs, and Permissions).

  • permissionsFlags: Valid permissions flags.

The following permissions flags are accepted:

  • BASE: Used for functionality that is available to all scripts by default.
  • INTERNAL: Used for functionality that is not part of any special permission level or BASE.
  • OS: Used for the OS singleton.
  • FILE: Used for any filesystem functionality.
  • HTTP: Used for any functionality allowing HTTP listen/requests.

@iconPath <path>

Defines a script’s icon in the editor.

  • path: The path to the icon. Must be absolute and point to an SVG image.

Class Type

@classType <classLocal>

This annotation should be attached to the type definition associated with your class.

Links a class to its type definition, which is used for registering properties and signals.

  • classLocal: The name of the local which your @class annotation is attached to.

Example:

--- @class
local MyClass = {}

--- @classType MyClass
export type MyClass = RefCounted & typeof(MyClass) & {
    -- Attach annotations in the "Property annnotations" section here.
    tableField1: number,
    tableField2: string,
} & {
    -- It's okay to intersect multiple table types.
    -- Annotations will still be parsed.
    tableField3: Vector2,
}

-- <truncated>

Only plain table types and intersections (&, not |) are supported.

Property annotations

These annotations should be attached to the individual properties in your class type.

Property groups

These annotations are evaluated in order alongside @property and @signal.

@propertyGroup [groupName]

Denotes a property group visible in the Godot editor.

  • groupName: The group name. If not supplied, the group for the next property is unset.

@propertySubgroup [groupName]

Denotes a subgroup of a property group visible in the Godot editor.

  • groupName: The group name. If not supplied, the group for the next property is unset.

@propertyCategory [groupName]

Denotes a property category visible in the Godot editor.

  • groupName: The group name. If not supplied, the category for the next property is unset.

Properties

The following annotations apply only to properties.

@property

Indicates a property should be exposed to Godot, registered based on its type annotation. All additional information necessary for the property is supplied through other annotations.

@default <defaultValue>

Registers a property’s default value, which will be automatically assigned when a script instance is initialized if no setter or getter exists.

  • defaultValue: The default value, in a format compatible with Godot’s str_to_var.

@set <method> and @get <method>

Register a property’s setter and getter, respectively.

  • method: The name of the setter/getter. The method should be part of this class and registered to Godot.

@range <min> <max> [step] [...'orGreater'|'orLess'|'hideSlider'|'radians'|'degrees'|'radiansAsDegrees'|'exp'|'suffix:'<suffix>]

Valid only for integer and number properties. Defines the range (and optional step) for a numeric value in the Godot editor.

Do not use decimals (including .0) for integer properties.

  • min: The minimum value.
  • max: The maximum value.
  • step: The step value.
  • orGreater and orLess: Allow the limit to apply only to the slider.
  • hideSlider: Hide the slider.
  • radians and degrees: Indicate the units of an angle value.
  • radiansAsDegrees: Indicate the value is in radians but should be displayed as degrees in the editor.
  • exp: Cause values to change exponentially.
  • suffix:<suffix>: Provide a custom suffix for the value.

@enum <...values>

Valid only for integer and string properties. Defines a set of acceptable values for a property in the Godot editor, either by index (for integer properties) or name (for string properties).

  • values: The enum values.

@suggestion <...values>

Valid only for string properties. Defines a set of suggested values for a property in the Godot editor.

  • values: The suggested values.

@flags <...flagValues>

Valid only for integer properties. Defines a set of bit flags which are easily set in the Godot editor.

  • flagValues: The names of the flag values.

@file ['global'] [...extensions]

Valid only for string properties. Indicates to the Godot editor that a string should be a file path.

  • global: If supplied, the path is not constrained to the current Godot project.
  • extensions: Desired extensions in the format *.ext, for example *.png and *.jpeg.

@dir ['global']

Valid only for string properties. Indicates to the Godot editor that a string should be a directory path.

  • global: If supplied, the path is not constrained to the current Godot project.

@multiline

Valid only for string properties. Indicates to the Godot editor that a string should have a multiline editor.

@placeholderText <text>

Valid only for string properties. Indicates to the Godot editor that a certain string should be displayed in the property’s text field if it is empty.

  • text: The placeholder text.

@flags{2D,3D}{Render,Physics,Navigation}Layers

Valid only for integer properties. Indicate to the Godot editor that one of the various layer editors should be shown in place of the default number editor.

@expEasing ['attenuation'|'positiveOnly']

Valid only for number properties. Indicates to the Godot editor that a easing curve editor should be shown in place of the default number editor.

  • attenuation: Flips the curve horizontally.
  • positiveOnly: Limit values to those greater than or equal to zero.

@noAlpha

Valid only for Color properties. Indicates to the Godot editor that the color should not have an editable alpha channel.

@signal

Indicates a property with the Signal or SignalWithArgs<F> type should be registered as a signal.

Methods

The following annotations should be attached to methods defined directly on the table indicated by @class. For example:

-- <truncated>

--- @registerMethod
function MyClass.TestMethod()
end

The following is not currently valid:

-- <truncated>

-- Not recognized
--- @registerMethod
MyClass.TestMethod = function()
end

@registerMethod

Indicates a method should be accessible to Godot, registered based on its type annotations.

If there are no type annotations, Variant is assumed for all arguments, and the return value is Variant if the method returns anything.

@param <name> [comment]

Indicates a parameter’s usage. Not currently used by godot-luau-script.

  • name: The name of the parameter.
  • comment: Comment describing the parameter.

@defaultArgs <array>

Indicates a method’s default arguments when called from Godot.

  • array: The default values, in the format of an array compatible with Godot’s str_to_var. The rightmost value corresponds to the rightmost argument.

@return [comment]

The @return annotation documents a return value. It is not currently used by godot-luau-script.

  • comment: Comment describing the return value.

@flags [...methodFlags]

Overrides a method’s flags.

See the Godot documentation for a list of flags and their meanings (omit METHOD_FLAG_).

  • methodFlags: The flag values.

@rpc ['anyPeer'|'authority'] ['unreliable'|'unreliableOrdered'|'reliable'] ['callLocal'] [channel]

Registers a remote procedure call with Godot.

See the this Godot blog post for details.

  • anyPeer: Allow the RPC to be called by any peer.
  • authority: Only allow the RPC to be called by the multiplayer authority.
  • unreliable: Transfer data unreliably without resending.
  • unreliableOrdered: Transfer data unreliably without resending, but in the correct order. Any older packets are dropped. This flag should not be used for different types of messages on the same channel.
  • reliable: Transfer data reliably; resend packets if they don’t arrive.
  • callLocal: Indicate this method must also be called locally when the RPC is sent.
  • channel: Indicate the channel this RPC should use. Each channel handles ordering of packets separately.

Assigns

The following annotations should be attached to assignments to fields on the table denoted by @class. For example:

-- <truncated>

--- @registerConstant
MyClass.TEST_CONSTANT = 5

@registerConstant

Valid only for assignments with Variant values. Registers a constant value with Godot.

Type Conversion

godot-luau-script does its best to determine the proper Godot type from Luau type annotations. Here are a few things to note about this process:

Nullable (? or T | nil) types

When annotating a method argument with a nullable type, the argument will be registered to Godot as non-nullable (i.e. number? -> FLOAT). If you want to have the argument be optional on the Godot side, register default arguments.

When annotating anything else (properties, return types, etc.), using a nullable type will cause the type to be registered with Godot as Variant unless the type extends Object. This is because the only Godot type that accepts both a value and null is Variant. If you encounter this, it’s recommended to reimplement your logic such that using a nullable is not necessary.

TypedArray<T>

When used for methods and properties, TypedArray<T> will supply Godot with the correct type for Array elements.

NodePathConstrained<T...>

When used for methods and properties, NodePathConstrained<T...> will tell Godot to constrain valid Node types to the given types. Only supply types which extend Node.

SignalWithArgs<F>

When used for signals, SignalWithArgs<F> will tell Godot the parameters of the registered signal. The supplied type F should be a void (-> ()) function type with no generics and any number of arguments.

Argument names are optional. If not supplied, the default is argN where N is the index of the argument starting from 1.

Other potential issues

  • Aliasing Godot types (e.g. type D = Dictionary) is not currently supported.

Virtual Methods

godot-luau-script supports the overriding of virtual methods.

Defined in an Object class

If a virtual method such as _process or _ready is defined on an Object-type class, you can override it by defining it in your class table and registering it to Godot.

Built into godot-luau-script

This project comes with a number of built-in virtual methods, which can be overridden just by defining them on your implementation table. They do not need to be registered to Godot.

function _Init(self): ()

Called as soon as an Object with your script is instantiated.

At this time, you can initialize any values on the object or its underlying table. Note that the virtual _Ready on Node may be equally or more appropriate in many cases.

function _Notification(self, what: number): ()

Called whenever your object receives a notification. Refer to Godot documentation to see what notifications you may receive.

function _ToString(self): string

Called whenever Godot wants to stringify your object, for example when printing your object or calling tostring. It should return the string representation of your object.

Custom property methods

These methods should be used in combination to define custom properties for your object, typically so they can show up in the editor. Keep in mind you must set @tool on your class for these methods to run in the editor.

function _GetPropertyList(self): {GDProperty}

Return a list of additional properties to be registered to Godot.

function _Set(self, name: string, value: Variant): boolean

Handle setting a value for a custom property. It should return true when it set the value successfully, and false when it did not (or when the property doesn’t exist).

function _Get(self, name: string): Variant

Handle getting a value for a custom property. It should return the value of the property or nil if the property doesn’t exist.

function _PropertyCanRevert(self, name: string): boolean

Return true if the given property has a default value which the editor should be able to revert to.

function _PropertyGetRevert(self, name: string): Variant

Overridden along with _PropertyCanRevert. It should return the default value of the given property, or nil if it doesn’t exist.

The “Idiomatic” Class

As this project is still in early stages, you shouldn’t consider this syntax/interface stable yet.

The following class provides an example as to what is currently considered “idiomatic” and type checkable:

--!strict
--- @class MyClass
--- @extends Resource
local MyClass = {}

-- 'C' stands for class. The `gdclass` method gives access to the `.new
-- function.
local MyClassC = gdclass(MyClass)

-- These items registered will be accessible by indexing `MyClass`,
-- making them accessible to other scripts if they `require` this one.
MyClass.TestEnum = {
    ONE = 1,
    TWO = 2,
    THREE = 3,
}

--- @registerConstant
MyClass.TEST_CONSTANT = "hello!"

--- @classType MyClass
export type MyClass = Resource & typeof(MyClassImpl) & {
    --- @property
    --- @range 0.0 1.0 0.1
    --- @default 0.5
    testProperty: number,
}

--- Register a method to be accessible to Godot, based on its type annotations
--- @registerMethod
--- @param arg1 Comment 1
--- @param arg2 Comment 2
--- @return Something
function MyClass.TestMethodAST(self: MyClass, arg1: number, arg2: TypedArray<Resource>): Variant
    return 123
end

return MyClassC

godot-luau-script API Reference

godot-luau-script provides its own library on top of bindings to Godot in order to allow custom class creation and provide various other useful functions (e.g. wait and require).

Currently, this library is defined and documented in definitions/luauLibTypes.part.d.lua.

---------------------
-- LUAGD_LIB TYPES --
---------------------

--- Constructs a new `StringName`.
--- @param str The string to use.
declare function SN(str: string): StringNameN

--- Constructs a new `NodePath`.
--- @param str The string to use.
declare function NP(str: string): NodePathN

-- TODO: constrain to Resource type?
--- Loads a resource. If the current script is not a core script, accessible
--- paths are restricted by SandboxService.
--- @param path The relative/absolute path to the resource from this script.
declare function load<T>(path: string): T?

--- Saves a resource. If the current script is not a core script, accessible
--- paths are restricted by SandboxService.
--- @param path The absolute path to save the resource to.
declare function save(resource: Resource, path: string, flags: ClassEnumResourceSaver_SaverFlags?)

--- Determines the Godot Variant type of a value, or `nil` if the value is not Variant-compatible.
--- @param value The value.
declare function gdtypeof(value: any): EnumVariantType?

--------------------
-- LUAU_LIB TYPES --
--------------------

--- A table type used to declare properties, method arguments, and return values.
export type GDProperty = {
    type: EnumVariantType?,
    name: string?,
    hint: EnumPropertyHint?,
    hintString: string?,
    usage: EnumPropertyUsageFlags?,
    className: string?,
}

--- A type returned from a script to register a custom class type.
declare class GDClass
    --- Constructs a new instance of the base object with this script.
    new: () -> Object
end

--- Creates a new @see GDClass.
--- @param tbl The implementation table for any methods, constants, etc. Items in this table will be accessible by indexing this class definition.
declare function gdclass<T>(tbl: T): T & GDClass

--- Yields the current thread and waits before resuming.
--- @param duration The duration to wait. If the engine is affected by a time factor, this duration will be affected by it.
declare function wait(duration: number): number

--- Yields the current thread and waits for the signal to be emitted before resuming.
--- @param signal The signal.
--- @param timeout The number of seconds to wait before timing out (default: 10 seconds).
--- @return The first return value is whether the signal was emitted (true) or timed out (false), and subsequent values are the arguments passed to the signal when it was emitted.
declare function wait_signal<T...>(signal: Signal, timeout: number?): (true, T...) | false

--- Gets a global constant (e.g. AutoLoad) which was defined in the Godot editor.
--- @param name The name of the global constant.
declare function gdglobal(name: StringNameLike): Variant

--------------
-- SERVICES --
--------------

export type EnumPCKScanError = number

declare class EnumPCKScanError_INTERNAL
    OK: EnumPCKScanError
    FILE_ERR: EnumPCKScanError
    MAGIC_NOT_FOUND_ERR: EnumPCKScanError
    PACK_VERSION_ERR: EnumPCKScanError
    GODOT_VERSION_ERR: EnumPCKScanError
    PACK_ENCRYPTED_ERR: EnumPCKScanError
    FILE_SCAN_ERR: EnumPCKScanError
end

export type EnumPCKFileScanError = number

declare class EnumPCKFileScanError_INTERNAL
    OK: EnumPCKFileScanError
    FILE_ENCRYPTED_ERR: EnumPCKFileScanError
    UNTRUSTED_GDSCRIPT_ERR: EnumPCKFileScanError
    UNTRUSTED_FILE_ERR: EnumPCKFileScanError
    ENDIANNESS_ERR: EnumPCKFileScanError
    RES_VERSION_ERR: EnumPCKFileScanError
    GODOT_VERSION_ERR: EnumPCKFileScanError
    SANDBOX_VIOLATION_ERR: EnumPCKFileScanError
end

export type EnumSandboxViolations = number

declare class EnumSandboxViolations_INTERNAL
    UNTRUSTED_EXT_SCRIPT_VIOLATION: EnumSandboxViolations
    RESOURCE_SANDBOX_VIOLATION: EnumSandboxViolations
    UNTRUSTED_INT_SCRIPT_VIOLATION: EnumSandboxViolations
end

--- Service handling script sandboxing
declare class SandboxService
    --- Returns whether a script is a core script.
    --- @param path The path to query.
    function IsCoreScript(self, path: string): boolean

    --- Initiates core script discovery from the project root. By default, any
    --- files present at this time will be considered core scripts.
    function DiscoverCoreScripts(self)

    --- Ignores a path from core script discovery. Any file paths starting with
    --- the given path will be ignored.
    --- @param path The path to ignore.
    function CoreScriptIgnore(self, path: string)

    --- Adds a script path as a core script.
    --- @param path The path to add.
    function CoreScriptAdd(self, path: string)

    --- Returns an array of all core scripts.
    function CoreScriptList(self): Array

    --- Adds access for non-core scripts to read or write resources to the given
    --- path and its subdirectories.
    --- @param path The path to add.
    function ResourceAddPathRW(self, path: string)

    --- Adds access for non-core scripts to read from the given path and its
    --- subdirectories.
    --- @param path The path to add.
    function ResourceAddPathRO(self, path: string)

    --- Removes access for non-core scripts to read or write from the given path
    --- and its subdirectories.
    --- @param path The path to remove.
    function ResourceRemovePath(self, path: string)

    --- Scans a PCK file and returns a report with details.
    --- @param path The path of the PCK to scan.
    function ScanPCK(self, path: string): Dictionary
end

--- Service handling debugging, GC diagnostics, and profiling
declare class DebugService
    --- Returns memory usage, in kibibytes (=1024 bytes), of each running VM.
    function GCCount(self): PackedFloat64Array

    --- Returns the current rate at which the garbage collector is attempting
    --- to collect on each VM, in KB/s, roughly corresponding to the rate at
    --- which memory usage is increasing.
    function GCStepSize(self): PackedInt32Array

    --- Executes code on the core VM with all permissions enabled.
    --- IMPORTANT: This method should not be used outside of debugging (i.e.
    --- core game logic should not depend on this functionality).
    --- @param src The source code to execute.
    --- @return An empty string if the call was successful, or the error message if any.
    function Exec(self, src: string): string
end

--- The main service through which all services are found.
declare LuauInterface: {
    SandboxService: SandboxService,
    DebugService: DebugService,
}

“Core Scripts”, VMs, and Permissions

These three concepts are essential to godot-luau-script’s sandboxing and security.

“Core Scripts”

Core scripts have special privileges such as being able to set their own permissions. By default, no scripts are considered core scripts.

Scripts can be designated as core scripts through the SandboxService, typically using init.lua.

VMs

godot-luau-script currently runs 3 different Luau VMs for different use cases:

  1. The loading VM, which is used to load essential script information (e.g. a list of methods and constant values).
  2. The core VM, which is where all core scripts are executed.
  3. The user VM, which is where all non-core scripts are executed.

This provides a certain level of isolation and security in addition to thread sandboxing.

Method calls

Within the same VM, any script can call any other script’s methods, so long as they are defined in the implementation table. Between VMs, scripts can only call other scripts’ methods if they are registered to Godot. This gives a rough notion of “public” and “private”.

Permissions

As mentioned in the introduction, godot-luau-script was primarily created to load untrusted scripts safely. This means that certain Godot APIs must be locked behind special permissions, and that scripts will need to declare these permissions to receive them.

The specific permission levels are listed here. As noted above, only a core script can declare its own permissions.

Certain APIs receive special permissions, with INTERNAL being the default. Permissions are set on the thread level, and are inherited to child threads. If a thread ever tries to execute a method or other code of a permission level it does not have, it will cause an error.

Note that only methods which are registered to Godot are executed on their script’s thread. If any non-registered methods use protected APIs, classes with Godot-registered methods that use them must declare the necessary permissions.

A comprehensive list of the permissions assigned to every Object class and method can be seen at bindgen/class_settings.toml.

Modules

godot-luau-script supports non-class module scripts and the require function.

require

The require function accepts a path relative to the current script, without the .lua extension. It will load the file at the given path and return whatever the file returned.

Requires are cached within the same Lua VM, and are reloaded when necessary (if the script changes).

Please note that cyclic dependencies are not supported and may cause odd behavior in the editor if encountered.

Module scripts

require can require a script (class) file, in which case it will return the output of gdclass for the class.

However, you may want to create your own Lua types which don’t need to be accessible to Godot. To do this in a dedicated file, create a file with the extension .mod.lua which returns a function or a table.

Then, you will be able to require this file (being sure to include the .mod but not the .lua in the require path).

Task Scheduler

godot-luau-script borrows the task scheduler (among other concepts) from Roblox.

Currently, the task scheduler is only responsible for resuming yielded threads and running the garbage collector.

It updates once every frame.

init.lua

If res://init.lua exists, it is run with on the core VM with all permissions enabled. Certain functionality, such as require and gdclass, are disabled as init.lua is not considered a script file.

This file can be used to configure features which can or should be initialized as soon as the extension is initialized, such as core script detection and sandboxing settings.

Typechecking and Autocomplete

Upon building, godot-luau-script will generate a single type definition file for use by Luau’s analyzer.

Autocomplete and analysis in the Godot editor is not supported. As such, the most viable setup is to use Visual Studio Code and the luau-lsp extension.

Setup

After installing VSCode and luau-lsp, you will need to do the following setup:

  • Set the preference luau-lsp.require.mode to relativeToFile to make require relative to the current script.
  • Set the preference luau-lsp.types.roblox to false to disable Roblox types being loaded by default.
  • Set the preference luau-lsp.types.definitionFiles to an array containing the path to definitions/luauScriptTypes.gen.d.lua.
  • Set the FFlags LuauRecursionLimit and LuauTarjanChildLimit to something higher than the defaults (e.g. 5000 and 20000 respectively). This can be done through the luau-lsp.fflags.override preference.
    • This is necessary because the generated definition file is too large and complex for Luau to parse by default.

After doing this (and reloading the workspace), you should get type checking and autocomplete for all Godot and godot-luau-script types.

Known Issues

godot-luau-script and Godot’s GDExtension script language interface are both relatively unstable. As such, you may experience issues when using them together.

Error: _gdvirtual__get_language_call: Required virtual method ScriptExtension::_get_language must be overridden before calling.

This is likely an upstream issue with the initialization of GDExtension classes. Scripts should run normally despite this error.

The issue is tracked here.

Tests

godot-luau-script contains tests written using the Catch2 framework. They can be built using the tests=yes flag when running scons.

Tests can be run by running Godot in the test_project directory with the --luau-tests flag. Then, Catch2 flags can be given after supplying a -- argument.

Note for Windows users

godot-luau-script uses a symbolic link to link bin/ to test_project/bin/. These do not work on Windows. If you want to run tests, you will need to copy the folder (including built binaries) yourself.

Debugging

Assuming you built godot-luau-script with the debug_symbols=yes flag, you will be able to debug the project by running Godot with a debugger attached.

In case you run into issues which the debugger cannot trace, they may be caused by Godot engine code. You can debug engine code by cloning Godot and building it with debug symbols as well. Be sure to git checkout the commit matching the current supported version of Godot before building.