Important
This document covers advanced implementation details and best practices for ALE (AzerothCore Lua Engine). For basic usage, see the Usage Guide.
ALE settings are located in the AzerothCore server configuration file.
Warning
Important: Always use the new configuration file generated after compiling with ALE. Without it, error logging and output may not function correctly.
- Enable/Disable ALE: Toggle the Lua engine on or off
- Traceback Function: Enable detailed debug information in error messages
- Script Folder Location: Configure where ALE looks for script files
- Logging Settings: Control log verbosity and output destinations
Reload scripts during development with:
.reload ale
Caution
Development Only: This command is for testing purposes only. For production use or troubleshooting, always restart the server.
Limitations:
- Events are not re-triggered for existing entities (e.g., logged-in players)
- Some state may persist from the previous load
- Race conditions may occur with active scripts
- Default Folder:
lua_scripts(configurable in server config) - Hidden Folders: Ignored during loading
- File Names: Must be unique across all subdirectories
- Loading Order: Not guaranteed to be alphabetical
Files with .ext extension load before standard .lua files:
init.extloads beforescript.lua
Tip
Instead of using .ext, prefer the standard Lua require() function for better maintainability.
The entire script folder structure is added to Lua's require path:
-- Require file: lua_scripts/utilities/helpers.lua
require("utilities/helpers")
-- Or simply (if in root)
require("helpers")Note: Omit the .lua extension when using require().
In C++, you must explicitly cast between types:
Unit* unit = ...;
Player* player = unit->ToPlayer(); // Manual cast requiredIn ALE, this happens automatically:
-- unit is automatically converted to the most specific type
-- No manual casting needed!
local name = unit:GetName() -- Works for Unit, Player, Creature, etc.All objects are automatically converted to their most specific type, giving you full access to all available methods.
Caution
Critical: Never store C++-managed userdata objects in global variables or across events!
C++ manages object lifetimes. A stored pointer can become invalid when:
- A player logs out
- A creature despawns
- An object is deleted by the core
Accessing invalid pointers causes crashes.
Objects are automatically set to nil when they become unsafe (usually when the hook function ends).
Instead of storing objects:
-- ❌ WRONG: Don't do this
local savedPlayer = nil
local function OnLogin(event, player)
savedPlayer = player -- Bad! Will be nil after function ends
end
local function OnLogout(event, player)
savedPlayer:SendMessage("Test") -- CRASH! savedPlayer is nil
endStore GUIDs instead:
-- ✅ CORRECT: Store GUID and retrieve object when needed
local playerGUID = nil
local function OnLogin(event, player)
playerGUID = player:GetGUID()
end
local function SomeLaterEvent(event, ...)
local player = GetPlayerByGUID(playerGUID)
if player then
player:SendMessage("Test") -- Safe!
end
endThese userdata objects are Lua-managed and safe to store:
- Query results (
ALEQuery) - World packets (
WorldPacket) - 64-bit numbers (
uint64,int64)
All userdata objects implement tostring:
print(player) -- Outputs: Player (Name: "John", GUID: 123456)
print(creature) -- Outputs: Creature (Entry: 1234, GUID: 789012)Each class has a global table containing its methods:
-- These global tables exist:
Player = { GetName = function(...) end, ... }
Creature = { GetEntry = function(...) end, ... }
GameObject = { GetDisplayId = function(...) end, ... }You can extend classes with custom methods:
function Player:CustomGreeting()
self:SendBroadcastMessage("Welcome, " .. self:GetName() .. "!")
end
function GameObject:IsChest()
return self:GetGoType() == 3
end
-- Usage:
player:CustomGreeting()
if gameobject:IsChest() then
print("Found a chest!")
endWarning
Avoid modifying or deleting global class tables in normal code, as this can break other scripts.
Important
Database queries are slow! The entire server waits while data is fetched from disk.
Use Execute for non-SELECT queries:
-- Asynchronous - doesn't block server
WorldDBExecute("UPDATE creature SET level = 80 WHERE entry = 1234")Use Query only when you need results:
-- Synchronous - blocks server until complete
local result = WorldDBQuery("SELECT name FROM creature_template WHERE entry = 1234")- Cache at Startup: Load data once during server start or script load
- Use Tables: Store frequently accessed data in Lua tables
- Batch Operations: Combine multiple queries when possible
- Async When Possible: Use
Executeinstead ofQueryif you don't need results
-- ✅ Good: Cache data at startup
local creatureNames = {}
local function LoadCreatureNames()
local query = WorldDBQuery("SELECT entry, name FROM creature_template")
if query then
repeat
local entry = query:GetUInt32(0)
local name = query:GetString(1)
creatureNames[entry] = name
until not query:NextRow()
end
end
-- Call once at server start
RegisterServerEvent(33, LoadCreatureNames) -- SERVER_EVENT_ON_CONFIG_LOAD
-- Now use cached data
local function OnSpawn(event, creature)
local name = creatureNames[creature:GetEntry()]
print("Spawned:", name)
endCaution
Critical: Use the correct getter function for each database type!
MySQL performs math in specific formats. Using the wrong getter can return incorrect values on different systems.
| Base Type | Defined Type | Database Type | Query Getter |
|---|---|---|---|
| char | int8 | tinyint(3) | GetInt8() |
| short int | int16 | smallint(5) | GetInt16() |
| (long int / int) | int32 | mediumint(8) | GetInt32() |
| (long int / int) | int32 | int(10) | GetInt32() |
| long long int | int64 | bigint(20) | GetInt64() |
| unsigned char | uint8 | tinyint(3) unsigned | GetUInt8() |
| unsigned short int | uint16 | smallint(5) unsigned | GetUInt16() |
| unsigned (long int / int) | uint32 | mediumint(8) unsigned | GetUInt32() |
| unsigned (long int / int) | uint32 | int(10) unsigned | GetUInt32() |
| unsigned long long int | uint64 | bigint(20) unsigned | GetUInt64() |
| float | float | float | GetFloat() |
| double | double | double, decimal | GetDouble() |
| std::string | std::string | varchar, text, etc. | GetString() |
-- ❌ WRONG: Can return 0 or 1 depending on system
local result = WorldDBQuery("SELECT 1")
local value = result:GetUInt32(0) -- Incorrect type!
-- ✅ CORRECT: Always returns 1
local result = WorldDBQuery("SELECT 1")
local value = result:GetInt64(0) -- Correct type for literal numbers-- ✅ Fast: Local variables
local count = 0
for i = 1, 1000 do
count = count + 1
end
-- ❌ Slow: Global variables
count = 0
for i = 1, 1000 do
count = count + 1
end-- ❌ Avoid: Creating tables in loops
for i = 1, 1000 do
local data = {i, i*2, i*3} -- 1000 table allocations!
end
-- ✅ Better: Reuse tables
local data = {}
for i = 1, 1000 do
data[1], data[2], data[3] = i, i*2, i*3
end-- ❌ Avoid: Repeated method calls
for i = 1, 100 do
player:GetName() -- Calls C++ function 100 times
end
-- ✅ Better: Cache the value
local playerName = player:GetName()
for i = 1, 100 do
-- Use playerName
end-- ❌ Bad: Query in a loop
for entry = 1, 100 do
local query = WorldDBQuery("SELECT name FROM creature_template WHERE entry = " .. entry)
end
-- ✅ Good: Single query with IN clause
local query = WorldDBQuery("SELECT entry, name FROM creature_template WHERE entry BETWEEN 1 AND 100")-- Basic output
print("Debug: Function called")
-- With variables
print("Player:", player:GetName(), "Level:", player:GetLevel())
-- Object inspection
print(player) -- Uses tostring metamethodCheck these locations for errors:
- Server Console: Real-time output
- Log File: Persistent record in server folder
Enable traceback in the server config for detailed error information:
ALE.TraceBack = 1
This adds call stack information to errors.
- Start Small: Test basic functionality first
- Add Gradually: Implement features one at a time
- Test Each Step: Verify each addition works before moving on
- Use Reload: Use
.reload alefor quick iteration (dev only) - Full Restart: Always do final testing with a server restart
Objects becoming nil:
- You're storing userdata objects instead of GUIDs
- See Storing Userdata Objects
Wrong database values:
- Using incorrect getter function for database type
- See Database Types
Script not loading:
- Check for duplicate filenames
- Check log for syntax errors
- Verify script folder configuration
ALE is built upon the foundation of the Eluna Lua Engine. We acknowledge and thank the Eluna team for their pioneering work in Lua scripting for World of Warcraft server emulators.