Appearance
Getting Started
To create a script, place a .lua file in the scripts folder within the .neon/NeonFabricData/scripts directory.
API Definitions (Autocomplete, optional)
For improved editor support (autocomplete), you can download the Lua type definitions for your IDE.
- Download lightweight Lua API definitions (ZIP) — recommended for normal scripting.
- Download full Lua API definitions (ZIP) — includes low-level LWJGL OpenGL/Vulkan classes.
The lightweight definitions include Minecraft, supported Java standard library classes, Gson, JOML, and Minecraft's rendering API alongside the Neon API. Use the full bundle for scripts that import org.lwjgl.opengl or org.lwjgl.vulkan classes directly. Extract the ZIP into a neon-types folder. At your script workspace root, create .luarc.json with the following settings, or merge them into your existing configuration:
json
{
"runtime.version": "Lua 5.2",
"runtime.special": {
"java.import": "require"
},
"workspace.library": [
"./neon-types"
],
"workspace.maxPreload": 20000,
"workspace.preloadFileSize": 8192,
"diagnostics.libraryFiles": "Disable",
"diagnostics.workspaceDelay": -1
}Literal imports such as java.import("java.lang.Math") infer the class automatically. The mc global, Java method results, enum constants, and java.new(...) instances also have types. See the Java API for examples.
Script Metadata
You can define metadata at the top of your script using --- @tag value format.
lua
--- @unsafe trueSupported Tags
| Tag | Description |
|---|---|
@unsafe | Set to true to enable unsafe mode (access to more APIs). If a script is marked unsafe, it will only load if the user has explicitly marked the script as unsafe, even if this is set to false scripts should not be blindly trusted, this is not trying to block everything malicious. |
Registering Features
Every script usually registers one or more features.
lua
local feature = neon.register_feature("Auto Example", "Automatically does something")
local auto_toggle = feature.boolean_builder("Auto Toggle").default_value(false).build()
local auto_toggle_hidden = feature.boolean_builder("Hidden Toggle")
.default_value(false)
.visibility(function() return false end)
.build()
feature.on("game_tick", function()
if auto_toggle.value then
-- logic
end
end)Creating Settings with Builders
Use a typed factory such as feature.number_builder(name) to configure a setting with named, chainable methods. The setting is added to the feature only when you call .build(), which returns the same property type as the corresponding register_* function.
lua
local feature = neon.register_feature("Builder Example", "Settings built step by step")
local category = feature.register_category("General", function() return true end)
local enabled = feature.boolean_builder("Enabled")
.default_value(true)
.build()
local speed = feature.number_builder("Speed")
.description("Movement speed")
.range(0, 10)
.step(0.1)
.default_value(5)
.category(category)
.visibility(function() return enabled.value end)
.observer(function(old_value, new_value)
print("Speed changed from " .. old_value .. " to " .. new_value)
end)
.build()
feature.on("game_tick", function()
if enabled.value then
print(speed.value)
end
end)Typed factories and their required methods:
| Factory | Required configuration | Result |
|---|---|---|
feature.boolean_builder(name) | .default_value(true) | types.boolean_property |
feature.string_builder(name) | .default_value("Hello") | types.string_property |
feature.number_builder(name) | .range(0, 10).default_value(5) | types.number_property |
feature.number_range_builder(name) | .range(0, 10).default_value(2, 8) | types.number_range_property |
feature.color_builder(name) | .default_value(types.color.new(255, 0, 255)) | types.color_property |
feature.enum_builder(name) | .options({"A", "B"}).default_value("A") | types.enum_property |
feature.multi_select_builder(name) | .options({"A", "B"}).default_value({"A"}) | types.multi_select_property |
All builders accept .description(...), .category(...), .visibility(...), and .observer(...). An observer receives the previous and new values when the property notifies a change, including updates made by scripts or the GUI. It is not called when the default value is first assigned. Number and number range builders also accept .step(...), which defaults to 0.01 and must be positive. Numeric defaults must be within the range, number range defaults must be ordered. Enum and multi-select defaults must appear in the configured options. Use .default_value({}) for an empty multi-select default.
For example, create a range and a selection:
lua
local delay = feature.number_range_builder("Delay")
.range(0, 1000)
.step(1)
.default_value(100, 300)
.build()
local mode = feature.enum_builder("Mode")
.options({"Normal", "Fast"})
.default_value("Normal")
.build()Deprecated Registration Functions
The original feature.register_boolean, register_string, register_number, register_number_range, register_color, register_enum, and register_multi_select APIs are deprecated. Use the corresponding typed builders and .build() for new settings. The original functions remain supported with their existing signatures for backwards compatibility, and both styles can coexist in the same feature:
lua
local legacy = feature.register_number("Legacy Speed", 0, 10, 0.1, 5)
local built = feature.number_builder("Builder Speed")
.range(0, 10)
.step(0.1)
.default_value(5)
.build()Draggable custom UIs
Lua features can provide custom UI language source directly:
lua
local feature = neon.register_draggable_ui_feature(
"My HUD",
"A draggable scripted HUD",
[[
box {
width: 100%;
height: 100%;
rect {
id: "draggable-root";
draggable: true;
width: 120px;
padding: 8;
color: rgba(0, 0, 0, 0.8);
text { text: #label; color: white; }
}
}
]]
)
feature.provide_property("label", function()
return "Hello from Lua"
end)See the Custom UI Language reference for the complete syntax, all properties, bindings, node access, and click-handler examples.
Registering Commands
Scripts can also register custom chat commands. You can define arguments and the execution handler directly in the register_command call.
lua
neon.register_command(
"test",
"A test command",
{ "alias1", "alias2" },
{
{ name = "message", type = enums.argument_type.string, optional = false },
{ name = "hello", type = enums.argument_type.string, optional = false },
{ name = "amount", type = enums.argument_type.integer, optional = true, completer = function(input)
local options = { "1", "5", "10", "15", "20" }
local suggestions = {}
for _, v in ipairs(options) do
if v:lower():find(input:lower(), 1, true) == 1 then
table.insert(suggestions, v)
end
end
return suggestions
end }
},
function(msg, hello, amount)
print("Message: " .. msg)
print("Message two: " .. hello)
if amount then
print("Amount: " .. amount)
end
end
)