Skip to content

Document.Reading a player journal

github-actions[bot] edited this page Aug 25, 2026 · 6 revisions

@elite-dangerous-almanac/core / Reading a player journal

Reading a player journal

Elite Dangerous writes a newline-delimited JSON journal. Two of its events carry most of what this library is for: Loadout describes the ship the commander is flying, and FSDJump (and Location, and FSDTarget) names the system they are in.

This guide turns both into library objects, and covers what to do when the game hands you something the catalogues do not recognise.

Reading the journal

Each line is one event. Read it, parse it, and switch on event.

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

declare const journalPath: string;

const lines = createInterface({ input: createReadStream(journalPath), crlfDelay: Infinity });

for await (const line of lines) {
    if (line.trim() === '') continue;
    const event = JSON.parse(line) as { event: string };

    switch (event.event) {
        case 'Loadout':
            // → a ShipLoadout, below
            break;
        case 'FSDJump':
        case 'Location':
            // → a ProceduralSystem, below
            break;
    }
}

Loadout → a fitted ship

ShipLoadout.fromLoadout takes the event as the game wrote it.

import { BuildMetrics } from '@elite-dangerous-almanac/core/ships/build-metrics';
import { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
import type { LoadoutEvent } from '@elite-dangerous-almanac/core/ships/slef';

declare const event: LoadoutEvent;

const build = ShipLoadout.fromLoadout(event);

build.shipSymbol; // -> 'krait_light'
build.shipName; // -> 'Jenny Longuet'
build.unladenMass; // -> 388.830017   tonnes

const metrics = BuildMetrics.of(build);
metrics.maxJumpRange(); // -> 60.5478    ly, best single jump
metrics.powerBudget().withinBudget; // -> true
metrics.shieldMetrics()?.strength; // -> 743.12     MJ
metrics.armourMetrics().hitPoints; // -> 307.8

Figures the event already stated — UnladenMass, CargoCapacity, FuelCapacity — are trusted verbatim rather than recomputed, so what you read back matches what the player sees in game — while the fit they describe survives import, which when the game hands you something unknown covers. MaxJumpRange is the exception either way: it is recomputed from the drive rather than taken from the event, so it may differ in the last decimal places from the number the capture carried.

Walking the modules

slots() gives every mount on the hull, occupied or not. Slot keys come from the game and are not derivable from position, so read them rather than composing them.

import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout; // the `ShipLoadout.fromLoadout(event)` from above

for (const slot of build.slots()) {
    slot.key; // -> 'FrameShiftDrive', 'Slot01_Size6', 'LargeHardpoint1', …
    slot.name; // -> 'Frame Shift Drive'
    slot.module?.symbol; // -> undefined when the mount is empty
}

build.slots('hardpoint').length; // -> 4

These views are snapshots, not live handles. After setModule or removeModule, call slots() again.

What a capture paid, and what the build is worth

A journal's purchase figures remain separate from catalogue retail. For the source record, export options and edit behavior, see Working with SLEF.

FSDJump → a system

StarSystem and SystemAddress come straight from the event.

import { ProceduralSystem } from '@elite-dangerous-almanac/core/astro/procedural-system';

const system = ProceduralSystem.fromName('Synuefe EN-H d11-96');
system?.systemAddress; // -> 3309179996515n
system?.namingRegionName; // -> 'Synuefe'

fromName returns null rather than throwing when the name is not procedural — which is the normal case for Sol, Shinrarta Dezhra and every other hand-named system. Treat null as "this is a hand-named system", not as a failure. A missing name is different: pass a field that was not there and it throws TypeError naming the argument, so a StarSystem your parser never found does not read back as a hand-named system.

Addresses accept a bigint, a safe-integer number, or a decimal string, and are always returned as bigint. A journal parsed with JSON.parse yields a number, which is exact for every real system address; a value above 2^53 - 1 is rejected rather than silently rounded.

Where is it?

FSDJump carries StarPos as [x, y, z] light-years. Reshape it before use — the library takes {x, y, z} so the two coordinate spaces cannot be confused.

import { findHandAuthoredRegionAt } from '@elite-dangerous-almanac/core/astro/hand-authored-regions';
import { findCodexRegionAt } from '@elite-dangerous-almanac/core/astro/codex-region-lookup';
import { nearestNebulae } from '@elite-dangerous-almanac/core/astro/nebulae';
import { REAL_NEBULAE } from '@elite-dangerous-almanac/core/astro/nebulae-real';

declare const starPos: readonly [number, number, number];

const position = { x: starPos[0], y: starPos[1], z: starPos[2] };

// Answered here for StarPos [-81.625, -151.3125, -376.0625], in the Pleiades:
findHandAuthoredRegionAt(position)?.name; // -> 'Pleiades Sector'
findCodexRegionAt(position)?.name; // -> 'Inner Orion Spur'
nearestNebulae(position, REAL_NEBULAE, 1)[0]?.name; // -> 'Pleiades'

Permit locks

Pass StarSystem to the permit-lock lookup described in Systems, sectors and regions.

When the game hands you something unknown

Journals can contain hulls or modules absent from the catalogues. A direct lookup that finds nothing returns null — check it. ShipLoadout applies a narrower rule at import. An entry is kept as the event stated it when the catalogue identifies its Item and the mount can hold it, when its slot is a known cosmetic or hull-geometry key (PaintJob, ShipCockpit, a numbered decal, …), or when it is a ModularCargoBayDoor* article in the cargo-hatch mount — some hull families name their own symbol for the one built-in article the catalogue carries.

Everything else is normalized: an unknown hull is refused; unknown modules in hardpoints, utilities, optional internals and unrecognised slots are discarded; and a fixed mount is filled with that hull's stock armour, core internal or cargo hatch whenever the event did not leave a fitting article there — one the catalogues cannot resolve, one the mount cannot hold (a cargo rack in Armour, a size-8 plant in a size-2 mount, anything at all in the cargo hatch), or none at all. Only fixed mounts are corrected this way: an optional, hardpoint or utility mount may stand empty, so an article the catalogue resolves but the mount refuses is left where the event put it, for validation to report. A stock replacement carries the source's On, Priority and Health across but none of its engineering or captured value.

When normalization changes the fitted set, the capture's aggregates are dropped: mass, cargo and fuel capacity are recomputed from the fit that remains, while modulesValue and rebuy read null, since nothing records what the discarded module cost; sourcePurchase still reports the captured figures. Stocking an absent bulkhead or cargo hatch is the exception — both stock articles are weightless and free — while an absent core internal stocked from the defaults invalidates them like any other change.

build.validation() therefore reports the fit that remains: optional, hardpoint and utility modules leave empty mounts and need no diagnostic, while required armour and core mounts remain complete through their stock replacements. build.importOutcomes is the frozen, machine-readable account of each change: the exact slot, the source module where the capture named one, whether it was emptied or defaulted, and the replacement symbol when one was fitted.

import type { ShipLoadout } from '@elite-dangerous-almanac/core/ships/ship-loadout';
declare const build: ShipLoadout; // the `ShipLoadout.fromLoadout(event)` from above

build.validation().issues; // -> structural problems in the normalized fit
build.fittedModuleAt('Slot01_Size5'); // -> null if its imported symbol was unknown
build.importOutcomes; // exact import changes for display or logging

The failure model sets the validation and calculation patterns out in full.

A journal line is one Loadout event, and it is taken whole or refused: bad JSON throws SyntaxError, and a structurally impossible event — two slot keys differing only in case, say — throws TypeError from fromLoadout. Catch both when the bytes come from somewhere you do not control. A SLEF file holds several builds and can be part-good, which is its own question — Working with SLEF covers parseSlef against inspectSlef and what each does with a bad entry.

Next

API

Guides
astro
Classes (1)
ProceduralSystem

Properties

Accessors

Methods

Interfaces (16)
Type Aliases (3)
Variables (11)
Functions (37)
Subpath modules (3)
commodities
Interfaces (1)
Type Aliases (1)
Variables (3)
Functions (3)
equipment
Interfaces (6)
Type Aliases (8)
Variables (3)
Functions (10)
Subpath modules (2)
i18n
Interfaces (1)
Type Aliases (1)
Functions (17)
materials
Enumerations (2)
Interfaces (2)
Type Aliases (2)
Variables (9)
Functions (9)
ships
Classes (3)
BuildMetrics

Methods

LoadoutEditError

Constructors

Properties

Methods

ShipLoadout

Accessors

Methods

Interfaces (125)
Type Aliases (44)
Variables (10)
Functions (85)
Subpath modules (8)

Clone this wiki locally