RSS Amplifier

redstrate's Blog · Nov 26, 2025

Exploring dialogue scripting in FFXIV

0
Sign in to vote or save

redstrate · redstrate.com

Posted on
Tags: FFXIV Modding My Favorites

Have you ever wondered how dialogue scenes work in-game? Regardless of your answer, this short video may pique your interest:

Yes, that’s actually a custom dialogue event! 👀 I’ll explain how this was accomplished this near the end - but first, I wanted to go over how events work at a high-level so that you can appreciate the engineering behind the game’s event system.

What is an event? #

Events are a collection of scenes, scripted with Lua to perform various functions. With only a few lines of Lua code, you can script entire sequences with built-in canned animations and UI elements. Their event system is surprisingly flexible, and their usage isn’t limited to only dialogue. For example, events are used to power the Aetheryte menus:

If you interact with an object in the world and it happens to speak, or open a menu that looks like the above - there’s a good chance it’s powered by the event system! This system can also be used to manage of a series of events, such as the game’s opening.

Are events client-side or server-side? #

The client has the knowledge of how scenes play out, including character movement and translated lines. But the server is the one who says which scenes are played, in what order and when (if at all.) Basically: think of the server as the “director”, but all of the “acting” is done by the client. The server should have no knowledge of what dialogue is played, only which scene leads into the next.

In a technical sense, this is our general understanding:

  • When talking to NPCs (in our case) the client sends a packet indicating which event to start.
  • This event is then handled by the server, which decides which scenes to start.
  • Once the scene is “finished” playing on the client-side, the server is informed via another packet.

This can go on quite a while, as you would imagine - a constant duet between the actor and director. But in our case, it would be something like:

  1. Start Talk Event (-> Client)
  2. Play Event Scene (-> Client)
  3. Event Has Returned (-> Server)
  4. Event Finishing (-> Client)

There are certain other things decided by the server for… reasons? This includes which scene elements are visible, for example.

Wait - what are scenes? #

Scenes are simply Lua functions that are called, but its not too important to know why you need to chain together multiple scenes. But the structure, order and number of scenes can be mostly arbitrary and left up to the designer. We’ll go over an example of how to write a scene next!

What does the Lua script look like? #

Here’s a small snippet of the script used in the demonstration video. I’m only including the text portions for brevity, and they’re also simpler to understand:

WarpInnGridania.SCRIPT_VERSION = 1

WarpInnGridania.IsAcceptEvent = (function(l_1_0, l_1_1, l_1_2, l_1_3, l_1_4, l_1_5)
    -- Always show this NPC
    return true
end)

WarpInnGridania.OnScene00000 = (function(event, sourceActor, targetActor)
    -- I cut out a bunch of the camera movement and other actions seen in the video.

    -- Begin our dialogue!
    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_FACIAL_SMILE)
    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_EVENT_GREETING)
    sourceActor:Talk(sourceActor, event, "Hi! This is a demonstration of a custom dialogue event. These are written in Lua, using the same API the game already has.", true)

    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_EVENT_JOY_BIG)
    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_FACIAL_SMILE)
    sourceActor:Talk(sourceActor, event, "As you can see, I can say whatever I want here! And this can be done without modifying any of the game's files.", true)

    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_EMOTE_PSYCH)
    sourceActor:Talk(sourceActor, event, "I also begun documenting the Lua API for these, which is interesting. At least to me!", true)

    sourceActor:PlayActionTimeline(event.ACTION_TIMELINE_EMOTE_GOODBYE)
    sourceActor:Talk(sourceActor, event, "If you're watching the video by itself, there's a link with more information in the description.", true)
end)

Throughout the script we’ll be referring to an internal Lua class name, in our case WarpInnGridania. This name can be completely arbitrary, but it’s not declared by us in the script. The game injects a custom class with that name, which includes a ton of useful state and functions I’ll skip over for now. But as you can see we have functions to play animations (PlayActionTimeline) and talk (Talk).

One of the functions we define is IsAcceptEvent. This is called when the game initially loads the NPC, to decide the NPC’s visibility. In our case we always want the NPC to be present, so we return true.

WarpInnGridania.IsAcceptEvent = (function(l_1_0, l_1_1, l_1_2, l_1_3, l_1_4, l_1_5)
  -- Always show this NPC
  return true
end)

The next function is something we went over already, defining a scene! This is declaring we are handling Scene 0 (but its always padded to five digits.) The function body is called when the server tells us to play that scene.

WarpInnGridania.OnScene00000 = (function(event, sourceActor, targetActor)
   -- I can do anything in here: speak, move a character, or maybe start a camera dolly!
end)

In case you’re wondering about the constants (ACTION_TIMELINE_EVENT_GREETING): these are defined in Excel sheets, but the idea is behind them is to use re-usable IDs instead of hard-coding magic numbers everywhere which is pretty smart.

How did you create the custom event? #

First, I needed to understand how event designers write the scripts to begin with. The game ships binary Lua bytecode, so it’s not possible to see the original Lua script unfortunately. It turns out it’s easy enough to decompile the bytecode into something that resembles the existing script though! I accomplished this through my own tooling called Novus which has a built-in Lua decompiler (based on luadec51):

This made exploring the game’s existing scripts trivially easy, which I needed to figure out enough of the API to create the dialogue event I demonstrated. But we depend on both the client and server portions to work, so I had to implement the event system in my server emulator Kawari. The server also doubled as a safe testbed, and then I created a plugin called Custom Theater to easily replace in-game events:

This plugin is still in its infancy, so don’t expect it to be perfect. In case you want to read more about this system, I’m currently in the middle of documenting the entire thing (including the API) on wiki.xiv.zone. I hope my plugin and documentation is useful to someone else!


I’ve been slowly working out this “how does FFXIV events work” idea for months now, so I’m happy to publish this 🥱 The final piece of the puzzle was writing custom events actually, which I thought would be harder to do. There was so much stuff I had to cut because I was afraid it would be too boring/off-topic, like the game’s other event handlers, injected variables and server-side scripting.

I do plan on expanding more on this topic in the future, so we’ll see…

Share on Mastodon!

See Also #

Read the original on redstrate.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.