---
title: "Kawari Progress Report 7 - Scripting"
date: 2025-03-29
draft: false
tags:
- FFXIV
- Reverse Engineering
series:
- Kawari Progress Report
summary: "In this update: I added server-side scripting, touched on the client-side scripts it hooks into, and started playing cutscenes!"
---

In this update: I added server-side scripting, touched on the client-side scripts it hooks into, and started playing cutscenes!

# Server-side scripting

As I said in a previous update, I don't plan on implementing much - if any - retail content such as quests, story, etc. Because the game is multiplayer, most of the game's "content" is made up of server-side scripting. This is something we don't (and will never) have access to, and I doubt anyone could accurately recreate the entire retail experience[^1].

...but someone else may be interested in taking on that task! And we don't _have_ to recreate retail exactly 1:1 either. I think it might be cool to allow people to script their own content, and it opens up the possibility for anyone to be a Game Designer (outside of an office in Tokyo.)

I chose Lua as my choice of scripting language in Kawari for a couple of reasons, in order of importance:
1. It doesn't require recompiling the server to make changes.
2. It's relatively simple to implement, as there is the fantastic [mlua](https://crates.io/crates/mlua) crate I can use.
3. The retail server most likely uses Lua, considering the client-side scripting uses it as well.

And so I have begun implementing and hooking up systems to the new scripting API. Here's a (stubbed) script to play the Ul'dah opening:

```lua
--- TODO: find a way to hardcode it this way
EVENT_ID = 1245187

--- TODO: load defines from Opening Excel sheet, which has this and we don't need to hardcode it
POS_START = 4101669

function Scene00000(player)
    player:play_scene(EVENT_ID, 00000, 4959237, 1)
end

function Scene00001(player)
    player:play_scene(EVENT_ID, 00001, 4959237, 1)
end

function onEnterTerritory(player, zone)
    --- move the player into the starting position
    start_pos = zone:get_pop_range(POS_START)
    player:set_position(start_pos)

    Scene00000(player);
end

function onSceneFinished(player, scene)
    if scene == 0 then
        Scene00001(player)
    end
end
```

There is still quite a bit of work to be done, such as grabbing the defines from Excel data instead of hard-coding values. I'm already really happy with the results though, Kawari is starting to feel less like a toy now!

# Client-side scripting

I also took a peek into the client's Lua system, but I didn't find much documentation online about how it actually works[^2]. Understanding the client's Lua scripts is essential in implementing them on the server, but I also think it's neat to see how events in the game function.

Starting from zero (and from the documented Lua functions in [FFXIVClientStructs](https://github.com/aers/FFXIVClientStructs/)), I figured out there is a bunch of Lua userdata that the client registers, such as `Pc`, `Chara`, `Actor` and `EventHandler`. I have started documenting their functions [here](https://docs.xiv.zone/concept/event-scripts/) and will continue to refine them as I discover more. I planned on doing a deep-dive into how event scripts work, but that ended up becoming too big of a project - so I might cover that in a future blog post instead.

# Starting Zones

There is a couple of curiosities surrounding how the game's opening works. The starting city you choose isn't _actually_ the city proper. It's a completely separate zone, but I don't know why that is. There's a bunch of things torn out, some invisible walls put in and it loads in much faster in Kawari.

Another thing that Kawari does differently to other servers is how it decides where to put the player. Most have it hard-coded to the initial position, but I noticed a curiosity in the `Opening` Excel sheet - there is a Lua define called `POS_START`! And indeed, this matches up with a PopRange[^3] on the starting map. This seems to match retail in every starting zone except Gridania, not sure why that is yet.

I also wondered why the client knows the starting position PopRange, despite the server being in control of where to put the client. (`POS_START` doesn't seem to be referenced in the client's `OpeningUldah` script either.) I think is a side-effect of how scripts are authored. Keeping several constants in sync between the client & server scripts (Event NPCs, locations, and more) is probably really annoying, so they end up sharing the same set of data.

# Events

Now with Lua scripts powering the server's event system - and the packets needed to actually play them - I can play the openings in each City-State. Here is the Ul'dah version:

{{< tube "https://tube.ryne.moe/videos/embed/1c2a7592-4e15-494b-8dd5-381f2b358436" >}}

The other ones are about just as scripted, it stops after the initial system message and there's no way to stop the event yet. This locks up the client, and you need to restart the client or the World server.

[^1]: To be clear, I think that's okay as the game is still being sold and paying developer's salaries.

[^2]: I *didn't* really discover anything new - people have been dissecting the game's Lua code for years. It's just that I didn't find much apart from the tools or plugins people made.

[^3]: A map object that defines where it put players (among other things), such as where you end up when traveling between zones.
