Skip to main content
Every NPC handler inside a framework section must return an action object. The action object tells the framework what to do and at what priority.

Action types

Actions are created from the container object (quest, mission, etc.):
-- Play a regular event (cutscene ID)
quest:event(csid)

-- Play an event and mark it as a quest-progress event (highest priority)
quest:progressEvent(csid)

-- Same as progressEvent
quest:priorityEvent(csid)

-- Play a cutscene (visually distinct from a regular event)
quest:cutscene(csid)

-- Play a progress cutscene
quest:progressCutscene(csid)

-- Show a text message from the NPC
quest:message(messageId)

-- Show a message with high enough priority to replace the NPC file's default
quest:replaceMessage(messageId)

-- Give the player a key item (with an implicit message)
quest:keyItem(keyItemId)

-- Play a sequence of messages, face directions, and waits
quest:sequence(
    { text = 11470, wait = 1000 },
    { text = 11471, face = 82, wait = 2000 },
    { face = 115 }
)

-- Do nothing (suppress default action)
quest:noAction()

Event parameters

Some events take additional parameters passed to the C++ startEvent call:
quest:event(csid, { [2] = 555 })       -- pass extra params table
quest:progressEvent(csid, option1, option2)

Action modifiers

Modifiers are chained on any action object. Each modifier returns the modified action.
-- Elevate to highest priority (equivalent to progressEvent)
quest:event(100):progress()

-- High priority the first time the NPC is interacted with,
-- lower priority on subsequent interactions
quest:event(100):importantOnce()

-- Execute only once per zone-in (resets on zone change)
quest:event(100):oncePerZone()

-- High enough priority to override the NPC file's handler
quest:event(100):replaceDefault()

-- Turn a regular event into a cutscene
quest:event(100):cutscene()

-- Make the NPC face the player (or another entity) before showing message
quest:message(456):face(player)
Modifiers can be chained:
quest:event(100):cutscene():progress()

Compact handler syntax

When an NPC only needs one handler, you can use shorthand to reduce nesting:
-- These four are equivalent:
['Some_NPC'] = quest:event(200),

['Some_NPC'] = { event = 200 },

['Some_NPC'] = {
    onTrigger = quest:event(200),
},

['Some_NPC'] = {
    onTrigger = function(player, npc)
        return quest:event(200)
    end,
},
Use the function form when you need conditionals.

Action table shorthand

In places where a container object is not available (like DefaultActions.lua), use the table shorthand format:
-- Events
{ event = 123 }                          -- quest:event(123)
{ event = 123, progress = true }         -- quest:progressEvent(123)
{ cutscene = 123 }                       -- quest:cutscene(123)
{ event = 123, options = { [2] = 555 } } -- quest:event(123, { [2] = 555 })

-- Messages
{ text = 456 }                           -- quest:message(456)
{ message = 456 }                        -- quest:message(456)
{ messageSpecial = 456 }                 -- messageSpecial variant

-- Sequence (outer array of step tables)
{
    { text = 11470, wait = 1000 },
    { text = 11471, face = 82, wait = 2000 },
    { face = 115 },
}
The sequence above plays message 11470, waits 1 second, plays message 11471 while facing direction 82, waits 2 seconds, then faces direction 115.

Handler signatures

All handler functions inside a section follow fixed signatures.

NPC handlers

-- Player talks to or examines the NPC
onTrigger = function(player, npc)
    return quest:event(100)
end,

-- Player trades items to the NPC
onTrade = function(player, npc, trade)
    if npcUtil.tradeHasExactly(trade, xi.items.SOME_ITEM) then
        return quest:progressEvent(111)
    end
end,

Event finish handlers

onEventFinish is a table indexed by event/cutscene ID:
onEventFinish =
{
    [101] = function(player, csid, option, npc)
        -- option is the cutscene choice (0, 1, 2, ...)
        quest:setVar(player, 'Prog', 1)
    end,

    [111] = function(player, csid, option, npc)
        quest:complete(player)
    end,
},

Zone-in handler

onZoneIn is a list with a single function. Return an event ID to auto-play it, or nil/negative for no event:
onZoneIn =
{
    function(player, prevZone)
        if quest:getVar(player, 'Prog') == 3 then
            return 543  -- auto-play event 543 on zone-in
        end
    end,
},

Region enter handler

onRegionEnter is indexed by trigger area ID:
onRegionEnter =
{
    [2] = function(player, triggerArea)
        quest:setVar(player, 'Prog', 3)
    end,
},

DefaultActions.lua example

DefaultActions.lua uses the table shorthand exclusively since no container is in scope:
local ID = zones[xi.zone.NORTHERN_SAN_DORIA]

return {
    ['Ailbeche']  = { event = 868 },
    ['Maurinne']  = { text  = ID.text.MAURINNE_DIALOG },
    ['_6fc']      = { messageSpecial = ID.text.ITS_LOCKED },
}
Default actions have the lowest priority. A quest or mission that targets the same NPC will always take precedence over its default action entry.

Build docs developers (and LLMs) love