Skip to main content

Overview

LandSandBoat uses sol2 to expose C++ game objects to Lua scripts. The binding layer lives in src/map/lua/ and consists of:
  • CLua* wrapper classes — thin wrappers around C++ game objects that expose methods to Lua
  • sol_bindings.h/cpp — macro infrastructure and sol_lua_push overloads that allow C++ to pass objects to Lua
  • luautils.h/cpp — the global Lua state, event dispatch functions, and utility helpers
The global sol::state lua is declared in luautils.h and is the single Lua VM that all scripts share.

How bindings work

Each C++ game class that Lua scripts can touch has a corresponding CLua* wrapper:
C++ class          Lua wrapper          Lua type name
─────────────────────────────────────────────────────
CBaseEntity    →   CLuaBaseEntity   →   "BaseEntity"
CZone          →   CLuaZone         →   "Zone"
CItem          →   CLuaItem         →   "Item"
CSpell         →   CLuaSpell        →   "Spell"
CStatusEffect  →   CLuaStatusEffect →   "StatusEffect"
CBattlefield   →   CLuaBattlefield  →   "Battlefield"
CInstance      →   CLuaInstance     →   "Instance"

sol_bindings macros

sol_bindings.h defines convenience macros for registering types and their inheritance:
// Register a new Lua usertype
SOL_USERTYPE("BaseEntity", CLuaBaseEntity);

// Register a usertype that inherits from another
SOL_USERTYPE_INHERIT("CharEntity", CLuaBaseEntity, CLuaBaseEntity);

// Register a method on the current type
SOL_REGISTER("getID", CLuaBaseEntity::getID);

// Register a read-only property
SOL_READONLY("id", CLuaBaseEntity::getID);

Pushing objects from C++ to Lua

When C++ needs to pass a game object to a Lua callback, sol_lua_push overloads handle the conversion. The SOL_BIND_DEF macro generates these:
// sol_bindings.h — generated declarations
SOL_BIND_DEC(CLuaBaseEntity, CBaseEntity);

// For subclasses that should be pushed as the base Lua type:
SOL_BIND_DEC_SUB(CLuaBaseEntity, CBaseEntity, CCharEntity);
SOL_BIND_DEC_SUB(CLuaBaseEntity, CBaseEntity, CMobEntity);
SOL_BIND_DEC_SUB(CLuaBaseEntity, CBaseEntity, CNpcEntity);
This means that when C++ calls a Lua function and passes a CCharEntity*, it automatically arrives in Lua as a BaseEntity with all CLuaBaseEntity methods available.

Binding files reference

Files: lua_baseentity.h/cppThe largest binding file. CLuaBaseEntity wraps CBaseEntity and provides methods available to all entity types (characters, mobs, NPCs, pets, trusts):
  • Identity: getID(), getName(), getEntityType()
  • Position: getPos(), setPos(), getZone()
  • Stats: getHP(), getMP(), getTP(), getMainJob(), getMainLvl()
  • Status effects: addStatusEffect(), delStatusEffect(), hasStatusEffect()
  • Inventory: addItem(), hasItem(), delItem()
  • Flags and variables: getCharVar(), setCharVar()
  • Events: startEvent(), updateEvent(), release()
Character subtype (CCharEntity) and mob subtype (CMobEntity) both arrive in Lua as BaseEntity.
Files: lua_zone.h/cppCLuaZone wraps CZone and provides:
  • Local variables: getLocalVar(), setLocalVar(), resetLocalVars()
  • Trigger areas: registerCuboidTriggerArea(), registerCylindricalTriggerArea()
  • Entity queries via the zone object
Files: lua_item.h/cppCLuaItem wraps CItem and its subtypes (CItemEquipment, CItemWeapon, CItemUsable, CItemFurnishing, etc.):
  • Item identity: getID(), getName(), getStackSize()
  • Equipment: slot, level requirements, jobs allowed
  • Usable items: charges, cast time, recast time
Files: lua_spell.h/cppCLuaSpell wraps CSpell:
  • getID(), getName(), getSkillType()
  • Target and AoE information
  • Element, MP cost, cast and recast times
  • Interrupted and reflected state
Files: lua_statuseffect.h/cppCLuaStatusEffect wraps CStatusEffect:
  • getStatusID(), getPower(), getTick(), getDuration()
  • setDuration(), setPower(), setSubPower()
  • Source entity, tier, and flag access
