12 KiB
Important
This document covers advanced implementation details and best practices for ALE (AzerothCore Lua Engine). For basic usage, see the Usage Guide.
📋 Table of Contents
⚙️ Configuration
Server Configuration File
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.
Available Settings
- 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
🔄 Script Management
Script Reloading
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
Script Loading
Default Behavior
- 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
Load Priority
Files with .ext extension load before standard .lua files:
init.extloads beforescript.lua
Tip
Instead of using
.ext, prefer the standard Luarequire()function for better maintainability.
Using Require
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().
🎯 Advanced Features
Automatic Type Conversion
In C++, you must explicitly cast between types:
Unit* unit = ...;
Player* player = unit->ToPlayer(); // Manual cast required
In 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.
Storing Userdata Objects
Caution
Critical: Never store C++-managed userdata objects in global variables or across events!
The Problem
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.
The Solution
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
end
Store 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
end
Safe to Store
These userdata objects are Lua-managed and safe to store:
- Query results (
ALEQuery) - World packets (
WorldPacket) - 64-bit numbers (
uint64,int64)
Userdata Metamethods
ToString Support
All userdata objects implement tostring:
print(player) -- Outputs: Player (Name: "John", GUID: 123456)
print(creature) -- Outputs: Creature (Entry: 1234, GUID: 789012)
Global Metatables
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, ... }
Custom Methods
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!")
end
Warning
Avoid modifying or deleting global class tables in normal code, as this can break other scripts.
🗄️ Database Integration
Query Performance
Important
Database queries are slow! The entire server waits while data is fetched from disk.
Synchronous vs Asynchronous
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")
Best Practices
- 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)
end
Database Types
Caution
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.
Type Mapping Table
| 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() |
Example
-- ❌ 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
⚡ Performance Tips
Variable Scope
-- ✅ 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
Table Efficiency
-- ❌ 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
Cache Frequently Used Values
-- ❌ 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
Minimize Database Access
-- ❌ 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")
🐛 Debugging
Print Debugging
-- Basic output
print("Debug: Function called")
-- With variables
print("Player:", player:GetName(), "Level:", player:GetLevel())
-- Object inspection
print(player) -- Uses tostring metamethod
Error Logs
Check these locations for errors:
- Server Console: Real-time output
- Log File: Persistent record in server folder
Traceback
Enable traceback in the server config for detailed error information:
ALE.TraceBack = 1
This adds call stack information to errors.
Incremental Testing
- 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
Common Issues
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
🌟 Acknowledgements
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.