commit 1a25e9f864428224c60949947e3842697119d632 Author: Joshua Goins Date: Sat Jul 16 18:54:44 2022 -0400 Add initial files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e699ca9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.directory +.hugo_build.lock +.DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..eeb449c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,39 @@ +build: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - if [ "$CI_COMMIT_REF_NAME" == "develop" ]; then DOMAIN="https://staging.docs.xiv.zone/"; else DOMAIN="https://docs.xiv.zone/"; fi + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json + - >- + /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/Dockerfile" + --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}" + --build-arg DOMAIN=$DOMAIN + +deploy-staging: + stage: deploy + image: + name: curlimages/curl:latest + script: + - 'curl -H "Authorization: Bearer ${WATCHTOWER_TOKEN}" https://xiv.zone/v1/update' + environment: + name: staging + url: https://staging.docs.xiv.zone + only: + - develop + +deploy-production: + stage: deploy + image: + name: curlimages/curl:latest + script: + - 'curl -H "Authorization: Bearer ${WATCHTOWER_TOKEN}" https://xiv.zone/v1/update' + environment: + name: production + url: https://docs.xiv.zone + only: + - main diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..3959583 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,26 @@ +{ + admin off # we dont use this so lol + debug +} + + +:9455 { + header { + -Server + + Cache-Control "max-age=604800, immutable" + } + + handle_path /astra-distrib/* { + root * /astra-distrib + } + + handle_path /distrib/* { + root * /astra-distrib + } + + encode gzip + + file_server +} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..683a754 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM klakegg/hugo:alpine AS build + +COPY . /site +WORKDIR /site +ARG DOMAIN + +RUN hugo --baseURL ${DOMAIN} + +FROM caddy + +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /site/public /srv diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..00e77bd --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,6 @@ +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..71f4277 --- /dev/null +++ b/config.toml @@ -0,0 +1,20 @@ +languageCode = "en-us" +title = "docs.xiv.zone" +theme = "mytheme" +summaryLength = 30 +disableHugoGeneratorInject = true +enableEmoji = true + +[privacy] + [privacy.disqus] + disable = true + [privacy.googleAnalytics] + disable = true + [privacy.instagram] + disable = true + [privacy.twitter] + disable = true + [privacy.vimeo] + disable = true + [privacy.youtube] + disable = true diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..dd35c54 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,62 @@ +This is my collection of everything I know, and that I collected on FFXIV inner workings. This information was previously hosted in my [ffxiv-info repository](https://git.sr.ht/~redstrate/ffxiv-info), but moved here. + +Developers, please look at the end of each page for example implementations that I know of. I find it incredibly useful to look at them, especially if I'm stuck on implementing something. If you have a project that you want to add to the list or suggest any kind of edit, don't hesitate to [email me](https://redstrate.com/about) or [fix it yourself](https://git.sr.ht/~redstrate/docs.xiv.zone)! + +## Executables + +**Note:** These are actually referring to their 64-bit counterparts, e.g. `ffxivboot.exe` is `ffxivboot64.exe`. + +### Boot + +* [ffxivboot.exe](executable/ffxivboot) - Launcher for the launcher. +* [ffxivupdater.exe](executable/ffxivupdater) - Game patcher. +* [ffxivlauncher.exe](executable/ffxivlauncher) - Boot/game launcher. + +### Game + +* [ffxiv.exe](executable/ffxiv) - Game executable. + +### Other + +* [ffxivinstaller.exe](executable/ffxivinstaller) - Retail game client installer. + +## Concepts/Techniques + +* [Logging into Official Servers](concept/logging-in-official) - Logging into the official game servers. +* [Logging into Sapphire](concept/logging-in-sapphire) - Logging into unofficial Sapphire servers. +* [SqexArg](concept/sqexarg) - Encrypted game arguments (sqexarg). +* [Equipment](concept/equipment) - All about reading equipment data ala TexTools. +* [Dalamud](concept/dalamud) - Launching Dalamud without the help of XIVQuickLauncher. + +## File Formats + +### Excel + +* [.exd](format/exd) - Excel data. +* [.exh](format/exh) - Excel header. +* [.exl](format/exl) - Excel list. + +### Graphics + +* [.mdl](format/mdl) - Game model. + +### SqPack + +* [.index/index2](format/sqpack-index) - Game Data Index file. +* [.dat](format/sqpack-dat) - Compressed game data. + +### Other + +* [.patch](format/patch) - ZiPatch files. +* [.fiin](format/fiin) - File info. + +## Credits + +This wouldn't be possible without all of the great people who open-source their work, and everyone else in the FFXIV community! + +* [xiv.dev](https://xiv.dev/) +* [XIVQuickLauncher](https://github.com/goatcorp/FFXIVQuickLauncher) +* [XIV-on-Mac](https://github.com/marzent/XIV-on-Mac) +* [xivModdingFramework](https://github.com/TexTools/xivModdingFramework) +* [Lumina](https://github.com/NotAdam/Lumina) +* Everyone else who develops FFXIV tools :-) diff --git a/content/concept/dalamud.md b/content/concept/dalamud.md new file mode 100644 index 0000000..519dd04 --- /dev/null +++ b/content/concept/dalamud.md @@ -0,0 +1,84 @@ +--- +title: "Launching 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 dalamud-distrib using the following URL: + +`https://goatcorp.github.io/dalamud-distrib/version` + +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-distrib: + +`https://goatcorp.github.io/latest.zip` + +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://goatcorp.github.io/DalamudAssets/asset.json` + +This is simply a long JSON describing where to find the assets, the current version and where to put them. The directory you put +assets in will be called `$DALAMUD_ASSET_DIR`. + +## Grabbing nativelauncher + +**Note:** Nativelauncher is no longer needed, as it seems Dalamud can host the game process on it's own. This section is kept for historical purposes as Astra will eventually move away from using it. + +Now you need an ACL bypass for Dalamud to actually gain privileges in order to read game memory. Using one of these programs is out of scope for this tutorial, but I recommend my own [nativelauncher](https://git.sr.ht/~redstrate/nativelauncher) as it's a standalone Windows binary. + +You can grab the latest version from this URL: + +`https://xiv.zone/distrib/nativelauncher/version` + +And using this URL you can grab the latest binary: + +`https://xiv.zone/distrib/nativelauncher/NativeLauncher.exe` + +Please check the [README](https://git.sr.ht/~redstrate/nativelauncher/#usage) on usage. + +## 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`. + +1. Launch the game using some sort of ACL bypass (nativelauncher, etc). Then continue once you recieve the PID of the game. +2. 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: + 1. The PID of the game process. + 2. 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 +3. If successful, the game should freeze for a few momements and Dalamud will successfully inject! diff --git a/content/concept/equipment.md b/content/concept/equipment.md new file mode 100644 index 0000000..c0d22b9 --- /dev/null +++ b/content/concept/equipment.md @@ -0,0 +1,50 @@ +--- +title: "Equipment" +--- + +**Note:** This section is currently incomplete, and 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](formats/exd). + +### 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. diff --git a/content/concept/logging-in-official.md b/content/concept/logging-in-official.md new file mode 100644 index 0000000..762032c --- /dev/null +++ b/content/concept/logging-in-official.md @@ -0,0 +1,172 @@ +--- +title: "Logging into Official Servers" +--- + +**Note:** This article currently only covers logging in via non-Steam Square Enix accounts. + +Logging into the official FFXIV servers is actually very simple, and all you need is the ability to send/receive HTTP requests, create JSON responses and read some files off of the disk. + +You'll notice below each HTTP request is a series of bullet points. These are HTTP request fields that **must** be filled. This is not a mere suggestion, as we want to closely emulate the official launcher as possible, else you will get nonsensical errors and things won't work. + +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 of all, you want to check if you can _even_ log in the first place. This is extremely important, as the official launcher refuses to "pass the gate" so to speak. If you allow your launcher to bypass this, you're at your own risk **as Square Enix does not expect legitimate users to enter under maintenance servers**. + +**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. + +**Note:** This is not a typo, and this endpoint is actually in plaintext HTTP. Seriously. + +**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. However if your boot components are out of date, you will receive a list of patches to +update. + +## Getting _STORED_ + +The next prerequisite we need is the `_STORED_` value. This is pretty simple though. + +**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`: 1 + * `isnew`: 1 + * `launchver`: 3 + * `issteam`: 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. + +**Note:** If you're logging in with a Steam service account, you can find your username using `.*)""\/>`. + +To get the `_STORED_` value, use `\t<\s*input .* name="_STORED_" value="(?.*)">` 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. Make sure to check the gate status! + +## Logging in + +Now you have all of the needed variables to actually attempt a log 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,(?.*)\);` 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,(?.*)\);`. 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. + +## Calculating the boot hash + +Before we can register the session, we must first 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 file list we need to start with: + +* fxivboot.exe +* ffxivboot64.exe +* ffxivlauncher.exe +* ffxivlauncher64.exe +* ffxivupdater.exe +* ffxivupdater64.exe + +For each and every file in that list, we use to build a string like so: + +``` +{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) and it's formatted as follows: + +``` +{file_size}/{file_sha1} +``` + +You'll want to store this completed hash as `{boot_hash}` for the next step. + +## Registering a Session + +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. 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. + +**Note:** `{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 seperated 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](executable/ffxiv) for more information. For a quick rundown: +* Set `DEV.TestSID` to `{true_SID}`. +* Set `DEV.MaxEntitledExpansionID` to `{max_expansion}`. +* Set `SYS.Region` to `{region}`. diff --git a/content/concept/logging-in-sapphire.md b/content/concept/logging-in-sapphire.md new file mode 100644 index 0000000..a84ad9e --- /dev/null +++ b/content/concept/logging-in-sapphire.md @@ -0,0 +1,44 @@ +--- +title: "Logging into Sapphire Servers" +--- + +Luckily 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](executable/ffxiv) for more information. 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. diff --git a/content/concept/sqexarg.md b/content/concept/sqexarg.md new file mode 100644 index 0000000..bc2d502 --- /dev/null +++ b/content/concept/sqexarg.md @@ -0,0 +1,136 @@ +--- +title: "SqexArg" +--- + +I also recommend reading the relevant [xiv.dev page on SqexArg](https://xiv.dev/sqexarg). + +## Purpose + +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](executable/ffxiv) 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. +* 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. +* 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#)](https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher.Common/Encryption/LegacyBlowfish.cs) + * [Astra (C++)](https://git.sr.ht/~redstrate/astra/tree/main/item/launcher/core/include/blowfish.h) + * [XIV-on-Mac (Swift)](https://github.com/marzent/XIV-on-Mac/blob/main/XIV%20on%20Mac/Encryption.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](https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher.Common/Encryption/BlockCipher/Blowfish.cs) 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](https://sr.ht/~redstrate/novus/#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. + +## Implementations + +* [XIVQuickLauncher (C#)](https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher.Common/Encryption/ArgumentBuilder.cs) +* [Astra (C++)](https://git.sr.ht/~redstrate/astra/tree/main/item/launcher/core/include/encryptedarg.h) +* [XIV-on-Mac (Swift)](https://github.com/marzent/XIV-on-Mac/blob/main/XIV%20on%20Mac/Encryption.swift) diff --git a/content/executable/ffxiv.md b/content/executable/ffxiv.md new file mode 100644 index 0000000..a6a8e9b --- /dev/null +++ b/content/executable/ffxiv.md @@ -0,0 +1,34 @@ +--- +title: "ffxiv.exe" +--- + +This is the actual game executable, and theres two versions - one for DX9 and DX11. You'll almost always be using the DX11 version, called `ffxiv_dx11.exe`. + +## Arguments + +* DEV.DataPathType + * **Guessing** that this controls the asset data type used by the client, this is will always be `1`. +* DEV.UseSqPack + * **Guessing** that this tells the client to try to load data from SqPack files instead of from regular, uncompressed files. Seems to always be `1`. +* DEV.MaxEntitledExpansionID + * This is the max entitled expansion ID that the currently logging in user has access to, you will want to set this from your login response. +* DEV.TestSID + * This is the SID you get from your login response. +* SYS.Region + * Your login region. +* language + * Your login language. +* ver + * The (base) game version string. +* DEV.GMServerHost + * This is the address of the frontier server to connect to. If empty, this will default to the official Square Enix frontier server. +* DEV.LobbyHost0X + * This is the address of the Nth lobby available to the client, these are numbered 0-9. If empty, this will default to the official Square Enix lobbies. +* DEV.LobbyPort0X + * This is the port number of the Nth lobby available to the client, these are numbered 0-9. If empty, this will default to the official Square Enix lobbies. + +**Note:** For benchmark versions of the game, there is a whole host of graphical options available as game arguments - but this seems to be missing in the retail version. + +## Alternative Implementations + +None. diff --git a/content/executable/ffxivboot.md b/content/executable/ffxivboot.md new file mode 100644 index 0000000..a08f56b --- /dev/null +++ b/content/executable/ffxivboot.md @@ -0,0 +1,14 @@ +--- +title: "ffxivboot.exe" +--- + +This executable handles launching [ffxivlauncher.exe](executable/ffxivlauncher), and also patching "boot files" (aka anything inside of the GameInstall/boot folder). + +## Arguments + +None(?) that I've found yet. + +## Alternative Implementations + +* [Astra](https://git.sr.ht/~redstrate/astra/tree/main/item/launcher/core/src/squareboot.cpp)/[libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/patch.cpp) +* [XIVQuickLauncher (C#)](https://github.com/goatcorp/FFXIVQuickLauncher/blob/master/src/XIVLauncher.Common/Game/Launcher.cs) diff --git a/content/executable/ffxivinstaller.md b/content/executable/ffxivinstaller.md new file mode 100644 index 0000000..bc43675 --- /dev/null +++ b/content/executable/ffxivinstaller.md @@ -0,0 +1,19 @@ +--- +title: "ffxivinstaller.exe" +--- + +This is not a standard InstallShield installer (at least to my knowledge) but seems to a self-extracting executable and then some form of InstallShield executable. + +If you only care about the .cab files (which can then be read with your usual installshield libraries/tools like unshield) then they are contained right in the executable, uncompressed. + +To find them, simply find the needle string "Disk1\\{FileName}" where FileName is the cab filename. They are as follows: + +* data1.cab +* data1.hdr (to my knowledge, this is some kind of "file index" for installshield installers) +* data2.cab + +They are simply put right next to each other in the executable, so if you follow that order you'll find each of them quite easily. + +## Alternative Implementations + +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/src/installextract.cpp) diff --git a/content/executable/ffxivlauncher.md b/content/executable/ffxivlauncher.md new file mode 100644 index 0000000..f3364f9 --- /dev/null +++ b/content/executable/ffxivlauncher.md @@ -0,0 +1,32 @@ +--- +title: "ffxivlauncher.exe" +--- + +## Arguments + +**Note:** Just like the other executables, it requires you to pass these using the [SqexArg](concept/sqexarg) format. + +- ExecuteArg + - This is a strange argument. This appears to be a random gibberish of numbers: + + `/T =1000000 /ExecuteArg =14431503 /UserPath =C:/users/yourname/Documents/My Games/FINAL FANTASY XIV - A Realm Reborn` + + In reality, decompiling the launcher reveals that they are literally sprintf'ing in this format: + + `%02d%02d%02d%02d` + + - 1st arg - current month + 1 + - 2nd arg - current day + - 3rd arg - current hour + - 4th arg - current minute + +- UserPath + - Your usual path to your FFXIV data folder in Documents. + + +## Alternative Implementations + +* [XIVQuickLauncher (C#)](https://github.com/goatcorp/FFXIVQuickLauncher) +* [Astra (C++)](https://sr.ht/~redstrate/astra/) +* [XIV-on-Mac (Swift)](https://github.com/marzent/XIV-on-Mac) +* [microlaunch (Rust)](https://github.com/eorzeatools/microlaunch) diff --git a/content/executable/ffxivupdater.md b/content/executable/ffxivupdater.md new file mode 100644 index 0000000..cd4f37b --- /dev/null +++ b/content/executable/ffxivupdater.md @@ -0,0 +1,64 @@ +--- +title: "ffxivupdater.exe" +--- + +**Note:** This is unconfirmed information, as I'm still progressing through my reverse engineering work. If you have any idea what this updater is doing, please contact me :-) + +## Arguments + +**Note:** Just like the other executables, it requires you to pass these using the [SqexArg](concept/sqexarg) format. + +- BootVersion + - The version of the boot component. +- GameVersion + - The version of the game component, however I'm curious as to the purpose of this variable. Is the patcher itself fetching the patches required for the provided game version? +- CallerWindow + - This seems to be the HWND which it sends messages to. Passing a HWND that is setup to receive messages, it appears to yield a 0x74 message (OpenWindow I believe) which means this is the most likely case. More on the IPC below. +- IsSteam + - Obviously, to check if this is the Steam version. I have not reversed the game arguments of the Steam version yet. +- NextExe + - I'm not entirely sure what the purpose of this is yet. It makes no sense for the updater to call the game itself, I'm pretty sure this is used for "patch chaining" but it's + also set for regular game patches. For example, when updating the game through the launcher it points to ffxiv.exe. Note that it is not ffxiv_dx11.exe, which means this is + probably unused in the retail launcher. +- ShowMode + - This is the most interesting parameter, more testing is needed. There is multiple values here, and some of them correspond to known launcher behavior: + - 1: This seems to be some sort of standalone updater window that I've never seen before in retail. Is this a leftover from a older version of the game? For example, it looks for "LauncherPatchInstall.list" (I can't seem to recreate this file). I have not gotten it to read from a "GamePatchInstall.list" either, however there is probably some other parameter to trigger this change. This "list" format is also unknown. + - 2: This has no user-visible window, and is the "default mode" when launched properly through ffxivlauncher.exe. It's communicating through regular Win32 IPC I believe, back to the launcher where the launcher updates it's own window. + - 3: I haven't gotten this mode to work, but on the standalone updater window it mentions "repairing game files". I believe this is what's used by the "repair game files" button in the launcher, but I've never used it before nor do I know it's actual purpose (just checking hashes?). +- UserPath + - This is the usual UserPath arugment, pointing to your FFXIV-ARR Documents directory. + +## Processes +During update execution, the launcher(?) copies the updater executable into your `UserPath/downloads` directory, where it is then ran. I'm assuming this is to work around a Windows limitation where you can't update the executable on disk, so it has to be ran from a location where it could "update" itself. + +### ShowMode 2 + +This is the regular retail patching mode, and it runs ffxivupdater64.exe in a hidden window that's just there for message processing. I'm not sure yet if ffxivupdater.exe is the one actually downloading the patch files yet. The launcher just reflects ffxivupdater's progress, and passes it along to the web form in the form of a json callback. You can set a breakpoint at 0x183278 (tested on the launcher ver 3/20/2022) and see that it first hits that area when updating. There is three seperate printf strings, which I'm guessing is the three "modes" of patch progress: + +| Offset | String Data +| ------ | ---------- | +| 1400e6df0 | {version:"%s",remaintime:"%s",progress:%6.2f} u"{version:\"%s\",remaintime:\"%s\",progress:%6.2f}" +| 1400e75a0 | {version:"%s",downloadsize:"%s",remaintime:"%s",speed:"%s",progress:%6.2f} +| 1400e76c0 | {version:"%s",remaintime:"%s",speed:"%S",progress:%6.2lf} + +_(The offsets are tested on 3/20/2022)_ +The first one appears very minimal but might be the one used when it's not downloading/installing any patches yet, and just gathering information. +The second one appears to be the regular "downloading" patch mode, considering it says "downloadsize" right there. +The last one would appear to be when it's actually applying the patches, as it has "remaintime" as well as "speed". + +#### IPC + +It appears that ffxivlauncher.exe and ffxivupdater.exe is communicating exclusively through Win32 IPC (SendMessage). I haven't deciphered the actual messages yet, but here is a few interesting ones I've seen used: + +##### 0x7fc + +The format is `{version:\"%s\",ID:%d}`. I'm guessing this means "install this patch", version obviously corresponds to the patch version, and maybe ID is the jumble of alphanumeric characters in the directory names of the ffxivpatch folder? + +0xbd4 - unknown + +0x81a - unknown + +## Alternative Implementations + +* [XIVQuickLauncher (C#)](https://github.com/goatcorp/FFXIVQuickLauncher/tree/master/src/XIVLauncher.Common/Game/Patch) +* [libxiv](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/patch.cpp) diff --git a/content/format/exd.md b/content/format/exd.md new file mode 100644 index 0000000..ccfec1e --- /dev/null +++ b/content/format/exd.md @@ -0,0 +1,15 @@ +--- +title: "EXD" +--- + +**Note:** To prevent confusion, "Excel" as described has nothing to do with Microsoft's Excel. + +**Note:** This documentation is incomplete. + +I recommend reading the ["Excel Data" section](https://xiv.dev/game-data/file-formats/excel#excel-data-.exd) on xiv.dev's Excel docs. + +## Alternative Implementations + +* [xivModdingFramework (C#)](https://github.com/TexTools/xivModdingFramework/tree/master/xivModdingFramework/Exd) +* [Lumina (C#)](https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/Excel/ExcelDataFile.cs) +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/exdparser.cpp) diff --git a/content/format/exh.md b/content/format/exh.md new file mode 100644 index 0000000..9efa0d3 --- /dev/null +++ b/content/format/exh.md @@ -0,0 +1,16 @@ +--- +title: "EXH" +--- + +**Note:** To prevent confusion, "Excel" as described has nothing to do with Microsoft's Excel. + +**Note:** This documentation is incomplete. + +I recommend reading the ["Excel Header" section](https://xiv.dev/game-data/file-formats/excel#excel-header-.exh) on xiv.dev's Excel docs. + +This is a schema file describing the column and row layout of an Excel sheet (such as `Achievement`). You **are required to parse** this before reading `.exd` files, as it contains important information on building a path and reading columns. + +## Alternative Implementations + +* [Lumina (C#)](https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/Excel/ExcelHeaderFile.cs) +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/exhparser.cpp) diff --git a/content/format/exl.md b/content/format/exl.md new file mode 100644 index 0000000..364a546 --- /dev/null +++ b/content/format/exl.md @@ -0,0 +1,28 @@ +--- +title: "EXL" +--- + +**Note:** To prevent confusion, "Excel" as described has nothing to do with Microsoft's Excel. + +I recommend reading the ["Excel List" section](https://xiv.dev/game-data/file-formats/excel#excel-list-.exl) on xiv.dev's Excel docs. + +This is simply a text file containing a list of comma-separated strings, where each line is separated by a newline: + +``` +EXLT,2 +Achievement,209 +Action,4 +content/DeepDungeon2Achievement,-1 +content/DeepDungeon2Gacha,-1 +``` + +This describes Excel sheets that exist in the game data, hence why it's called dubbed an "Excel List". The easiest one to parse is `exd/rool.exl` as it contains a bunch of Excel sheets to test a parser on. + +With an Excel List parsed, you can then build a path to a [Excel header or .exh](format/exh). To do so, simply move all characters to lowercase and append `.exh` at the end: + +`Achievement,209` -> `exh/achievement.exh` + +## Alternative Implementations + +* [Lumina (C#)](https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/Files/Excel/ExcelListFile.cs) +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/exlparser.cpp) diff --git a/content/format/fiin.md b/content/format/fiin.md new file mode 100644 index 0000000..b73ad3e --- /dev/null +++ b/content/format/fiin.md @@ -0,0 +1,44 @@ +# .fiin + +I don't know the actual purpose of this file, but it was a fun reverse-engineering exercise. These are usually listed like `fileinfo.fiin` (like in `/boot`) and start with a header like this: + +``` +struct FileInfoHeader { + char magic[9]; + uint8_t dummy1[16]; + uint8_t unknown; // version? always seems to be 4 + uint8_t dummy2[2]; + uint8_t unknown1; + uint8_t unknown2; + uint8_t dummy[994]; +}; +``` + +`magic` is always `FileInfo`. + +To get the number of entries, calculate it like so: + +``` +int overflow = info.header.unknown2; +int extra = overflow * 256; +int first = info.header.unknown1 / 96; +int first2 = extra / 96; +int actualEntries = first + first2 + 1; +``` +_(Note: I have no idea if this is actually necessary, and it could just be a unsigned int or something)_ + +Then each entry is located right after the header, and is structured as so: + +``` +struct FileInfoEntry { + uint8_t dummy[8]; // length of file name in some format + char str[64]; // simple \0 encoded string + uint8_t dummy2[24]; // sha1 +}; +``` + +This file appears to have SHA1 hashes, but for what purpose I do not know. + +## Alternative Implementations + +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/fiinparser.cpp) diff --git a/content/format/mdl.md b/content/format/mdl.md new file mode 100644 index 0000000..e741879 --- /dev/null +++ b/content/format/mdl.md @@ -0,0 +1,9 @@ +--- +title: "MDL" +--- + +**Note:** This documentation is incomplete. + +## Alternative Implementations + +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/mdlparser.cpp) diff --git a/content/format/patch.md b/content/format/patch.md new file mode 100644 index 0000000..32a21d4 --- /dev/null +++ b/content/format/patch.md @@ -0,0 +1,11 @@ +--- +title: "ZiPatch" +--- + +**Note:** This documentation is incomplete. + +I recommend reading the ["ZiPatch" section](https://xiv.dev/data-files/zipatch) on xiv.dev. + +## Alternative Implementations + +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/patch.cpp) diff --git a/content/format/sqpack-dat.md b/content/format/sqpack-dat.md new file mode 100644 index 0000000..d9a71ac --- /dev/null +++ b/content/format/sqpack-dat.md @@ -0,0 +1,13 @@ +--- +title: "dat" +--- + +**Note:** This documentation is incomplete. + +I recommend reading the ["SqPack" page](https://xiv.dev/data-files/sqpack) on xiv.dev. + +## Alternative Implementations + +* [xivModdingFramework (C#)](https://github.com/TexTools/xivModdingFramework/blob/master/xivModdingFramework/SqPack/FileTypes/Dat.cs) +* [Lumina (C#)](https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/SqPackStream.cs) +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/gamedata.cpp) diff --git a/content/format/sqpack-index.md b/content/format/sqpack-index.md new file mode 100644 index 0000000..623a442 --- /dev/null +++ b/content/format/sqpack-index.md @@ -0,0 +1,13 @@ +--- +title: "index/index2" +--- + +**Note:** This documentation is incomplete. + +I recommend reading the ["Reading Index Data" section](https://xiv.dev/data-files/sqpack#reading-index-data) on xiv.dev's SqPack docs. + +## Alternative Implementations + +* [xivModdingFramework (C#)](https://github.com/TexTools/xivModdingFramework/blob/master/xivModdingFramework/SqPack/FileTypes/Index.cs) +* [Lumina (C#)](https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Data/SqPackIndex.cs) +* [libxiv (C++)](https://git.sr.ht/~redstrate/libxiv/tree/main/item/src/indexparser.cpp) diff --git a/resources/_gen/images/astra/main-screenshot_hu19881f1cccbca9d0fa0ea6020d6522f3_307487_800x0_resize_q75_h2_box_3.webp b/resources/_gen/images/astra/main-screenshot_hu19881f1cccbca9d0fa0ea6020d6522f3_307487_800x0_resize_q75_h2_box_3.webp new file mode 100644 index 0000000..95ee906 Binary files /dev/null and b/resources/_gen/images/astra/main-screenshot_hu19881f1cccbca9d0fa0ea6020d6522f3_307487_800x0_resize_q75_h2_box_3.webp differ diff --git a/themes/mytheme/LICENSE b/themes/mytheme/LICENSE new file mode 100644 index 0000000..9ed963a --- /dev/null +++ b/themes/mytheme/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2022 Joshua Goins + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/themes/mytheme/archetypes/default.md b/themes/mytheme/archetypes/default.md new file mode 100644 index 0000000..ac36e06 --- /dev/null +++ b/themes/mytheme/archetypes/default.md @@ -0,0 +1,2 @@ ++++ ++++ diff --git a/themes/mytheme/assets/css/style.css b/themes/mytheme/assets/css/style.css new file mode 100644 index 0000000..460437a --- /dev/null +++ b/themes/mytheme/assets/css/style.css @@ -0,0 +1,126 @@ +@media screen and (min-width: 900px) { + #wrapper { + display: flex; + } + + #center-wrapper { + width: 900px; + margin: auto; + } +} + + +html { + font-family: sans-serif; + --content-color: white; + --link-color: rgb(199, 26, 26); + --link-color-visited: rgb(128, 0, 0); + --content-border: 1px solid; + --border-color: white; + overflow-y: scroll; + color: #2f2f2f; + background-color: white; +} + +hr { + height: 0px; + border: none; + border-top: 1px solid black; +} + +body { + margin: 0; +} + +header { + margin-bottom: 20px; + width: 100%; + height: 100%; +} + +a:link { + color: var(--link-color); +} + +a:visited { + color: var(--link-color-visited) +} + +img { + max-width: 100%; + height: auto; +} + +table { + border-collapse: collapse; + font-size: 10pt; +} + +td { + border-top: 1px solid #ddd; + padding: 4px +} + +main { + background-color: var(--content-color); + border: var(--content-border); + border-radius: 10px; + border-color: var(--border-color); + margin: 2px; +} + +main > h2 { + margin: 0px; +} + +@media screen and (max-width: 900px) { + #header-links { + justify-content: center; + flex-wrap: wrap; + } + + html { + font-size: 1.1em; + } + + header > h1 { + text-align: center; + } + + #center-wrapper { + margin-left: 10px; + margin-right: 10px; + } +} + +footer { + font-size: .9rem; +} + +.blurb { + background-color: #eaeaea; + padding: 1em; + border-radius: 7px; + margin: 0.5em; +} + +.blurb-button { + padding: 1em; + border-radius: 7px; + margin: 0.5em; + text-decoration: none; +} + +.blurb-button:hover { + background-color: #eaeaea; +} + +.blurb-button h4, .blurb h4 { + margin: 0 0 3px; +} + +.buttons { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + margin: auto; +} diff --git a/themes/mytheme/layouts/404.html b/themes/mytheme/layouts/404.html new file mode 100644 index 0000000..b032a7b --- /dev/null +++ b/themes/mytheme/layouts/404.html @@ -0,0 +1,5 @@ +{{ define "main" }} + +404 not found. + +{{ end }} \ No newline at end of file diff --git a/themes/mytheme/layouts/_default/_markup/render-image.html b/themes/mytheme/layouts/_default/_markup/render-image.html new file mode 100644 index 0000000..3616165 --- /dev/null +++ b/themes/mytheme/layouts/_default/_markup/render-image.html @@ -0,0 +1,12 @@ +{{- $url := .Destination | safeURL -}} +{{- $img := .Page.Resources.GetMatch .Destination -}} +{{- if and (not $img) .Page.File -}} +{{ $path := path.Join .Page.File.Dir .Destination }} +{{- $img = resources.Get $path -}} +{{- end -}} +{{- with $img -}} +{{- $large := $img.Resize "800x webp" -}} +{{ $.Text }} +{{- else -}} + +{{ $.Text }}{{- end -}} diff --git a/themes/mytheme/layouts/_default/bare.html b/themes/mytheme/layouts/_default/bare.html new file mode 100644 index 0000000..47a04ff --- /dev/null +++ b/themes/mytheme/layouts/_default/bare.html @@ -0,0 +1,19 @@ +{{ partial "head.html" . }} + +
+ + {{ partial "header.html" . }} + +
+ +
+

