eolib 0.5.0
A core C library for writing applications related to Endless Online
Loading...
Searching...
No Matches
Getting Started (Lua)

This guide walks you through setting up a Lua development environment, downloading the eolib-c Lua bindings, and writing two practical example scripts.

The Lua bindings expose the same protocol types as the C library through a native C extension module (eolib.so / eolib.dll) that you require from Lua 5.4.

Prerequisites

You only need Lua 5.4 to use the pre-built bindings.

Linux (Ubuntu / Debian)

sudo apt-get install lua5.4

macOS

brew install lua

Windows

Download and install a Lua 5.4 binary from https://luabinaries.sourceforge.net/ (or use LuaRocks which bundles Lua).


Getting the bindings

Option 1 — Pre-built drop-in (recommended)

The Lua bindings are included in the main eolib release archives. Download the one for your platform:

Platform Download
Linux x86_64 eolib-0.5.0-Linux.x64.zip
Linux ARM64 eolib-0.5.0-Linux.arm64.zip
macOS Apple Silicon eolib-0.5.0-macOS.arm64.zip
macOS Intel eolib-0.5.0-macOS.x64.zip
Windows MSVC x64 eolib-0.5.0-Windows.VS2022.x64.zip
Windows MSVC x86 eolib-0.5.0-Windows.VS2022.x86.zip
Windows MinGW x64 eolib-0.5.0-Windows.MinGW.x64.zip
Windows MinGW x86 eolib-0.5.0-Windows.MinGW.x86.zip

Drop-in (quickest): Extract the archive into your project directory. Lua searches ./ by default so require("eolib") will just work, and lua-language-server will pick up the type definitions automatically.

Or copy to a system prefix:

# Linux / macOS
cp eolib-0.5.0-{platform}/lib/lua/5.4/eolib.so /usr/local/lib/lua/5.4/
cp eolib-0.5.0-{platform}/lib/libeolib.* /usr/local/lib/
cp eolib-0.5.0-{platform}/share/lua/5.4/eolib.d.lua /usr/local/share/lua/5.4/

Option 2 — Build and install from source

Building from source requires a C toolchain, CMake 3.16+, libxml2, and json-c.

Linux (Ubuntu / Debian):

sudo apt-get install gcc cmake build-essential libxml2-dev libjson-c-dev liblua5.4-dev lua5.4

macOS:

xcode-select --install && brew install cmake libxml2 json-c lua

Windows — MSVC: Install Visual Studio 2022 Community with the Desktop development with C++ workload. libxml2 and json-c are handled automatically via the bundled vcpkg configuration.

Windows — MinGW via MSYS2:

pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-libxml2 mingw-w64-x86_64-json-c mingw-w64-x86_64-lua

Then build and install:

git clone --recurse-submodules https://github.com/sorokya/eolib-c.git
cd eolib-c
cmake -S . -B build -DEOLIB_BUILD_LUA_BINDINGS=ON
cmake --build build
cmake --install build

Troubleshooting: module 'eolib' not found

Some systems (e.g. macOS with Homebrew Lua) use a different prefix than /usr/local. Check where your Lua looks:

lua -e "print(package.cpath)"

Then copy files to the right path, or set LUA_CPATH manually:

export LUA_CPATH="/usr/local/lib/lua/5.4/?.so;;"

IDE support (lua-language-server)

eolib.d.lua provides full type annotations for lua-language-server. For the drop-in method, having it in your project root is enough. For a system install, luals typically picks it up from share/lua/5.4/ automatically.


Example 1 — Packet Inspector

This script reads raw packet bytes from stdin and decodes a LoginRequestClientPacket (family = 4, action = 1). It then re-encodes the packet and prints the byte count.

Note
In the EO wire format the two-byte family/action header is stripped before the packet body is passed to deserialize. Your network layer is responsible for removing those two bytes first.

packet_inspect.lua