Files: lua_battlefield.h/cppCLuaBattlefield wraps CBattlefield (BCNM/ISNM/ENM content):
  • getID(), getStatus(), setStatus()
  • getTimeLimit(), getTimeInside()
  • addEnemy(), getLocalVar(), setLocalVar()
  • Win/loss/time-limit condition triggers
Files: lua_instance.h/cppCLuaInstance wraps CInstance (instanced zone content):
  • getID(), getStage(), setStage()
  • Progress tracking, time remaining
  • Entity spawning within the instance
  • CLuaAbility — job ability methods (ID, type, range, area)
  • CLuaMobSkill — mob skill methods (ID, range, target type)
  • CLuaWeaponSkill — weapon skill methods (ID, skill type, TP cost)
  • CLuaPetSkill — pet skill methods
Files: lua_trade_container.h/cppCLuaTradeContainer wraps CTradeContainer:
  • getItem(), getItemCount(), hasItemQty()
  • Trade slot iteration

luautils — C++ to Lua dispatch

luautils.h/cpp is the bridge that allows C++ to call Lua functions. It declares the global sol::state lua and provides:

callGlobal — calling Lua from C++

// Call a void global function
luautils::callGlobal<void>("xi.server.onTimeServerTick");

// Call with arguments
luautils::callGlobal<void>("xi.player.onPlayerDeath", PChar);

// Call and get a return value
auto value = luautils::callGlobal<uint32>("xi.server.functionThatReturnsANumber");
If the function is not found or throws an error, callGlobal logs the error and returns a default value.

Event dispatch

The luautils namespace exposes C++ functions for every game event that Lua scripts can handle:
// Zone lifecycle
void OnZoneIn(CCharEntity* PChar);
void OnZoneOut(CCharEntity* PChar);
void OnZoneInitialize(uint16 ZoneID);
void OnZoneTick(CZone* PZone);

// NPC interaction
int32 OnTrigger(CCharEntity* PChar, CBaseEntity* PNpc);
void  OnTrade(CCharEntity* PChar, CBaseEntity* PNpc);

// Mob events
void OnMobSpawn(CBaseEntity* PMob);
void OnMobEngage(CBaseEntity* PMob, CBaseEntity* PTarget);
void OnMobDeath(CBaseEntity* PMob, CBaseEntity* PKiller);

// Player events
void OnPlayerDeath(CCharEntity* PChar);
void OnPlayerLevelUp(CCharEntity* PChar);
void OnPlayerMount(CCharEntity* PChar);

// Spell/ability events
int32 OnSpellCast(CBattleEntity* PCaster, CBattleEntity* PTarget, CSpell* PSpell);
int32 OnUseAbility(CBattleEntity* PUser, CBattleEntity* PTarget, CAbility* PAbility, action_t* action);

Calling C++ from Lua

From a Lua script, C++-bound methods are called directly on the object:
-- NPC trigger handler in scripts/zones/South_Gustaberg/npcs/MyNPC.lua
local entity = {}

entity.onTrigger = function(player, npc)
    -- player is a CLuaBaseEntity wrapping CCharEntity
    local name = player:getName()        -- calls CLuaBaseEntity::getName()
    local level = player:getMainLvl()    -- calls CLuaBaseEntity::getMainLvl()
    local zone  = player:getZone()       -- returns a CLuaZone

    -- Start a cutscene event
    player:startEvent(100)
end

return entity
-- Mob death handler
local mob = {}

mob.onMobDeath = function(mob, player, optParams)
    -- Drop a key item on death
    if player then
        player:addKeyItem(xi.ki.FORBIDDEN_KEY)
        player:messageBasic(xi.msg.basic.KEYITEM_OBTAINED, xi.ki.FORBIDDEN_KEY)
    end
end

return mob

sol_bindings.cpp — registration

sol_bindings.cpp contains the actual sol_lua_push function bodies generated by the SOL_BIND_DEF and SOL_BIND_DEF_SUB macros. These are the functions that sol2 calls when C++ code passes a C++ pointer into a Lua function call. They ensure null pointers become nil in Lua and that subclass pointers are correctly presented as the base Lua type.

Build docs developers (and LLMs) love