Zone directory structure
Each zone has its own directory under scripts/zones/. A fully scripted zone typically contains:
scripts/zones/<ZoneName>/
Zone.lua # Zone-level handlers (zone-in, events, conquest)
IDs.lua # Text message, NPC, and mob ID constants
DefaultActions.lua # Default NPC events/messages for the interaction framework
npcs/ # One .lua file per NPC
Ailbeche.lua
Antonian.lua
...
mobs/ # One .lua file per notable mob or NM
Bigmouth_Billy.lua
...
Not every zone has all directories. Simpler zones may only have Zone.lua and IDs.lua.
IDs.lua — zone-local constants
IDs.lua defines the zones[xi.zone.<NAME>] table for a zone. This is the standard way to reference text message IDs, NPC entity IDs, and mob entity IDs without hardcoding magic numbers.
-- scripts/zones/Northern_San_dOria/IDs.lua
zones = zones or {}
zones[xi.zone.NORTHERN_SAN_DORIA] =
{
text =
{
HOMEPOINT_SET = 188, -- Home point set!
ITEM_CANNOT_BE_OBTAINED = 6593, -- You cannot obtain the <item>...
ITEM_OBTAINED = 6599, -- Obtained: <item>.
GIL_OBTAINED = 6600, -- Obtained <number> gil.
KEYITEM_OBTAINED = 6602, -- Obtained key item: <keyitem>.
},
npc =
{
EXPLORER_MOOGLE = 17737845,
},
mob =
{
-- mob entity IDs
},
}
All NPC and zone scripts load this table via:
local ID = zones[xi.zone.NORTHERN_SAN_DORIA]
-- then use ID.text.ITEM_OBTAINED, ID.npc.EXPLORER_MOOGLE, etc.
Zone.lua — zone handlers
Zone.lua returns a zoneObject table with handler functions. All handlers are optional; omit any that are not needed.
---@type TZone
local zoneObject = {}
-- Called once when the zone loads. Register trigger areas here.
zoneObject.onInitialize = function(zone)
xi.helm.initZone(zone, xi.helmType.LOGGING)
zone:registerCuboidTriggerArea(1, -7, -3, 110, 7, -1, 155)
end
-- Called when any player zones in. Return an event ID to play automatically,
-- or -1 (or nil) for no event.
zoneObject.onZoneIn = function(player, prevZone)
local cs = -1
if player:getXPos() == 0 and player:getYPos() == 0 and player:getZPos() == 0 then
player:setPos(86.131, -65.817, 273.861, 25)
end
return cs
end
-- Called when conquest tallying updates
zoneObject.onConquestUpdate = function(zone, updatetype, influence, owner, ranking, isConquestAlliance)
xi.conquest.onConquestUpdate(zone, updatetype, influence, owner, ranking, isConquestAlliance)
end
-- Called when a player enters a registered trigger area
zoneObject.onTriggerAreaEnter = function(player, triggerArea)
switch (triggerArea:getTriggerAreaID()): caseof
{
[1] = function()
player:startEvent(569)
end,
}
end
-- Called when a player leaves a registered trigger area
zoneObject.onTriggerAreaLeave = function(player, triggerArea)
end
-- Called during cutscene playback for mid-event updates
zoneObject.onEventUpdate = function(player, csid, option, npc)
end
-- Called when a cutscene/event finishes
zoneObject.onEventFinish = function(player, csid, option, npc)
if csid == 569 then
player:setPos(0, 0, -13, 192, 233)
end
end
return zoneObject
NPC scripts
Each NPC file in npcs/ returns a local entity table with handler functions. The file name must match the NPC’s in-game name.
-- scripts/zones/Northern_San_dOria/npcs/Ailbeche.lua
---@type TNpcEntity
local entity = {}
-- Called when a player triggers (talks to) the NPC
entity.onTrigger = function(player, npc)
local questStatus = player:getQuestStatus(xi.questLog.SANDORIA, xi.quest.id.sandoria.A_BOYS_DREAM)
if questStatus == xi.questStatus.QUEST_AVAILABLE then
player:startEvent(41)
end
end
-- Called when a player trades items to the NPC
entity.onTrade = function(player, npc, trade)
if npcUtil.tradeHasExactly(trade, xi.item.GIANT_SHELL_BUG) then
player:startEvent(15)
end
end
-- Called when an event/cutscene started by this NPC finishes
entity.onEventFinish = function(player, csid, option, npc)
if csid == 41 and option == 1 then
player:addQuest(xi.questLog.SANDORIA, xi.quest.id.sandoria.A_BOYS_DREAM)
player:setCharVar('aBoysDreamCS', 2)
end
end
return entity
NPC handler reference
| Handler | Signature | When called |
|---|
onTrigger | (player, npc) | Player talks to or examines the NPC |
onTrade | (player, npc, trade) | Player trades items to the NPC |
onEventFinish | (player, csid, option, npc) | A cutscene started by this NPC finishes |
onEventUpdate | (player, csid, option, npc) | Mid-event update during a cutscene |
onTimer | (npc) | NPC timer fires (set via npc:timer()) |
DefaultActions.lua — default NPC dialogue
The interaction framework automatically lowers the priority of default NPC responses so that quest/mission dialogue always takes precedence. Default actions are defined in DefaultActions.lua using the action table shorthand:
-- scripts/zones/Northern_San_dOria/DefaultActions.lua
local ID = zones[xi.zone.NORTHERN_SAN_DORIA]
return {
['Ailbeche'] = { event = 868 }, -- plays event 868
['Maurinne'] = { text = ID.text.MAURINNE_DIALOG }, -- shows text message
['_6fc'] = { messageSpecial = ID.text.ITS_LOCKED }, -- plays special message
}
See actions and handlers for the full shorthand syntax.
Any NPC listed in DefaultActions.lua does not need its own file in npcs/ unless it also participates in quests, missions, or other logic.
Trigger areas
Zones can define 3D trigger regions that fire onTriggerAreaEnter and onTriggerAreaLeave. Registered in onInitialize:
-- Cuboid trigger area (id, x1, y1, z1, x2, y2, z2)
zone:registerCuboidTriggerArea(1, -7, -3, 110, 7, -1, 155)
-- Cylindrical trigger area (id, centerX, centerZ, radius)
zone:registerCylindricalTriggerArea(2, 146.420, 127.601, 10)
The trigger area ID is passed to onTriggerAreaEnter as triggerArea:getTriggerAreaID().