local eolib = require("eolib")
local PACKET_FAMILY_LOGIN = 4
local PACKET_ACTION_REQUEST = 1
-- Parse command-line arguments.
local family = tonumber(arg[1])
local action = tonumber(arg[2])
if not family or not action then
io.stderr:write("Usage: lua packet_inspect.lua <family> <action>\n")
os.exit(1)
end
-- Read the packet body from stdin (family/action bytes already stripped).
local raw = io.read("*a")
if #raw == 0 then
io.stderr:write("No data on stdin.\n")
os.exit(1)
end
print(string.format("Received %d byte(s), family=%d, action=%d",
#raw, family, action))
if family == PACKET_FAMILY_LOGIN and action == PACKET_ACTION_REQUEST then
-- Deserialize the packet body.
local pkt, err = eolib.LoginRequestClientPacket.deserialize(raw)
if not pkt then
io.stderr:write("Deserialize failed: " .. tostring(err) .. "\n")
os.exit(1)
end
print(" username: " .. (pkt.username or "(nil)"))
print(string.format(" password: (hidden, %d char(s))",
pkt.password and #pkt.password or 0))
-- Re-encode and report the output size.
-- The binding pre-allocates the exact buffer size internally, so no
-- manual sizing is needed from Lua.
local bytes, err2 = pkt:serialize()
if not bytes then
io.stderr:write("Serialize failed: " .. tostring(err2) .. "\n")
os.exit(1)
end
print(" re-encoded: " .. #bytes .. " byte(s)")
else
print(string.format(" (no decoder for family=%d action=%d)",
family, action))
end

Running:

# Linux / macOS (pipe raw bytes for Login_Request)
printf '\x06\x07admin\xFFsecret' | lua5.4 packet_inspect.lua 4 1
# Windows (PowerShell)
[System.Text.Encoding]::Latin1.GetBytes("`x06`x07admin`xFFsecret") |
lua packet_inspect.lua 4 1

Example 2 — EIF Pub File Reader

This script reads an EIF (Endless Item File) pub file, parses it, and prints all items whose type matches the given integer argument.

The eolib.ItemType table exposes every ITEM_TYPE_* constant from the C API (e.g. eolib.ItemType.WEAPON == 10).

pub_reader.lua

local eolib = require("eolib")
-- Print usage if arguments are missing.
local path = arg[1]
local filter = tonumber(arg[2])
if not path or not filter then
io.stderr:write("Usage: lua pub_reader.lua <file.eif> <item_type_int>\n")
io.stderr:write(" e.g.: lua pub_reader.lua dat001.eif " ..
eolib.ItemType.WEAPON .. " -- list weapons\n")
os.exit(1)
end
-- Read the file into a string.
local f, err = io.open(path, "rb")
if not f then
io.stderr:write("Cannot open file: " .. tostring(err) .. "\n")
os.exit(1)
end
local data = f:read("*a")
f:close()
-- Deserialize the EIF file.
local eif, deser_err = eolib.Eif.deserialize(data)
if not eif then
io.stderr:write("Failed to parse EIF: " .. tostring(deser_err) .. "\n")
os.exit(1)
end
print(string.format("EIF loaded: %d item(s) total", #eif.items))
print(string.format("Listing items of type %d:", filter))
-- Item index 1 is the placeholder record; real items start at index 2.
for i, item in ipairs(eif.items) do
if item.type == filter then
print(string.format(" id=%-5d type=%-4d name=%s",
i - 1, item.type, item.name or "(nil)"))
end
end
Note
Lua arrays are 1-based. eif.items[1] corresponds to EIF item ID 0 (the placeholder). To get the in-game item ID subtract 1 from the Lua index, as shown in the example above.

Running:

# Linux / macOS
lua5.4 pub_reader.lua dat001.eif 10 # list all weapons
lua5.4 pub_reader.lua dat001.eif 12 # list all armor
# Windows
lua pub_reader.lua dat001.eif 10

Common ItemType values:

Constant Value Meaning
eolib.ItemType.GENERAL 0 Miscellaneous item
eolib.ItemType.CURRENCY 2 In-game currency
eolib.ItemType.HEAL 3 Healing potion
eolib.ItemType.WEAPON 10 Weapon
eolib.ItemType.SHIELD 11 Shield
eolib.ItemType.ARMOR 12 Armor
eolib.ItemType.HAT 13 Hat
eolib.ItemType.BOOTS 14 Boots

Memory Safety

Lua's garbage collector manages memory automatically, so you generally do not need to free eolib objects explicitly. A few rules still apply:

EoWriter and EoReader are full Lua userdata objects. They are collected when no more Lua references point to them. Hold a local variable to keep them alive; let the variable go out of scope (or set it to nil) when you are done.

local writer = eolib.EoWriter.new()
writer:add_short(42)
local bytes = writer:to_byte_array()
writer = nil -- allow collection; or just let it fall out of scope

Deserialized protocol structs are plain Lua tables returned by deserialize(). They are collected like any other table once there are no more references to them.

String fields (e.g. pkt.username, record.name) are regular Lua strings interned by the VM — no special handling required.

Buffer lifetime for EoReader: When you create a reader from a Lua string the binding keeps an internal reference to that string, so the underlying bytes remain valid for the reader's lifetime. You do not need to hold onto the original string variable.

local reader = eolib.EoReader.new(io.read("*a"))
-- safe: binding holds the string reference internally
local action = reader:get_byte()

Next Steps