L2GODOT · LUA API V1

Addon documentation

Install ready-made addons or build your own in Lua.

Install an addon

Use the New UI to install and display addons. Try one of the ready-made examples if you’re new to addons.

  1. Download the addon and extract its ZIP.
  2. In Options → Addons, select Open folder and copy the extracted addon folder there.
  3. Return to the game, turn on the addon’s switch, and select OK. Its panels appear when you’re in the world.

To reopen a panel you closed, select Show panels next to the addon in Options. To move a panel, use Options → Interface → Layout → Rearrange.

To uninstall an addon, disable it in Options, select OK, and remove its folder.

Write your first addon

Give your addon its own folder. The folder name identifies the addon and uses up to 48 lowercase letters, digits, hyphens, or underscores. Place both required files directly inside it.

my-vitals/
├── addon.json
└── main.lua

Addon manifest

Write addon.json as UTF-8 JSON. The name, version, author, and url fields are required strings. The Addons list uses these values for the display name, version, author, and website link. Use an absolute HTTP or HTTPS URL.

{
  "name": "My Vitals",
  "version": "1.0.0",
  "author": "Your Name",
  "url": "https://example.com/my-vitals"
}

Lua entry point

Define a global main() in your UTF-8 script. The client calls it once per load. Register events there and use an ID to create or update a HUD panel.

function main()
    l2.on("tick", function()
        local me = l2.units.self()
        if me then
            local health = string.format("%d / %d HP", me.hp, me.max_hp)
            l2.ui.panel("vitals", "My vitals", health)
        end
    end)
end

Install your folder using the steps above. After editing your script, select Reload addons in Options to test the changes. Zip the whole folder when you share it. Keep the same folder name across releases to preserve players’ preferences.

API reference

Lifecycle

l2.version
The numeric API version. Currently 1.
l2.has(feature)
Check for "ui" controls or "settings" support in the client, or "combat" support on the connected server.
l2.on(event, callback)
Register one callback per event in main().
world.ready
Delivers your player snapshot once after the addon loads in the world, including after a reload.
tick
Runs about four times per second. event.client_ms is a monotonic client timestamp in milliseconds.

Show the active character on load

function main()
    if l2.version < 1 then return end
    l2.on("world.ready", function(me)
        if me then l2.ui.panel("session", "Character", me.name) end
    end)
end

Units and party

l2.units.self() and l2.units.target()
Get your own or your target’s unit snapshot. Returns nil when unavailable.
l2.units.by_id(id)
Look up a unit known to the client. Returns nil for unknown IDs.
l2.units.party()
Array with self and party members. Returns an empty array when solo.
l2.units.nearby(kind, limit)
Get up to 32 known nearby units, nearest first. Use "npc", "player", or "all". Defaults to 16 NPCs.
party.changed
Delivers the current party array when membership changes.

Unit tables include id, name, and kind. Other fields can include level, class_id, hp, max_hp, mp, max_mp, cp, max_cp, leader, attackable, dead, and distance in Interlude world units. Unknown fields are nil.

Show the nearest known NPC

function main()
    l2.on("tick", function()
        local nearest = l2.units.nearby("npc", 1)[1]
        l2.ui.panel("nearby", "Nearby NPC", nearest and nearest.name or "None nearby")
    end)
end

Skills and casting

l2.skill_name(id)
Get a display name from the client’s skill table.
l2.cooldown(skill_id)
Get your cooldown’s remaining and total in seconds, or nil if the timer is unknown.
cast.started
Delivers source, target, skill, level, cast_ms, and reuse_ms for a visible cast.
cast.canceled
Delivers source when a visible cast is interrupted.

Track the last skill you cast

local skill

function main()
    l2.on("cast.started", function(cast)
        local me = l2.units.self()
        if me and cast.source == me.id then skill = cast.skill end
    end)
    l2.on("tick", function()
        if not skill then return end
        local reuse = l2.cooldown(skill)
        local time = reuse and string.format("%.1f s", reuse.remaining) or "Timer unavailable"
        l2.ui.panel("reuse", "Skill cooldown", l2.skill_name(skill) .. " · " .. time)
    end)
end

Combat feed

l2.has("combat")
Returns whether the connected L2G server supports the combat feed.
combat.batch
Delivers confirmed combat results for you and visible party members in the same instance. Each batch contains sequence, overflow, and a records array.

Records contain kind (damage, heal, or miss), actor IDs source, target, and owner, a skill ID, applied hp and cp amounts, and server_ms. Zero indicates an unavailable source, owner, or skill ID. The flags critical, periodic, reflected, and transferred identify those outcomes when available. Treat totals as incomplete after an overflow. Coverage depends on the server build.

Total your character’s damage

local damage, incomplete = 0, false

function main()
    l2.on("combat.batch", function(batch)
        incomplete = incomplete or batch.overflow
        local me = l2.units.self()
        if not me then return end
        for _, event in ipairs(batch.records) do
            if event.kind == "damage" and event.source == me.id then
                damage = damage + event.hp + event.cp
            end
        end
    end)
    l2.on("tick", function()
        local text = "Waiting for a compatible combat feed."
        if l2.has("combat") then
            text = string.format("%.1f%s", damage, incomplete and " (incomplete)" or "")
        end
        l2.ui.panel("damage", "My damage", text)
    end)
end

HUD panels and controls

