Automated performance analysis and suggestion platform for Final Fantasy XIV: Shadowbringers, using data sourced from FF Logs.
Before starting, you will need:
Once you've set those up, you'll need to pull down the codebase. If you plan to contribute code, you'll need to create a fork of the project first, and use your fork's URL in place of the main project's when cloning.
# Clone the project
git clone https://github.com/xivanalysis/xivanalysis.git
cd xivanalysis
NOTE: Drop past our Discord channel before you get too into it, have a chat! Duping up on implementations is never fun.
If you are working with a fork, I would highly suggest configuring an upstream remote, and making sure you sync it down reasonably frequently - you can check the #automations channel on Discord to get an idea of what's been changed.
You've now got the primary codebase locally, next you'll need to download all the project's dependencies. Please do use yarn
for this - using npm
will ignore the lockfile, and potentially pull down untested updates.
yarn
While yarn
is running, copy the .env.local.example
file in the project root, and call it .env.local
. Make a few changes in it:
- Replace
TODO_FINAL_DEPLOY_URL
withhttps://www.fflogs.com/v1/
. - Replace
INSERT_API_KEY_HERE
with your public fflogs api key. If you don't have one, you can get yours here. Don't forget to set your Application Name there as well.
NOTE: If you are also configuring the server locally, you can use [server url]/proxy/fflogs/
as the base url, and omit the api key.
Once that's done, you're ready to go! To start the development server, just run
yarn start
If you would like to compile a production copy of the assets (for example, to be served when testing the server), run
yarn build
The parser is the meat of xivanalysis. Its primary job is to orchestrate modules, which read event data and output the final analysis.
The modules are split into a number of groups:
core
: Unsurprisingly, the core system modules. These provide commonly-used functionality (see the section on dependency below), as well as job-agnostic modules such as "Don't die".jobs/[job]
: Each supported job has its own group of modules, that provide specialised analysis/information for that job.bosses/[boss]
: Like jobs, some bosses have groups of modules, usually used to analyse unique fight mechanics, or provide concrete implementations that fflogs does not currently provide itself.
Modules from core
are loaded first, followed by bosses, then jobs.
Each group of modules is contained in its own folder, alongside any other required files. All groups also require an index.js
, which provides a reference to all the modules that should be loaded. These index files are referenced in parser/AVAILABLE_MODULES.js
With the parser orchestrating the modules, it's down to the modules themselves to analyse the data and provide the final output.
Each module should be in charge of analysing a single statistic or feature, so as to keep them as small and digestible as reasonably possible. To aid in this, modules are able to 'depend' on others, and directly access any data they may expose. Modules are guaranteed to run before anything that depends on them - this also implicitly prevents circular dependencies from being formed (an error will be thrown).
For more details, check out the API Reference below, and have a look through the core
and jobs/smn
modules.
This project makes use of jsLingui with a dash of custom logic to make dynamic content a bit easier. It's recommended to familiaise yourself with its available components to help implementation.
All text displayed to the end-user should be passed through this translation layer. See below for a few examples.
All translated strings should be given an explicit ID, to help keep things consistent. This project formats i18n IDs using the syntax: [job].[module].[thing]
As an example, for a Red Mage you might end up with the key rdm.gauge.white-mana
. These
keys should be somewhat descriptive to make it clear for translators what exactly they're editing.
If your module has output
, it should also be given a translated title. This title will be shown above its output, as well as used for the link in the sidebar.
import {t} from '@lingui/macro'
import Module from 'parser/core/Module'
export default class MyModule extends Module {
// ...
static title = t('my-job.my-module.title')`My Module`
// ...
}
In most cases, you can skip the peculiar syntax shown above, and use the Trans
JSX tag, which automates a lot of the hard yards for you. This is commonly seen in use in module output and suggestions, among other things. There's a number of other utility tags besides Trans
, such as Plural
- see the lingui documentation for more info.
import {Trans, Plural} from '@lingui/react'
import ACTIONS from 'data/ACTIONS'
import {Suggestion, SEVERITY} from 'parser/core/modules/Suggestions'
const supportedLanguages = 6
this.suggestions.add(new Suggestion({
icon: ACTIONS.RAISE.icon,
severity: SEVERITY.MORBID,
content: <Trans id="my-job.my-module.suggestions.my-suggestion.content">
You should <strong>really</strong> use localization.
</Trans>,
why: <Trans id="my-job.my-module.suggestions.my-suggestion.why">
Localization is important, we support
<Plural
value={supportedLanguages}
one="# language"
other="# languages"
/>
</Trans>,
}))
Sometimes, you really gotta put a lot of content in - it's cases like this that markdown comes in handy. We use a slightly extended syntax based on CommonMark.
Key differences:
[~action/ACTION_KEY]
will automatically turn into anActionLink
with icon, tooltip, and similar.[~status/STATUS_KEY]
will likewise automatically turn into aStatusLink
.- Don't use code blocks (
`...`
). Just... don't. Please. It breaks everything.
import {t} from '@lingui/macro'
import TransMarkdown from 'components/ui/TransMarkdown'
const description = t('your-job.about.description')`
This is an _example_ of using **markdown** in conjunction with the TransMarkdown component.
I am also [contractually](https://some-url.com/) obliged to remind you to [~action/RUIN_III] everything.
`
const rendered = <TransMarkdown source={description}/>
All modules should extend this class at some point in their hierarchy. It provides helpers to handle events, and provides a standard interface for the parser to work with.
Required. The name that should be used to reference this module throughout the system/dependencies. Without this set, the module will break during build minification.
The name that should be shown above any output the module generates. If not set, it will default to the module's handle
, with the first letter capitalised.
Should be wrapped in the t(id)
template from @lingui/macro
, see above for details.
An array of module handles that this module depends on. Modules listed here will always be executed before the current module, and will be available on the this.<handle>
instance property.
A number used to control the position the module should have in the final output. The core Module file exports the DISPLAY_ORDER
const with a few defaults.
Add an event hook.
event
should be the name of the event you wish to listen for. 'all'
can be passed to listen for all events.
filter
, if specified, is an object specifying properties that must be matched by an event for the hook to fire. Keys can be anything that the event may have. There are a few special keys and values available to the filter:
- Setting the value of a property to an array will check if any of the values match the event.
abilityId: <value>
is shorthand forability: {guid: <value>}
by: <value>
andto: <value>
are shorthand forsourceID
andtargetID
respectively, and support the following additional values:'player'
: The ID of the current player'pet'
: The IDs of all the current player's pets.
callback
is the function that should be called when an event (optionally passing the filter) is run. It will receive the full event object as its first parameter.
An object representing the added hook is returned, that can be later used to modify it. The actual structure of this hook object should not be relied upon.
Override this function to provide output for the user. Any markup returned will be displayed on the analysis page, under a header defined by static title
.
Return false
(the default implementation does this) to prevent generating output for the module.
Override this function if the module absolutely needs to process events before the official 'parse', such as to add missing applybuff
events. Avoid if addHook
could be used instead.
events
is an array of every event that is about to be parsed.
Return value should be the events
array, with any required modifications made to it. Failing to return this will prevent the parser from parsing any events at all.
Override this function to customize the information that the module provides for automatic
error reporting. This function is called when an error occurs in event hooks or the
output()
method on the faulting module as well as all modules that module depends on.
source
is either event
or output
error
is the error that occurred
event
is the error that was being processed when the error occurred, if applicable
If this function is not overridden or if this function returns undefined, primitive values will be scraped from the module and uploaded with the error report.
The core parser object, orchestrating the modules and providing meta data about the fight. All modules have access to an instance of this via this.parser
.
The full report metadata object, and the specific fight and player object from it for the current parse, respectively. The data in these is direct from FFLogs, check your networking tab to see the structure.
The timestamp of the event currently being parsed in ms. Note that timestamps do not start at 0. Subtract fight.start_time
to get a relative timestamp
The remaining duration in the fight (yes, I'm aware it's badly named), in ms.
An array of friendly actors that took part in the fight currently being parsed.
Trigger an event throughout the system.
event
should be the event object being called. A type
property must be defined for this do anything. If not specified, timestamp
will be set to the current timestamp.
Checks if the specified event was by/to the specified player. If playerId
is not set, the current user will be used.
The same as their xxPlayer
counterparts, but check if the event was by/to one of the specified player's pets.
Formats the specified duration (in ms) as a MM:SS
string. If under 60s, will display seconds with specified precision (default 2 decimal places).
The result of formatDuration
for the duration of the fight up until the specified timestamp.