{{ .Title }}

+ + {{ .Content }} +
+ +
+ +
+ + \ No newline at end of file diff --git a/themes/mytheme/layouts/_default/baseof.html b/themes/mytheme/layouts/_default/baseof.html new file mode 100644 index 0000000..66e3436 --- /dev/null +++ b/themes/mytheme/layouts/_default/baseof.html @@ -0,0 +1,19 @@ +{{ partial "head.html" . }} + +
+ + {{ partial "header.html" . }} + +
+ +
+ {{- block "main" . }}{{- end }} +
+ +
+ +{{ partial "footer.html" . }} + +
+ + diff --git a/themes/mytheme/layouts/_default/list.html b/themes/mytheme/layouts/_default/list.html new file mode 100644 index 0000000..1c996c8 --- /dev/null +++ b/themes/mytheme/layouts/_default/list.html @@ -0,0 +1,9 @@ +{{ define "main" }} + +{{ range .Data.Pages.ByDate.Reverse }} +

{{ .Title }}

+{{ .Content }} +
+{{ end }} + +{{ end }} diff --git a/themes/mytheme/layouts/_default/simple.html b/themes/mytheme/layouts/_default/simple.html new file mode 100644 index 0000000..fb3ffc2 --- /dev/null +++ b/themes/mytheme/layouts/_default/simple.html @@ -0,0 +1 @@ +{{ .Content }} \ No newline at end of file diff --git a/themes/mytheme/layouts/_default/single.html b/themes/mytheme/layouts/_default/single.html new file mode 100644 index 0000000..9d176a0 --- /dev/null +++ b/themes/mytheme/layouts/_default/single.html @@ -0,0 +1,5 @@ +{{ define "main" }} +

