Concepts
SqexArg
This is the “encrypted argument” format used by a lot of FFXIV programs and is used in place of regular plaintext command line arguments. However, this is barely a security measure and just prevents easily snooping stuff like your login token. Despite this, the SqexArg format is well known, reversible and easily breakable.
Format #
When viewing the command line arguments for, say ffxiv.exe you will see it’s only something like this:
//**sqex0003S2_Utl8qdamv3_82SH7Lhtk=S**//
(Yes, I did garble some of the text, so it’s not actually decodable :-))
There are three distinct parts of this string:
//**sqex0003S2_Utl8qdamv3_82SH7Lhtk=S**//
^^ ^
version|| |
| base64 string |
| checksum
Let’s break them down:
- version
- From what I’ve seen, this is always
3
. I’m not sure if there any more meaning behind this, apart from they revised this 3 times.
- From what I’ve seen, this is always
- base64 string
- This is your usual base64-encoded string. However, there is a couple of important things to note:
- Use the URL-safe version of Base64.
- You may omit the trailing equal.
- The result is unreadable garbage, but how to encode/decode this will be revealed below.
- This is your usual base64-encoded string. However, there is a couple of important things to note:
- checksum
- This is also covered in a later section, but this is always one character long and located at the end of the string.
Encryption Algorithm #
The resulting bytes when you decode the base64 string is going to Blowfish ECB encrypted.
- However, please note that Square Enix does some weird bitflip endian-encoding nonsense which means your out-of-box Blowfish library might not work. I would highly recommend reading up on some existing implementations to get an idea of what to do:
- XIVQuickLauncher (C#)
- Astra (C++)
- XIV-on-Mac (Swift)
- Before encrypting or decrypting, ensure the buffer is padded.
Note: In the new Steam launcher update, Square Enix has actually switched to a more sane version of Blowfish ECB without their weird changes. Please look at XIVQuickLauncher for the changes required, as I have not tested this yet.
Key #
The key used for encrypting/decrypting the encrypted arguments is just your system’s uptime clock. All FFXIV executables just call GetTickCount()
, and theres about ~50 or so ticks of freedom before the game or launcher considers it invalid. There is a specific transformation you need to do in order to fetch a valid key though:
unsigned int rawTicks = TickCount();
unsigned int ticks = rawTicks & 0xFFFFFFFFu;
unsigned int key = ticks & 0xFFFF0000u;
char buffer[9] = {};
sprintf(buffer, "%08x", key);
To make this easier, here is a couple of platform-specific implementations of TickCount()
. Thank you Wine for being easily searchable, as this is what Wine is actually doing under the hood to emulate GetTickCount()
, so these are exact and have been tested on Astra for all platforms.
Windows #
uint32_t TickCount() {
return GetTickCount();
}
macOS #
uint32_t TickCount() {
struct mach_timebase_info timebase;
mach_timebase_info(&timebase);
auto machtime = mach_continuous_time();
auto numer = uint64_t (timebase.numer);
auto denom = uint64_t(timebase.denom);
auto monotonic_time = machtime * numer / denom / 100;
return monotonic_time / 10000;
}
Linux #
uint32_t TickCount() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
}
Checksum #
If you’re just interested in decrypting game arguments, this is not essential. This is presumably used as a checksum when the game checks your encrypted string.
static char ChecksumTable[] = {
'f', 'X', '1', 'p', 'G', 't', 'd', 'S',
'5', 'C', 'A', 'P', '4', '_', 'V', 'L'
};
static char GetChecksum(unsigned int key) {
auto value = key & 0x000F0000;
return ChecksumTable[value >> 16];
}
Decrypting #
You can try the dedicated argcracker in Novus for this purpose. It allows you to easily crack any SqexArg enabled program assuming you have access to the string.
Notes #
Every instance where SqexArg is used, the first argument is always T
, where T
is set to the value of ticks
(as shown above). I’m not sure what the purpose of this really is, maybe for verifying the checksum?
The arguments (before encoding of course) must be formatted as " /%1 =%2"
. The extra space is required, even at the beginning of the arguments. Make sure that any spaces in your string is double padded as well.
See Also #
Implementations #
Logging into Sapphire Servers
Sapphire has a much, much easier login process than the official servers, which only consist of one or two requests.
Logging in #
POST http(s)://{sapphire_lobby_url}/sapphire-api/lobby/login
You’ll need to construct a JSON body as follows:
{
"username": {username},
"pass": {pass}
}
Of course, {username}
and {password}
is the user account credentials.
The response is also JSON, and if it’s not empty (which indicates a login error) it will be constructed like this:
{
"sId": {SID},
"lobbyHost": {lobby_host},
"frontierHost": {frontier_host}
}
Now you can launch the game! See ffxiv.exe for more arguments. For a quick rundown:
- Set
DEV.TestSID
to{SID}
. - Set
DEV.MaxEntitledExpansionID
to your desired expansion level. - Set
SYS.Region
to 3. - Set
DEV.GMServerHost
to{frontier_host}
. - Set
DEV.LobbyHost01
…DEV.LobbyHost09
to{lobby_host}
. - Set
DEV.LobbyPort01
…DEV.LobbyPort09
to your Sapphire lobby’s port - usually 54994.
Registering an account #
POST http(s)://{sapphire_lobby_url}/sapphire-api/lobby/createAccount
This request is identical to the one used for logging in, so refer to the section above for details.
Logging into Official Servers
This only covers logging in via non-Steam Square Enix accounts right now.
Logging into the official FFXIV servers is actually very simple, and all you need is the ability to send/receive HTTP requests, parse JSON responses and read some files off of the disk.
If you’re wondering about the safety of these calls, as long as you don’t do anything stupid (i.e. throw a 1 megabyte username into a form) then your account is safe. I guess Square Enix doesn’t care about these endpoints too much, because even if you log into the game legitimately a hundred times in an hour they don’t care. However, if you try logging into the account with invalid credentials, then it might get locked.
You’ll also notice the variable {unique_id}
used in some of the User Agents. This is a unique id used by the official launcher, for an unknown purpose. However, any sort of unique ID will work.
Checking the Gate Status #
First you must check the gate status from the Frontier server, which tells if the servers are under maintenance. The legitimate launcher will not allow you to log in if the gate is closed. Square Enix does not expect legitimate users to enter servers under maintenance, not that you even can.
GET https://frontier.ffxiv.com/worldStatus/gate_status.json
The response is a simple JSON as follows:
{
"status": 1
}
If the status
is 1, the gate is open and you’re free to log in. Any other value should indicate that you should not attempt to log in, the gate is “closed”.
Boot Update Check #
You also need to ensure that the boot components of the game are properly updated by contacting the boot patch server.
This is not a typo, and this endpoint is actually in plaintext HTTP…
GET http://patch-bootver.ffxiv.com/http/win32/ffxivneo_release_boot/{boot_version}
- User Agent:
FFXIV PATCH CLIENT
(macOS:FFXIV-MAC PATCH CLIENT
) - Host:
patch-bootver.ffxiv.com
{boot_version}
is the version stored in$GAME_DIR/boot/boot.ver
.
If you receive an empty response, then you don’t need to update any of your boot components and proceed to the next step. However if your boot components are out of date, you will receive a list of patches to update.
Getting STORED #
GET https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/top
- Query items:
lng
: “en” (even if the client isn’t actually English.)rgn
: 3 (even if this isn’t the client’s region.)isft
: 1 if attempting to log in with a free trial account, otherwise 0.cssmode
: 1isnew
: 1launchver
: 3issteam
: 1 is attempting to log in with a Steam service account.session_ticket
: The session ticket acquired from the Steamworks API.ticket_size
: The session ticket size.
- User Agent:
SQEXAuthor/2.0.0(Windows 6.2; ja-jp; {unique_id})
(macOS:macSQEXAuthor/2.0.0(MacOSX; ja-jp)
) - Accept:
image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*
- Accept-Encoding:
gzip, deflate
- Accept-Language:
en-us
The response is actually fully formed HTML, most likely better suited for the real launcher where it’s a web browser. However, if you have regex available, you can query the variables needed for later.
If you’re logging in with a Steam service account, you can find your username using `<input name=
To get the _STORED_
value, use \t<\s*input .* name="_STORED_" value="(?<stored>.*)">
and use the second captured variable. You also need to the store the full URL of this request (including all of the queries) for use in the next request.
If you get an error during this response, it may indicate that the Square Enix servers are down for maintenance.
Logging in #
Now it’s time to perform the first step of logging in:
POST https://ffxiv-login.square-enix.com/oauth/ffxivarr/login/login.send
- Query items:
_STORED_
: Your_STORED_
value you just fetched.sqexid
: The account username.password
: The account password.otppw
: The account one-time password, if needed. This may be left blank of course.
- User Agent:
SQEXAuthor/2.0.0(Windows 6.2; ja-jp; {unique_id})
(macOS:macSQEXAuthor/2.0.0(MacOSX; ja-jp)
) - Accept:
image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*
- Accept-Encoding:
gzip, deflate
- Accept-Language:
en-us
- Content-Type:
application/x-www-form-urlencoded
- Referer: The previous request’s URL.
- Cache-Control:
no-cache
Just like the previous request, you will get a pretty disgusting HTML response. You will need some way to parse this data, but we have some regex queries to get you started.
The response may have multiple parts depending on how you log in, and if the login was successful. Start with this: window.external.user\("login=auth,ok,(?<launchParams>.*)\);
to get the launch parameters.
If you do not manage to get a match, this means there is a general account error. Luckily, Square Enix actually gives us an error message! Match this regex query now: window.external.user\("login=auth,ng,err,(?<launchParams>.*)\);
. The second capture has a comma-separated string that contains the relevant error message such as “Account locked due to too many attempts.”
However if you do get a match, that’s good but there’s still quite a bit of parsing to do. First you’ll want to split the second captured group since it’s a comma-separated string. There are multiple parts which we’ll refer to by name:
- parts[1] is
{SID}
- parts[3] is
{terms}
- parts[5] is
{region}
- parts[9] is
{playable}
- parts[13] is
{max_expansion}
First you’ll want to check if the account is even playable, which of course is checking to see if {playable}
is 1. This may indicate billing or license issues with that account. You’ll want to check if {terms}
is 1 as well, which indicates that there’s a terms of service agreement the account must sign.
The {SID}
, {region}
and {max_expansion}
will be needed later, so store these variables.
Now that we got an SID, you may expect that we can now log into the game! Well you’d be wrong, as we still have to register a session with the lobby server. If you attempt to launch the client with the {SID}
you got, the lobby server will disconnect you as soon as you log in.
Calculating the boot hash #
We need to calculate the hashes of everything in the boot directory. Why you ask? I guess this is Square Enix’s idea of security.
Here’s the files we need to hash:
- fxivboot.exe
- ffxivboot64.exe
- ffxivlauncher.exe
- ffxivlauncher64.exe
- ffxivupdater.exe
- ffxivupdater64.exe
We now build a string like this:
{file_name}/{file_hash},...
Please note that it’s comma separated and there is no newlines. The file hash is simply the SHA1 of the file (yes, really, SHA1). However, it’s not just the SHA1 and must also be sent with the file size in bytes:
{file_size}/{file_sha1}
The final string might look something like this:
ffxivboot.exe/256/fea677811b91f51a9f66dcb809a94ddac480f054,ffxivboot64.exe...
You’ll want to store this completed hash as the variable {boot_hash}
for the next step.
Registering a Session #
Square Enix expects the launcher to pass it’s “security check” next, and this request will also check for if any game updates are required too.
{game_version}
is referring to the version stored in $GAME_DIR/game/ffxivgame.ver
.
POST https://patch-gamever.ffxiv.com/http/win32/ffxivneo_release_game/{game_version}/{SID}
- X-Hash-Check:
enabled
- User Agent:
FFXIV PATCH CLIENT
- Content-Type:
application/x-www-form-urlencoded
Before you can POST this request, you need to build a report of all of your installed game versions as well as some hashes. This is simply a string in the body of the HTTP request.
The string is built as follows:
{boot_version}={boot_hash}\n
ex1\t{ex1_version}\n
...
Please note that the client must report all of it’s installed expansions. The base game version is already reported in the request URL itself, so you should start at “ex1”. Each entry in this body is separated by newlines, except for the last entry. Yes, the \t
in the body is referring to the tab character.
Once you send this request, there may or may not be a response body. First you’ll want to check for the response header called X-Patch-Unique-Id
, if this found then you’ve actually successfully registered a session! If this is missing, you may have triggered the anti-tamper check, or the game requires an update.
The {true_SID}
is now the value of the X-Patch-Unique-ID
field. Congratulations, you now logged into the game!
Launching the game #
Now you can launch the game! See ffxiv.exe for more arguments.
- Set
DEV.TestSID
to{true_SID}
. - Set
DEV.MaxEntitledExpansionID
to{max_expansion}
. - Set
SYS.Region
to{region}
.
Integrating Dalamud
If you’re developing your own launcher, you might be interested in integrating Dalamud support. Here’s a detailed walk-through of setting up a proper Dalamud environment.
Grabbing .NET Runtime #
You’ll need a .NET environment to actually launch Dalamud, since it’s based it uses .NET. It won’t try to use your system .NET, and will require to put it into a separate directory.
In order to determine which .NET runtime you need, first check the Dalamud Distribution server using the following URL:
https://kamori.goats.dev/Dalamud/Release/VersionInfo
This will return a JSON containing keys for runtimeVersion
. This is the required .NET runtime, which then can be fetched directly from Microsoft:
https://dotnetcli.azureedge.net/dotnet/Runtime/%1/dotnet-runtime-%1-win-x64.zip
https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/%1/windowsdesktop-runtime-%1-win-x64.zip
You can then extract both zip files into some directory, henceforth called $RUNTIME_DIR
.
Grabbing Dalamud #
Now you can grab Dalamud from Dalamud Distribution (https://kamori.goats.dev/Dalamud/Release/VersionInfo
) where the URL is downloadUrl
.
You can then extract this zip file, and the resulting directory will be referred to as $DALAMUD_DIR
.
Note: You can find out the version of Dalamud you have installed by reading the dependencies file, located under $DALAMUD_DIR/Dalamud.deps.json
.
Grabbing Dalamud assets #
These are not grabbed by Dalamud (for some reason) and instead you must download these yourself. These include fonts, icons and other things which are required for regular operation.
You can find the asset manifest at:
https://kamori.goats.dev/Dalamud/Asset/Meta
This is simply a long JSON describing where to find the assets, the current version and where to put them. It’s recommended to use the packageUrl
to download all of the assets at once. The directory you put assets in will be called $DALAMUD_ASSET_DIR
.
Launching Dalamud #
Now with all of your ugly ducklings in a row, you can begin launching Dalamud! First, please make sure these environment variables are set on the game process and all relevant processes and children. Please double check these, as Dalamud may silently fail without them.
DALAMUD_RUNTIME
should be set to your$RUNTIME_DIR
.- If you are in Wine, please set
XL_WINEONLINUX
.
- Launch
$DALAMUD_DIR/Dalamud.Injector.exe
.- You may be able to launch the injector without any additional configuration, but it’s recommended to set these.
- Arguments:
- The arguments for the game.
- Base64-encoding of a JSON dictionary which may contain these options:
- WorkingDirectory - overrides the working directory for Dalamud
- ConfigurationPath - the file path of
dalamudConfig.json
- PluginDirectory - the directory for the
installedPlugins
folder - AssetDirectory - should point to
$DALAMUD_ASSET_DIR
. - DefaultPluginDirectory - the default directory for the
devPlugin
folder. - DelayinitializeMs - how much Dalamud should wait before injection
- GameVersion - the (base) game version string
- Language - language code of the game
- OptOutMbCollection - whether or not to opt out from anonymous marketboard statistics collection
- If successful, the game should freeze for a few momements and Dalamud will successfully inject!
Equipment
This documentation is incomplete nd may be missing information for weapons, demihumans, other races and slot types.
This is useful for people implementing similar TexTools or FFXIV Explorer functionality, and it’s actually trivial to do so. Before you can do so, you must be able to read Excel data sheets.
Read item data #
The Excel sheet you’re interested is called item
and since it also contains localized names make sure to choose the relevant language sheet. Once you have done so, you’re interested in a couple of columns (tested as of 6.1):
- Column 9 (String)
- This is the name of the item.
- Column 17 (Unsigned 64-bit Integer)
- This is the slot id, explained below.
- Column 47 (Unsigned 64-bit Integer)
- This is the primary model data, explained below.
- Column 48 (Unsigned 64-bit Integer)
- This is the secondary model data, explained below.
Reading the slot id #
You’ll get an integer from the slot item column, and this corresponds to a specific slot:
Integer | Slot |
---|---|
3 | Head |
4 | Body |
5 | Hands |
7 | Legs |
8 | Feet |
9 | Earring |
10 | Neck |
11 | Wrists |
12 | Rings |
Reading the model data #
There are two separate integers, primary and secondary. Right now, we’re only interested in the first 2 bytes of the primary integer - this is your primary ID.
Grabbing the equipment path #
chara/equipment/e{model_id:04d}/model/c{race_id:04d}e{model_id:04d}_{slot}.mdl
{race_id}
is the race-specific equipment you want.
{model_id}
is the primary model id.
Note: :04d
means that it must be padded with 4 zeroes.
Creating your own Dalamud repository
This is for creating a 3rd party repository, which is probably not what you want. Almost all plugins should be submitted to the main repository, see https://dalamud.dev/plugin-development/plugin-submission.
Creating a 3rd party Dalamud repository is easy, since it’s pretty stateless and only requires you to host the files somewhere. People even host them on free servers like GitHub.
Plugin Manifests #
First you need to craft a folder somewhere (preferably with version control like Git) and start putting plugin manifests inside. These manifest.toml
contain vital information like where we check the plugin source code from and more. In your manifest folder, create a stable
subfolder. And then create more folders as needed inside, which correspond to the plugin name.
<manifest dir> /
PluginA /
manifest.toml
PluginB /
manifest.toml
The manifest.toml
is in this format:
[plugin]
repository = "<git repository>"
commit = "<git hash>"
owners = [ "<your name>" ]
changelog = '''
Your changelog in here.
'''
All plugins require an icon.png
, which you need to provide inside of an images
folder:
<manifest dir> /
PluginA /
images /
icon.png
manifest.toml
PluginB /
images /
icon.png
manifest.toml
Now your manifest folder is ready to go, we’ll move onto setting up the build system next.
Setting up and using Plogon #
Plogon is the preferred method of building plugins, and it’s used for the main Dalamud repository. It uses Docker to standardize the building process and takes care of repository state for you. Follow their README and continue this guide once you have a folder with a State.toml
and your built plugins.
Creating the repo JSON #
Now that we have our built plugin DLLs, our new repository is almost usable inside of Dalamud. But Dalamud does not consume the State.toml
directly, the plugin manifests need to be concentrated into a single JSON file. XLWebServices does this, but I built a smaller tool just for this purpose called DalamudRepoTool. Install it via Cargo:
cargo install --git https://github.com/redstrate/DalamudRepoTool.git
And then run it on your repository:
DalamudRepoTool --download-host <URL to your hosted repository without the stable channel path> --repo-path <your repository path where the State.toml is>
When it’s done, it will create a repo.json
where the State.toml
was. Your repository is complete and can now be used in Dalamud!
Building Dalamud plugins on Linux
Believe it or not, it’s actually very easy to develop with Dalamud on Linux since they moved to .NET Core!
For example, in JetBrain’s Rider you can open any Dalamud project and have it build out of the box.
The .NET SDK #
You need the .NET SDK of course, which is in many package managers:
- Fedora Linux:
dotnet
- Arch Linux:
dotnet-sdk
- Gentoo Linux:
dotnet-sdk
ordotnet-sdk-bin
Fixing DalamudLibPath #
Depending on the plugin, it might already have sensible paths to the Dalamud library folder. The SamplePlugin is an example of this. If it doesn’t, or you don’t use XIVQuickLauncher/XIV.Core then you need to manually edit the path. Tweak the <DalamudLibPath>
folder to the location where your Dalamud.dll
and files sit, and you’ll know when you’re successful when .NET stops screaming.
If for some reason your build of Dalamud is not a developer-enabled one, check the Dalamud release server for a download URL.
The devPlugin path #
Remember that when entering the dev plugin path in the Dalamud settings, to prepend it with Z:
and take care of your slashes so they make sense under Wine.