Files
mod-ale/docs/IMPL_DETAILS.md
2025-11-09 15:34:33 +01:00

12 KiB

⚙️ ALE Implementation Details

Advanced features and technical documentation for ALE

Discord AzerothCore


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.ext loads before script.lua

Tip

Instead of using .ext, prefer the standard Lua require() 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

  1. Cache at Startup: Load data once during server start or script load
  2. Use Tables: Store frequently accessed data in Lua tables
  3. Batch Operations: Combine multiple queries when possible
  4. Async When Possible: Use Execute instead of Query if 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

  1. Start Small: Test basic functionality first
  2. Add Gradually: Implement features one at a time
  3. Test Each Step: Verify each addition works before moving on
  4. Use Reload: Use .reload ale for quick iteration (dev only)
  5. Full Restart: Always do final testing with a server restart

Common Issues

Objects becoming nil:

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.


Developed with ❤️ by the AzerothCore and ALE community

⬆ Back to Top