{{ .Title }}

+ + {{ .Content }} +{{ end }} diff --git a/themes/mytheme/layouts/index.html b/themes/mytheme/layouts/index.html new file mode 100644 index 0000000..2733876 --- /dev/null +++ b/themes/mytheme/layouts/index.html @@ -0,0 +1,6 @@ +{{ define "main" }} + +{{ .Content }} + + +{{ end }} diff --git a/themes/mytheme/layouts/partials/footer.html b/themes/mytheme/layouts/partials/footer.html new file mode 100644 index 0000000..0ed3a6c --- /dev/null +++ b/themes/mytheme/layouts/partials/footer.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/themes/mytheme/layouts/partials/head.html b/themes/mytheme/layouts/partials/head.html new file mode 100644 index 0000000..695772a --- /dev/null +++ b/themes/mytheme/layouts/partials/head.html @@ -0,0 +1,38 @@ + + + + + + + + + + + {{ if .IsHome }} + + {{.Site.Title}} + {{ else }} + {{.Site.Title}} - {{ .Title }} + + {{ end }} + + + + + {{ if .IsHome }} + {{ with .OutputFormats.Get "RSS" }} + + {{ end }} + {{ end }} + + {{ $css := resources.Get "css/style.css" | resources.Minify | resources.Fingerprint }} + + + + + + diff --git a/themes/mytheme/layouts/partials/header.html b/themes/mytheme/layouts/partials/header.html new file mode 100644 index 0000000..e1019d5 --- /dev/null +++ b/themes/mytheme/layouts/partials/header.html @@ -0,0 +1,14 @@ +
+

