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
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:
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