redstrate.com/content/blog/kawari10/index.md
Joshua Goins 8392e7c1e3
All checks were successful
Deploy / Deploy Website (push) Successful in 31s
Publish new post
2025-05-10 11:26:27 -04:00

7.9 KiB

title date draft tags series summary
Kawari Progress Report 10 - Polish 2025-05-10 false
FFXIV
Reverse Engineering
Kawari Progress Report
This update is all about polish and making Kawari easier to setup/use. What's the point of building a project that's difficult to setup?

This update is all about polish and making Kawari easier to setup/use. What's the point of building a project that's difficult to setup?

Better Artifacts

I have redone the structure of the artifacts, which was mostly putting the binaries in the top-level directory. The original web template files are now included, so they can be freely changed without recompiling:

How the ZIP file is setup now.

Additionally, there are now run scripts (that work!) for Windows and Linux to start all of Kawari's services. I have no clue how to write Batch scripts, so it ends up doing funny things like spawning a dozen console windows. If you're a Windows expert and know how to make that better, I would appreciate the help!

Better Web

I redone the web interface and slapped on Bootstrap styling since I'm too lazy to come up with my own CSS. It actually looks decently nice now:

The new account management webpage! The new login page!

Thanks to Sayaniku for reporting bugs for these pages, and those issues should be fixed like redirects not going to the right places.

Official Launcher

Kawari can now be used with the official launcher, making it super easy to use. For Windows, this only consists of dragging files into a folder - and Linux isn't that much harder. The setup instructions are located on the internal website:

The (hopefully easy to follow) setup instructions

What you're downloading is an auto-generated config file, pointing to the server names (like ffxiv.localhost) defined in your Kawari config. That is then read by our winmm.dll, enabling us to bypass the default SqEx URLs - effectively taking over the launcher. And here's the end result:

The official launcher doing something it was never meant to :)

This is powered by LauncherTweaks and it enables anyone to write their own custom launcher pages too1! Digging into the launcher has honestly been the most RE fun I've had in a while...

Scripting

I have expanded the breadth of things you can script, and I have it working making a variety of events work - such as:

{{< tube "https://tube.ryne.moe/videos/embed/qhrk2kdf13p4qLXPggqupB" >}}

Events used to be hard-coded but can now be added dynamically in the Global.lua script:

registerEvent(721028, "tosort/UnendingJourney.lua")
registerEvent(721044, "tosort/CrystalBell.lua")

Event IDs are now declared when you register the function, so you can reuse the same script across multiple IDs:

--- all of these are simple, so they can be handled by the same script!
registerEvent(131082, "common/GenericWarp.lua")
registerEvent(131083, "common/GenericWarp.lua")
registerEvent(131084, "common/GenericWarp.lua")

The ID can now be accessed inside of event scripts as EVENT_ID:

player:play_scene(target, EVENT_ID, 00000, 8192, 0)

And of course a breadth of new player API for you to use:

--- go to the pre-defined warp (and switch the zone if nessecary)
player:warp(2)

--- stop this event and give control back to the player
player:finish_event(EVENT_ID)

--- set the player's class/job
player:set_classjob(1)

--- set the player's position
player:set_position({ x = 1, y = 2, z = 3 })

--- set the player's remake flag
player:set_remake_mode("EditAppearance")

It's now possible to register new commands in Lua, I have already ported the !setpos command for example:

function onCommand(args, player)
    local parts = split(args)
    player:set_position({ x = tonumber(parts[1]), y = tonumber(parts[2]), z = tonumber(parts[3]) })
end

They are registered like how'd you expect:

registerCommand("setpos", "commands/debug/SetPos.lua")

Better Multiplayer

As seen in a previous update, multiplayer "worked" but it was bad. It only displayed the other player's position and their initial appearance. I improved it a bunch since then including propagating your rotation, targets and poses to other players:

{{< tube "https://tube.ryne.moe/videos/embed/ewr7CAABUjaRea46TQMR7Z" >}}

I struggled with sending player rotations for a while because they changed the packet used to update actor positions in Endwalker(?) After fixing that, everything fell into place. I also implemented very basic zone isolation, so people won't phase-shift into your zone.

Part of the reason why this takes a while is because Kawari was built under the assumption that there's only a single connection, so there's a lot of code written before I added multiplayer. A lot of my time is spent untangling that mess, and of course that work can't really be showcased here because it's super boring.

Fantasia

Everyone's favorite item/coping mechanism is now fully implemented, just for funsies. It doesn't remove the Fantasia from your inventory yet, because we lack the Lua API for it. Here it is in action:

{{< tube "https://tube.ryne.moe/videos/embed/kUE72k1gTCxXPPxf1k9f9r" >}}

Oh yeah, and the Armory Chest is now 100% functional too instead of crashing the World server.

Excel Data

This is just a technical tidbit, hence why it's at the end here. Kawari needs to read lots of Excel data to function properly, and the code to do so usually looks like this:

/// Returns the pop range object id that's associated with the warp id
pub fn get_warp(&mut self, warp_id: u32) -> (u32, u16) {
    let exh = self.game_data.read_excel_sheet_header("Warp").unwrap();

    let exd = self
        .game_data
        .read_excel_sheet("Warp", &exh, Language::English, 0)
        .unwrap();

    let ExcelRowKind::SingleRow(row) = &exd.get_row(warp_id).unwrap() else {
        panic!("Expected a single row!")
    };

    let physis::exd::ColumnData::UInt32(pop_range_id) = &row.columns[0] else {
        panic!("Unexpected type!");
    };

    let physis::exd::ColumnData::UInt16(zone_id) = &row.columns[1] else {
        panic!("Unexpected type!");
    };

    (*pop_range_id, *zone_id)
}

🤢 Yeah, that's bad! We are hardcoding column indices, and these also tend to change over time. There's also a whole lot of let x = else matching code, because column data is actually an enum. Our error handling here is also terrible with lots of panic!s, oh man!

I have spent some time cleaning up this technical debt, so now our Excel code looks like this:

/// Returns the pop range object id that's associated with the warp id
pub fn get_warp(&mut self, warp_id: u32) -> Option<(u32, u16)> {
    let sheet = WarpSheet::read_from(&mut self.game_data, Language::English)?;
    let row = sheet.get_row(warp_id)?;

    let pop_range_id = row.PopRange().into_u32()?;
    let zone_id = row.TerritoryType().into_u16()?;

    Some((*pop_range_id, *zone_id))
}

Some of this was fixed by improving the signature of the function itself (with Option!) Another part of it is introducing a higher-level API for grabbing Excel data. I now have a library called Icarus based on the fantastic schema in EXDSchema, generated with EXDGen. While it only supports a small portion of the schema, it's more than good enough for Kawari's purposes. Another improvement that helped was in Physis, where ColumnData now has into_x() helpers so you don't have to write match code yourself in the caller.


  1. If you are interested, I have some incomplete documentation here. You might also want to take a look at Kawari's launcher page HTML. ↩︎