{{ .Site.Title}}

+ + +
diff --git a/themes/mytheme/layouts/partials/sidebar.html b/themes/mytheme/layouts/partials/sidebar.html new file mode 100644 index 0000000..fc9e9a6 --- /dev/null +++ b/themes/mytheme/layouts/partials/sidebar.html @@ -0,0 +1,53 @@ +{{ $image := resources.Get "img/profile.jpg" }} +{{ with $image }} +{{ $resized := .Resize "webp 256x" }} +Profile Picture +{{ end }} + +
+ {{ $currentPage := . }} + {{ range $index, $element := .Site.Menus.formats }} + {{ $len := (len $.Site.Menus.formats) }} + + {{ .Name }} + + {{ if not (eq (add $index 1) $len) }}ยท{{ end }} + {{ end }} +
+ +

+ Hey I do graphics dev, write tools, cross-platform stuff and contribute + where I can! I draw sometimes. +

+ +
+ +

Accounts:

+ + + +

Contact:

+ + + +

Donate:

+ + diff --git a/themes/mytheme/layouts/partials/svg.html b/themes/mytheme/layouts/partials/svg.html new file mode 100644 index 0000000..e616cb7 --- /dev/null +++ b/themes/mytheme/layouts/partials/svg.html @@ -0,0 +1,6 @@ +{{ $svg := . }} +{{ $class := print $svg "-icon" }} +{{ $match := "(.*)" }} + +{{ $replaceWith := printf `${2}` $class }} +{{ return (replaceRE $match $replaceWith (printf "/static/images/%s.svg" $svg | readFile) | safeHTML) }} \ No newline at end of file diff --git a/themes/mytheme/layouts/shortcodes/begin-grid.html b/themes/mytheme/layouts/shortcodes/begin-grid.html new file mode 100644 index 0000000..b878352 --- /dev/null +++ b/themes/mytheme/layouts/shortcodes/begin-grid.html @@ -0,0 +1 @@ +
diff --git a/themes/mytheme/layouts/shortcodes/blurb.html b/themes/mytheme/layouts/shortcodes/blurb.html new file mode 100644 index 0000000..b0c9390 --- /dev/null +++ b/themes/mytheme/layouts/shortcodes/blurb.html @@ -0,0 +1,15 @@ +{{ $title := .Get "title"}} +{{ $desc := .Get "description" }} + +{{ if (.Get "url") }} + +{{ else }} + +{{ end }} diff --git a/themes/mytheme/layouts/shortcodes/end-grid.html b/themes/mytheme/layouts/shortcodes/end-grid.html new file mode 100644 index 0000000..04f5b84 --- /dev/null +++ b/themes/mytheme/layouts/shortcodes/end-grid.html @@ -0,0 +1 @@ +
diff --git a/themes/mytheme/layouts/shortcodes/rawhtml.html b/themes/mytheme/layouts/shortcodes/rawhtml.html new file mode 100644 index 0000000..520ec17 --- /dev/null +++ b/themes/mytheme/layouts/shortcodes/rawhtml.html @@ -0,0 +1,2 @@ + +{{.Inner}} \ No newline at end of file diff --git a/themes/mytheme/theme.toml b/themes/mytheme/theme.toml new file mode 100644 index 0000000..1fb3151 --- /dev/null +++ b/themes/mytheme/theme.toml @@ -0,0 +1,10 @@ +name = "xiv.zone theme" +license = "MIT" +description = "A simple theme." +tags = [] +features = [] +min_version = "0.41.0" + +[author] + name = "Joshua Goins" + homepage = "https://redstrate.com"