diff --git a/src/interfaces/JournalInterface.ts b/src/interfaces/JournalInterface.ts index 178b5eb..bed3654 100644 --- a/src/interfaces/JournalInterface.ts +++ b/src/interfaces/JournalInterface.ts @@ -71,7 +71,7 @@ export class JournalInterface extends EventEmitter { // Get current location on setup, so if app is restarted, user can pick up where they left off // Rather than waiting til they jump to the next system to use the program again. getCurrentLocation(): void { - reverseLineReader.eachLine(this.currentJournal, (raw: string, last: boolean) => { + reverseLineReader.eachLine(this.currentJournal, (raw: string, last: boolean) => { if (raw) { // skip blank line at end of file const line = JSON.parse(raw) @@ -236,6 +236,7 @@ export class JournalInterface extends EventEmitter { /* ------------------------------------------------------------------------- getNavRoute ---- */ async getNavRoute(init: boolean = false) { + this.navRoute = [] // clear previous route, to catch overwritten routes let routeFile: string|null = null try { @@ -247,13 +248,26 @@ export class JournalInterface extends EventEmitter { if (routeFile) { const route: navRoute = JSON.parse(routeFile) + // system -> skip + // CURRENT -> push = true; skip + // system -> push + + let push: boolean = false route.Route.forEach((system) => { - if (system.SystemAddress !== this.location.SystemAddress) { + if (!push && system.SystemAddress === this.location.SystemAddress) { + push = true + } + + if (push && system.SystemAddress !== this.location.SystemAddress) { this.navRoute.push(new System(system)) } }) - log('Nav route set.') + if (this.navRoute.length > 0) { + log('Nav route set.') + } else { + log('No nav route found.') + } if (init) { this.emit('INIT_COMPLETE') @@ -268,13 +282,14 @@ export class JournalInterface extends EventEmitter { handleFsdJump(line: completeFsdJump): void { this.location = new System((line as completeFsdJump)) log(`FSD Jump detected, current location updated to ${this.location.name}.`) - this.emit('ENTERED_NEW_SYSTEM') if (this.navRoute.length > 0) { _.remove(this.navRoute, (system) => { - system.SystemAddress === this.location.SystemAddress + return system.SystemAddress === this.location.SystemAddress }) } + + this.emit('ENTERED_NEW_SYSTEM') } /* --------------------------------------------------------------------------- parseLine ---- */ @@ -285,6 +300,14 @@ export class JournalInterface extends EventEmitter { let DSSFlag: boolean = false switch (line.event) { + // Hyperspace jump started (3.. 2.. 1..) + case 'StartJump': { + if ('JumpType' in line && line.JumpType === 'Hyperspace') { + this.emit('ENTERING_WITCH_SPACE') + } + break + } + // CMDR jumped to new system, so update current location. case 'FSDJump': { this.handleFsdJump((line as completeFsdJump)) @@ -306,13 +329,16 @@ export class JournalInterface extends EventEmitter { break } + // CMDR set a new nav route. case 'NavRoute': { this.getNavRoute() break } + // CMDR cleared the nav route. case 'NavRouteClear': { this.navRoute = [] + log('Nav route cleared.') this.emit('SET_NAV_ROUTE') break } diff --git a/src/models/App.ts b/src/models/App.ts new file mode 100644 index 0000000..46134b7 --- /dev/null +++ b/src/models/App.ts @@ -0,0 +1,58 @@ +const chokidar = require('chokidar') +const fs = require('node:fs') +const { globSync } = require('glob') +import * as _ from 'lodash-es' +const os = require('node:os') +const path = require('node:path') + +import { Journal } from "./Journal" +import { Log } from "./Log" + +export class App { + #journalDir?: string + #journalPattern?: string + journal?: Journal + + constructor(isPackaged: boolean) { + if (!isPackaged) { // Account for WSL during development + this.#journalDir = "/mnt/c/Users/marle/Saved\ Games/Frontier\ Developments/Elite\ Dangerous/" + } else if (os.platform() === 'win32') { // Windows + this.#journalDir = os.homedir() + '\\Saved Games\\Frontier Developments\\Elite Dangerous' + } else if (os.platform() === 'linux') { // Linux + this.#journalDir = os.homedir() + '/.local/share/Steam/steamapps/compatdata/359320/pfx/drive_c/users/steamuser/Saved Games/Frontier Developments/Elite Dangerous/' + } else { + Log.write(`ERROR: Journal files not found. OS: ${os.platform()}.`) + } + + if (this.#journalDir) { + this.#journalPattern = this.#journalDir + "Journal.*.log" + this.journal = this.#getLatestJournal() + } + } + + /* ------------------------------------------------------------------- #getLatestJournal ---- */ + + // https://stackoverflow.com/questions/15696218/get-the-most-recent-file-in-a-directory-node-js + #getLatestJournal(): Journal|undefined { + const journals = globSync(this.#journalPattern) + const journalPath: string|undefined = _.maxBy(journals, file => fs.statSync(file).mtime) + + if (journalPath) { + Log.write(`New journal file found, now watching ${path.basename(journalPath)}.`) + return new Journal(journalPath) + } else { + Log.write('ERROR: Unable to find latest journal.') + return undefined + } + } + + /* --------------------------------------------------------------------- watchJournalDir ---- */ + + watchJournalDir(): void { + const watcher = chokidar.watch(this.#journalPattern, {usePolling: true, persistant: true}) + + watcher.on('add', () => this.journal = this.#getLatestJournal()) + + Log.write('Watching journal folder for changes...') + } +} \ No newline at end of file diff --git a/src/models/Journal.ts b/src/models/Journal.ts new file mode 100644 index 0000000..c1ab055 --- /dev/null +++ b/src/models/Journal.ts @@ -0,0 +1,326 @@ +import type { Tail as TailType } from 'tail' +import type { autoScan, completeFsdJump, detailedScan, journalEntry, navRoute, planetScan } from "../@types/journalLines" + +import * as _ from 'lodash-es' +const path = require('node:path') +const { readFile } = require('node:fs/promises') +const reverseLineReader = require('reverse-line-reader') +const Tail = require('tail').Tail + +import EventEmitter from "events" +import { System } from "./System" +import { Log } from "./Log" +import { Body } from "./Body" + + +export class Journal extends EventEmitter { + #path: string + location: System + navRoute: System[] + + constructor(journalPath: string) { + super() + + this.#path = journalPath + this.location = new System('Unknown') + this.navRoute = [] + + // Start ReverseLineReader chain here. + Log.write(`Journal initialized. Attempting to find current location.`) + this.#getLastFsdJump() + // -> IF no FSD Jump: this.#getLastLocation() + // --> this.#getScannedBodies() + } + + /* --------------------------------------------------------------------- #getLastFsdJump ---- */ + + // Get current location on setup, so if app is restarted, user can pick up where they left off + // Rather than waiting til they jump to the next system to use the program again. + #getLastFsdJump(): void { + reverseLineReader.eachLine(this.#path, (raw: string) => { + if (raw) { //skip blank line at end of file + const line: journalEntry = JSON.parse(raw) + + if (line.event === 'FSDJump') { + this.location = new System((line as completeFsdJump)) + Log.write(`Current location set to ${this.location.name}.`) + this.emit('ENTERED_NEW_SYSTEM') + return false + } + } + }).then(() => { + if (this.location.name === 'Unknown') { + Log.write('Unable to find last hyperspace jump. Searching for last known location.') + this.#getLastLocation() + } else { + Log.write('Attempting to find scanned bodies in current system.') + this.#getScannedBodies() + } + }) + } + + /* -------------------------------------------------------------------- #getLastLocation ---- */ + + // If no FSDJump found, search for a location entry as this is populated when journal is created. + #getLastLocation(): void { + reverseLineReader.eachLine(this.#path, (raw: string, last: boolean) => { + // Extra check just to be sure. + if (this.location.name !== 'Unknown') { + return false + } + + if (raw) { + const line: journalEntry = JSON.parse(raw) + + if (line.event === 'Location') { + this.location = new System((line as completeFsdJump)) + Log.write(`Current location set to ${this.location.name}.`) + this.emit('ENTERED_NEW_SYSTEM') + return false + + } else if (last) { + Log.write('WARNING: Unable to find last known location.') + return false + } + } + }).then(() => { + if (this.location.name !== 'Unknown') { + Log.write('Attempting to find scanned bodies in current system.') + this.#getScannedBodies() + } + }) + } + + /* ------------------------------------------------------------------- #getScannedBodies ---- */ + + // Look for all scanned bodies before last FSDJump, for same reasons as getting location. + #getScannedBodies(): void { + let dssLine: detailedScan|null = null + + reverseLineReader.eachLine(this.#path, (raw: string) => { + if (raw) { + const line: journalEntry = JSON.parse(raw) + + // Check if previous line was ScanType = Detailed, and handle that. + if (dssLine) { + if (line.event === 'SAAScanComplete') { + // This was a DSS, so add to list with DSS flag set to true. + this.location.bodies.push(new Body(dssLine, true)) + } else { + // Else, check that the body hasn't already been added (by a DSS scan line). + const dupChecker = {'BodyName': dssLine.BodyName, 'BodyID': dssLine.BodyID} + const r = _.find(this.location.bodies, dupChecker) + + if (r === undefined) { + // Body was not already logged, so add to list. + this.location.bodies.push(new Body(dssLine)) + } + } + + // Finally, clear the variable. + dssLine = null + } + + // Now move on to evaluating the current line. + if (line.event === 'Scan' && 'ScanType' in line) { + // If ScanType = Detailed and body is not a star, save the line so we can check + // the one immediately above for event = SAAScanComplete, which indicates this + // was a DSS. + if (line.ScanType === 'Detailed' && !('StarType' in line)) { + dssLine = (line as detailedScan) + + } else if ('StarType' in line) { // Save stars to bodies list. + this.location.bodies.push(new Body((line as autoScan|detailedScan))) + + } else if (line.ScanType === 'AutoScan') { // Save auto/discovery scan bodies. + // Check if planet, and then do the duplicate check (otherwise it's an + // astroid, as we've already accounted for stars). + if ('PlanetClass' in line) { + const dupChecker = { + 'BodyName': (line as planetScan<'AutoScan'>).BodyName, + 'BodyID': (line as planetScan<'AutoScan'>).BodyID, + } + const r = _.find(this.location.bodies, dupChecker) + + if (r === undefined) { + this.location.bodies.push(new Body((line as autoScan))) + } + + } else { // Asteroids. + this.location.bodies.push(new Body((line as autoScan))) + } + } + + } else if (line.event === 'FSDJump') { + // Stop evaluating once we reach the beginning of current system entries. + return false + } + } + }).then(() => { + if (this.location.bodies.length > 0) { + Log.write('No scanned bodies found in current system.') + this.emit('BUILD_BODY_LIST') + } else { + Log.write('Scanned bodies found.') + } + + Log.write('Checking for nav route.') + this.#getNavRoute() + }) + } + + /* ------------------------------------------------------------------------ #getNavRoute ---- */ + + async #getNavRoute(): Promise { + this.navRoute = [] // Clear previous route, to catch overwritten routes. + let routeFile: string|null = null + + try { + const filePath: string = path.dirname(this.#path) + 'NavRoute.json' + routeFile = await readFile(filePath, {encoding: 'utf8'}) + } catch (err) { + Log.write(`Error reading nav route file: ${err.message}.`) + } + + if (routeFile) { + const route: navRoute = JSON.parse(routeFile) + + // system -> skip + // CURRENT -> push = true; skip + // system -> push + let push: boolean = false + route.Route.forEach((system) => { + if (!push && system.SystemAddress === this.location.SystemAddress) { + push = true + } + + if (push && system.SystemAddress !== this.location.SystemAddress) { + this.navRoute.push(new System(system)) + } + }) + + if (this.navRoute.length > 0) { + Log.write('Nav route set.') + } else { + Log.write('No nav route found.') + } + + // Call this no matter what, so that cleared routes are properly dealt with. + this.emit('SET_NAV_ROUTE') + } + } + + /* ----------------------------------------------------------------------------- watch() ---- */ + + // Watch the journal for changes. + watch(): void { + const tail: TailType = new Tail(this.#path, {useWatchFile: true}) + + Log.write(`Watching ${path.basename(this.#path)}...`) + + tail.on('line', (data) => data ? this.#parseLine(data) : undefined) + tail.on('error', (err) => Log.write(`Tail error in Journal.watch(): ${err}`)) + } + + /* ------------------------------------------------------------------------ #parseLine() ---- */ + + // Parse and handle journal lines. + #parseLine(raw: string) { + const line: journalEntry = JSON.parse(raw) + let dssFlag: boolean = false + + switch (line.event) { + // Hyperspace jump started (3.. 2.. 1..) + case 'StartJump': { + if ('JumpType' in line && line.JumpType === 'Hyperspace') { + this.emit('ENTERING_WITCH_SPACE') + } + break + } + + // CMDR jumped to new system, so update current location. + case 'FSDJump': { + this.#handleFsdJump((line as completeFsdJump)) + break + } + + // CMDR completed DSS scan, so set flag for when next line processes and we want to + // figure out what kind of scan occurred. + case 'SAAScanComplete': { + dssFlag = true + break + } + + // A scan occurred, so let's hand that info off to the appropriate function and then + // reset the DSS flag. + case 'Scan': { + this.#handleScanLine((line as autoScan|detailedScan), dssFlag) + dssFlag = false + break + } + + // CMDR set a new nav route. + case 'NavRoute': { + this.#getNavRoute() + break + } + + // CMDR cleared the nav route. + case 'NavRouteClear': { + this.navRoute = [] + Log.write('Nav route cleared.') + this.emit('SET_NAV_ROUTE') + break + } + } + } + + /* ---------------------------------------------------------------------- #handleFsdJump ---- */ + + #handleFsdJump(line: completeFsdJump): void { + this.location = new System(line) + Log.write(`FSD Jump detected, current location updated to ${this.location.name}.`) + + if (this.navRoute.length > 0) { + _.remove(this.navRoute, (system) => { + return system.SystemAddress === this.location.SystemAddress + }) + } + + this.emit('ENTERED_NEW_SYSTEM') + } + + /* --------------------------------------------------------------------- #handleScanLine ---- */ + + #handleScanLine(line: autoScan|detailedScan, DSS: boolean = false) { + const dupChecker = {'BodyName': line.BodyName, 'BodyID': line.BodyID} + let body: Body|null = null + + // If it's a DSS scan, then we should have already added the body to the list. But we'll + // check to make sure. + if (DSS) { + // Using findIndex() rather than find() so we can edit the body if found. + const bodyIndex: number = _.findIndex(this.location.bodies, dupChecker) + + if (bodyIndex > -1) { // Body was found in list, so simply toggle the DSS flag. + this.location.bodies[bodyIndex].DSSDone = true + + } else { // Body was missed on initial journal scan, so add it to the list. + body = new Body(line, true) + this.location.bodies.push(body) + } + + } else { // Otherwise it's an FSS or auto scan, and needs to be added to the list. + // Probably overkill, but do a duplicate check just in case. + const r = _.find(this.location.bodies, dupChecker) + + if (r === undefined) { + body = new Body(line) + this.location.bodies.push(body) + } + } + + Log.write(`Scan detected. Body: ${line.BodyName}.`) + this.emit('BODY_SCANNED', body, DSS) + } +} \ No newline at end of file diff --git a/src/models/Log.ts b/src/models/Log.ts new file mode 100644 index 0000000..58d2437 --- /dev/null +++ b/src/models/Log.ts @@ -0,0 +1,7 @@ +export class Log { + constructor() {} + + static write(message: string): void { + console.log(message) + } +} \ No newline at end of file diff --git a/src/models/UI.js b/src/models/UI.js index 15477e1..d0fc084 100644 --- a/src/models/UI.js +++ b/src/models/UI.js @@ -1,9 +1,24 @@ export class UI { constructor() {} + /* --------------------------------------------------------------------- enterWitchSpace ---- */ + + static enterWitchSpace() { + $('#highValueScans').children().remove() + $('#lowValueScans').children().remove() + + $('#currentSystem').removeClass('charted').addClass('highlighted text-center') + $('#currentSystemIcon').addClass('hidden') + + $('#currentSystemName').text('> > > Hyperspace < < <') + } + /* -------------------------------------------------------------------- setCurrentSystem ---- */ static setCurrentSystem(system) { + $('#highValueScans').children().remove() + $('#lowValueScans').children().remove() + if (system.name === 'Unknown') { $('#currentSystem').removeClass('charted').addClass('highlighted text-center') $('#currentSystemIcon').addClass('hidden') diff --git a/src/renderer.js b/src/renderer.js index 38dfc9a..ef270f9 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -57,11 +57,9 @@ journal.watchJournal() const test = {name: 'Test', ID: 'TestID'} -/* --------------------------------------------------------------------------- init complete ---- */ - -journal.once('INIT_COMPLETE', () => { - UI.setCurrentSystem(journal.location) +/* ------------------------------------------------------------------------- build body list ---- */ +journal.once('BUILD_BODY_LIST', () => { if (journal.location?.bodies?.length > 0) { journal.location.bodies.forEach((body) => { const row = UI.createBodyRow(body) @@ -71,13 +69,21 @@ journal.once('INIT_COMPLETE', () => { } }) +/* ----------------------------------------------------------------- started hyperspace jump ---- */ + +journal.on('ENTERING_WITCH_SPACE', () => UI.enterWitchSpace()) + /* ---------------------------------------------------------------------- entered new system ---- */ journal.on('ENTERED_NEW_SYSTEM', () => { - $('#highValueScans').children().remove() - $('#lowValueScans').children().remove() + UI.setCurrentSystem(journal.location) - $('#currentSystemName').text(journal.location.name) + $(`#${CSS.escape(journal.location.SystemAddress)}`).remove() + + // verify that the internal navRoute matches the UI navRoute, and rebuild it if not + if ($('#navRoute').children().length !== journal.navRoute.length) { + journal.emit('SET_NAV_ROUTE') + } }) /* ---------------------------------------------------------------------- body scan detected ---- */ @@ -109,7 +115,7 @@ journal.on('SET_NAV_ROUTE', () => { // clear previous nav route, if any $('#navRoute').children().remove() - if (journal.navRoute.length > 1) { + if (journal.navRoute.length > 0) { journal.navRoute.forEach((system) => { // duplicate check // CSS.escape is needed since CSS technically doesn't allow numeric IDs