l2.ui.panel(id, title, text)
Create a panel or update its title and body. New panels open automatically. Updates respect the existing open or closed state.
l2.ui.open(id), l2.ui.close(id), l2.ui.is_open(id)
Show or hide an existing panel. Query its open state with is_open, which returns false for an unknown panel. In Legacy UI the logical state is kept for the next switch to New UI.
l2.ui.remove(panel, id)
Remove a widget. Returns true if a widget was removed. Remove a widget before reusing its ID for another type.

See UI elements for the calls and in-client previews for each control.

Each addon can create up to three panels with 12 widgets each. IDs use up to 48 lowercase letters, digits, hyphens, or underscores. Titles allow 52 characters and panel body text allows 840. Button and checkbox captions, dropdown options, and input placeholders allow 80 characters.

UI events

ui.opened and ui.closed
Deliver event.panel when a panel is first created or its open state changes. Register before creating the panel to receive its first open event.
ui.clicked
Delivers event.panel and event.widget when the player presses a button.
ui.changed
Also delivers event.value. It is a boolean for a checkbox or a one-based index for a dropdown.
ui.submitted
Delivers the panel ID, widget ID, and submitted text in event.value.

Setting a control’s value from Lua applies the update without emitting a user change event.

Make an interactive progress counter

local count = 0

function main()
    if not l2.has("ui") then return end
    l2.on("ui.clicked", function(event)
        if event.panel == "counter" and event.widget == "add" then
            count = math.min(count + 1, 5)
            l2.ui.progress("counter", "progress", count, 5)
        end
    end)
    l2.ui.panel("counter", "Progress counter", "")
    l2.ui.button("counter", "add", "Add one")
    l2.ui.progress("counter", "progress", count, 5)
end

UI elements

These previews come from the L2Godot client. Use the same panel and widget IDs to update existing controls.

Panel

l2.ui.panel(id, title, text)

A movable container with a title, body, and close button. Create it before adding widgets.

l2.ui.panel("tracker", "Goal tracker", "Track a personal goal.")
Goal tracker addon panel with a title, close button, and six controls

Label

l2.ui.label(panel, id, text)

Display up to 256 characters of text.

l2.ui.label("tracker", "status", "3 of 5 objectives complete")
Label reading 3 of 5 objectives complete

Button

l2.ui.button(panel, id, text)

A press sends ui.clicked.

l2.ui.button("tracker", "complete", "Complete objective")
Complete objective button with the New UI gold border

Checkbox

l2.ui.checkbox(panel, id, text, checked)

Pass true or false for the checked state. User changes send that boolean in ui.changed.

l2.ui.checkbox("tracker", "show-progress", "Show progress", true)
Show progress checkbox in its checked state

Dropdown

l2.ui.dropdown(panel, id, options, selected)

Supply 1 to 16 options. The selected index starts at 1. A new selection sends ui.changed.

l2.ui.dropdown("tracker", "goal", {"Goal: 5", "Goal: 10", "Goal: 20"}, 1)
Dropdown showing the selected option Goal: 5

Progress bar

l2.ui.progress(panel, id, value, maximum)

Use finite numbers and a positive maximum. The value is clamped to the bar’s range.

l2.ui.progress("tracker", "progress", 3, 5)
Progress bar filled to 60 percent

Text field

l2.ui.input(panel, id, text, placeholder)

Accepts up to 128 characters. The placeholder is optional. Enter sends the text in ui.submitted.

l2.ui.input("tracker", "note", "", "Add a note")
Empty text field with an Add a note placeholder

Saved settings

l2.settings.get(key, default)
Get a saved value. Returns the optional default or nil when absent.
l2.settings.set(key, value)
Save a boolean, finite number, or string up to 256 characters. Pass nil to remove the key.

Each addon has up to 32 settings. Keys use the same naming rules as widget IDs. Changes are immediately available to the script and saved locally about once per second. Settings are keyed to the addon folder and survive reloads, client restarts, and upgrades that keep the same folder name.

Remember a checkbox across sessions

function main()
    if not l2.has("settings") or not l2.has("ui") then return end
    l2.on("ui.changed", function(event)
        if event.panel == "prefs" and event.widget == "details" then
            l2.settings.set("show-details", event.value)
        end
    end)
    l2.ui.panel("prefs", "Preferences", "")
    l2.ui.checkbox("prefs", "details", "Show details", l2.settings.get("show-details", true))
end

Examples

Panel Demo

Use a button, checkbox, dropdown, and text field to track a small goal. The demo saves its preferences and runs with the client UI API.

Download Panel Demo ZIP

View Panel Demo source

Combat Meter

See damage and healing totals for you and your party. Start fighting after you enter the world to see your totals. This addon requires a server that supports the L2Godot combat feed.

Download Combat Meter ZIP

View Combat Meter source · View manifest

Security

Addons run inside the game with restricted access. An addon can use supported game data, show its own panels, and save its own settings. Scripts cannot make network requests, open files on your computer, or run system commands. Enable addons from sources you trust.

Addon authors must distribute Lua source code in main.lua. The client blocks Lua’s file and module loaders, precompiled bytecode, and access to .NET objects. It also limits Lua instructions and API calls per callback.

Limits and troubleshooting

If an addon is missing from the list, check that you copied the extracted folder into the Addons folder. If it appears disabled with an error, check that addon.json and main.lua are directly inside that addon’s folder.

Up to eight addons can run at once. For addon authors, the limits are 64 KB for main.lua and 8 KB for addon.json. The Addons list displays script errors.