├── .eslintrc ├── .gitattributes ├── .npmignore ├── src ├── utilities │ └── index.js ├── defaults │ └── index.js ├── constants │ └── index.js └── index.js ├── examples ├── server │ ├── index.js │ ├── port.js │ ├── REAMDE.md │ └── custom.js ├── client │ ├── basic.js │ ├── REAMDE.md │ ├── index.js │ └── discovery.js ├── README.md └── mocks │ ├── Journal.171017123456.01.log │ ├── Journal.171016003403.02.log │ └── Journal.171016003403.01.log ├── .github ├── pull_request_template.md ├── FUNDING.yml └── issue_template.md ├── .editorconfig ├── CODE_OF_CONDUCT.md ├── LICENSE ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "node": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.js -crlf 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github* 2 | examples* 3 | .editorconfig 4 | .eslintrc 5 | .gitattributes 6 | .gitignore 7 | CODE_OF_CONDUCT.md 8 | CONTRIBUTING.md 9 | -------------------------------------------------------------------------------- /src/utilities/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | formatClientName(name) { 3 | const clientName = name.replace(/\s\s+/g, ' '); 4 | 5 | return clientName.trim(); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /examples/server/index.js: -------------------------------------------------------------------------------- 1 | const EliteDangerousJournalServer = require('../../src/index.js'); 2 | 3 | const JournalServer = new EliteDangerousJournalServer(); 4 | 5 | JournalServer.init(); 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # `[insert Issue # here]` 2 | 3 | **Proposed Changes**: 4 | 5 | *Add a list of changes/fixes here* 6 | 7 | --- 8 | 9 | @DVDAGames/ed-journal-server-maintainers 10 | -------------------------------------------------------------------------------- /examples/server/port.js: -------------------------------------------------------------------------------- 1 | const EliteDangerousJournalServer = require('../../src/index.js'); 2 | 3 | const JOURNAL_SERVER_PORT = 0; // should generate a random port 4 | 5 | const JournalServer = new EliteDangerousJournalServer(JOURNAL_SERVER_PORT); 6 | 7 | JournalServer.init(); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | # two spaces for tabs 8 | [*] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ericrallen] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ericrallen 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: https://supporters.eff.org/donate/ 9 | -------------------------------------------------------------------------------- /examples/client/basic.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const WebSocket = require('ws'); 4 | 5 | // Journal Server connection 6 | const ws = new WebSocket('ws://localhost:31337'); 7 | 8 | // Journal Server broadcast 9 | ws.on('message', (data) => { 10 | // parse our stringified JSON 11 | const eventData = JSON.parse(data); 12 | 13 | // extract Journal payload from broadcast 14 | const { payload } = eventData; 15 | 16 | // new Journal file 17 | if (payload.event === 'Fileheader') { 18 | console.log(`${eventData.journal}`); 19 | // other event 20 | } else { 21 | console.log(eventData); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Journal Server Examples 2 | 3 | To run the most basic examples: 4 | 5 | 1. Clone this repo and `cd` into it 6 | 2. Run the following command in your Terminal: 7 | ```shell 8 | npm install 9 | ``` 10 | 3. Run the following command in your Terminal: 11 | ```shell 12 | node examples/server/index.js 13 | ``` 14 | 4. Open another Terminal 15 | 5. Run the following command in your second Terminal: 16 | ```shell 17 | node examples/client/index.js 18 | ``` 19 | 6. Run *Elite Dangerous* 20 | 7. Watch as the client responds to the Journal Events broadcast by the server 21 | 22 | **NOTE**: Each directory also has more advanced examples of Journal Server usage. 23 | -------------------------------------------------------------------------------- /examples/server/REAMDE.md: -------------------------------------------------------------------------------- 1 | ## Server Examples 2 | 3 | This directory contains some basic examples of how to initialize your Journal Server: 4 | 5 | - [index.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/server/index.js): 6 | is the simplest example of initializing a Journal Server with default configuration 7 | - [port.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/server/port.js): 8 | is an example of setting up a Journal Server on a custom port 9 | - [custom.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/server/custom.js): 10 | is an example of passing customized configuration to the Journal Server 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | ## Code of Conduct 3 | 4 | 1. No trolling, harassing, name-calling, etc. will be tolerated in Issues, Pull 5 | Requests, Wiki Pages, etc. 6 | 2. Leave gender, race, sexual orientation, age, religion, etc. out of it 7 | 3. Keep Code Reviews civil and feedback non-personal; we all want to improve and 8 | code Reviews are some of the best teaching and learning moments that we have. 9 | 4. Be excellent to each other. 10 | 5. Don't fly without your rebuy. 11 | 12 | ## Repercussions 13 | 14 | Breaking `#1` or `#2` **WILL** result in no longer being associated with this repo. 15 | 16 | Breaking `#3` **MAY** result in no longer being assicated with this repo. 17 | 18 | Breaking `#4` will leave you sad and lonely. 19 | 20 | Breaking `#5` may result in deep despair and in-game financial ruin. 21 | -------------------------------------------------------------------------------- /examples/server/custom.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v5'); 2 | 3 | const EliteDangerousJournalServer = require('../../src/index.js'); 4 | 5 | const UUID_NAMESPACE = 'ws://journalserver.dvdagames.com/'; 6 | 7 | const port = 12345; 8 | const serviceName = 'Example Elite Dangerous Journal Server'; 9 | const id = uuid(UUID_NAMESPACE, uuid.URL); 10 | const headers = { 11 | TESTING: true, 12 | }; 13 | const interval = 1000; 14 | 15 | const config = { 16 | port, 17 | id, 18 | headers, 19 | watcher: { 20 | interval, 21 | }, 22 | subscriptions: { 23 | enabled: true, 24 | }, 25 | discovery: { 26 | serviceName, 27 | }, 28 | registration: { 29 | force: true, 30 | }, 31 | }; 32 | 33 | const JournalServer = new EliteDangerousJournalServer(config); 34 | 35 | JournalServer.init(); 36 | -------------------------------------------------------------------------------- /examples/client/REAMDE.md: -------------------------------------------------------------------------------- 1 | ## Client Examples 2 | 3 | This directory contains some basic examples of how to configure your client for 4 | use with a Journal Server: 5 | 6 | - [index.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/client/index.js): 7 | is a fairly standard client implementation for subscribing to specific Journal events 8 | - [basic.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/client/basic.js): 9 | is a barebones client implementation for listening to Journal events 10 | - [discovery.js](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/client/discovery.js): 11 | utilizes network discovery via Zeroconf/Bonjour to discover a Journal Server and 12 | connect to it 13 | 14 | **NOTE**: The `discover.js` example is the only one that can be run without the server 15 | already up and running. 16 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Please try to answer the following 3 | 4 | 1. Was this issue on the **Journal Server** or a **Client** connected to the Journal 5 | Server? 6 | 2. Does the Journal Server have a **custom config**? 7 | - If so, please provide it below in the `Custom Journal Server Config` section 8 | 3. Can you **recreate the issue** or was it a **one-time occurrence**? 9 | - If you can recreate it, is there a particular Journal Event that causes it? 10 | - If you cannot recreate it, is there any information you can give us about what 11 | you were doing in-game when it happened? 12 | 4. What **version** of the *Elite Dangerous Journal Server* are you running? 13 | 14 | ## Please provide the relevant Journal Server Broadcast if you have it 15 | 16 | *Your Client should be able to give you this information.* 17 | 18 | ```json 19 | {} 20 | ``` 21 | 22 | ### Custom Journal Server Config 23 | 24 | *You can remove this section if you don't have a custom Journal Server configuration* 25 | 26 | ```json 27 | {} 28 | ``` 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dead Villager Dead Adventurer Games 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | !examples/mocks/*.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | -------------------------------------------------------------------------------- /examples/client/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const WebSocket = require('ws'); 4 | 5 | // Journal Server connection 6 | const ws = new WebSocket('ws://localhost:31337'); 7 | 8 | ws.on('open', () => { 9 | // we want to subscribe 10 | const eventType = 'subscribe'; 11 | 12 | // these are two easy events to see when starting up the Journal Server 13 | // and the Elite Dangerous game so that you don't have to work too hard to test 14 | // successful subscriptions 15 | const payload = ['Music', 'Fileheader']; 16 | 17 | // the server update our subscriptions 18 | ws.send(JSON.stringify({ type: eventType, payload })); 19 | 20 | // the server should return an error for it's payload 21 | ws.send(JSON.stringify({ testing: true })); 22 | }); 23 | 24 | // Journal Server broadcast 25 | ws.on('message', (data) => { 26 | // parse our stringified JSON 27 | const eventData = JSON.parse(data); 28 | 29 | // extract Journal payload from broadcast 30 | const { payload } = eventData; 31 | 32 | // new Journal file 33 | if (payload.event === 'Fileheader') { 34 | console.log(`${eventData.journal}`); 35 | // other event 36 | } else { 37 | console.log(eventData); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dvdagames/elite-dangerous-journal-server", 3 | "version": "3.2.0", 4 | "description": "WebSocket server for broadcasting Elite Dangerous Captain's Journal updates", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "lint": "eslint src/**/*js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "dvdagames", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/DVDAGames/elite-dangerous-journal-server.git" 15 | }, 16 | "private": false, 17 | "keywords": [ 18 | "elite dangerous", 19 | "journal", 20 | "elite", 21 | "dangerous", 22 | "ed" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/DVDAGames/elite-dangerous-journal-server/issues" 26 | }, 27 | "homepage": "https://github.com/DVDAGames/elite-dangerous-journal-server", 28 | "dependencies": { 29 | "bonjour": "^3.5.0", 30 | "chalk": "^2.1.0", 31 | "chokidar": "^1.7.0", 32 | "lodash": "^4.17.4", 33 | "moment": "^2.19.1", 34 | "node-dir": "^0.1.17", 35 | "uuid": "^3.1.0", 36 | "ws": "^3.2.0" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^4.9.0", 40 | "eslint-config-airbnb": "^16.0.0", 41 | "eslint-plugin-import": "^2.7.0", 42 | "eslint-plugin-jsx-a11y": "^6.0.2", 43 | "eslint-plugin-react": "^7.4.0", 44 | "pre-commit": "^1.2.2" 45 | }, 46 | "pre-commit": [ 47 | "lint" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/defaults/index.js: -------------------------------------------------------------------------------- 1 | const constants = require('../constants'); 2 | 3 | /** 4 | * Default configuration Object for our constructor 5 | * @type {Object} 6 | */ 7 | module.exports = { 8 | port: constants.JOURNAL_SERVER_PORT, 9 | watcher: { 10 | interval: constants.JOURNAL_WATCH_INTERVAL, 11 | path: constants.JOURNAL_DIR, 12 | fileRegEx: constants.JOURNAL_FILE_REGEX, 13 | }, 14 | discovery: { 15 | enabled: constants.SERVER_ALLOW_DISCOVERY, 16 | serviceType: constants.JOURNAL_SERVER_SERVICE_TYPE, 17 | serviceName: constants.JOURNAL_SERVER_SERVICE_NAME, 18 | }, 19 | heartbeat: { 20 | interval: constants.SERVER_HEARTBEAT_INTERVAL, 21 | }, 22 | registration: { 23 | enabled: constants.SERVER_ALLOW_REGISTRATION, 24 | force: constants.SERVER_FORCE_REGISTRATION, 25 | messageType: constants.SERVER_REGISTRATION_MESSAGE_TYPE, 26 | }, 27 | subscriptions: { 28 | enabled: constants.SERVER_ALLOW_SUBSCRIPTIONS, 29 | subscribeTo: constants.DEFAULT_EVENT_SUBSCRIPTIONS, 30 | messageType: constants.SERVER_SUBSCRIPTION_MESSAGE_TYPE, 31 | }, 32 | errors: { 33 | mustRegister: { 34 | message: constants.ERROR_MUST_REGISTER_MSG, 35 | code: constants.ERROR_MUST_REGISTER_CODE, 36 | }, 37 | invalidMessage: { 38 | message: constants.ERROR_INVALID_MESSAGE_MSG, 39 | code: constants.ERROR_INVALID_MESSAGE_CODE, 40 | }, 41 | invalidPayload: { 42 | message: constants.ERROR_INVALID_PAYLOAD_MSG, 43 | code: constants.ERROR_INVALID_PAYLOAD_CODE, 44 | }, 45 | }, 46 | headers: {}, 47 | }; 48 | -------------------------------------------------------------------------------- /examples/client/discovery.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const WebSocket = require('ws'); 4 | const zeroconf = require('bonjour')(); 5 | 6 | // name for the service we want to discover 7 | const SERVICE_NAME = 'Elite Dangerous Journal Server'; 8 | 9 | // initialize variable to hold our WebSocket 10 | let socket; 11 | 12 | console.log(`Searching for ${SERVICE_NAME}...`); 13 | 14 | // search for WebSocket services 15 | const discovery = zeroconf.find({ type: 'ws' }); 16 | 17 | // if our service goes down 18 | discovery.on('down', (server) => { 19 | console.log(`Disconnected from Journal Server ${server.txt.id}`); 20 | 21 | // kill socket connection 22 | socket.close(); 23 | }); 24 | 25 | // when our service comes up 26 | discovery.on('up', (server) => { 27 | // check to see if this is the serivce we are loking for 28 | if (server.name === SERVICE_NAME) { 29 | // get relevant info from Service 30 | const { 31 | addresses, 32 | type, 33 | port, 34 | txt: { 35 | id, 36 | }, 37 | } = server; 38 | 39 | // we're going to want to connect via ipv4 for this one 40 | const ipv4RegEx = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/; 41 | 42 | // initialize variable to store correct address 43 | let validAddress; 44 | 45 | // iterate through addresses (ipv6 and ipv4) 46 | addresses.some((address) => { 47 | // check to see if we have an ipv4 address 48 | if (ipv4RegEx.test(address)) { 49 | // save ipv4 address 50 | validAddress = address; 51 | 52 | // break out of Array.some() 53 | return true; 54 | } 55 | 56 | // continue execution 57 | return false; 58 | }); 59 | 60 | // if we found a valid address 61 | if (validAddress) { 62 | // create our socket connection 63 | socket = new WebSocket(`${type}://${validAddress}:${port}`); 64 | 65 | // catch socket error 66 | socket.on('error', () => { 67 | console.log('Journal Server WebSocket Error'); 68 | }); 69 | 70 | // once we have successfully connected to our Journal Server 71 | socket.on('open', () => { 72 | console.log(`Connected to Journal Server ${id}`); 73 | }); 74 | 75 | // Journal Server broadcast 76 | socket.on('message', (data) => { 77 | // parse our stringified JSON 78 | const eventData = JSON.parse(data); 79 | 80 | // extract Journal payload from broadcast 81 | const { payload } = eventData; 82 | 83 | // if there was an error 84 | if (payload.error) { 85 | console.log('Journal Server Communication Error'); 86 | } 87 | 88 | // new Journal file 89 | if (payload.event === 'Fileheader') { 90 | console.log(`Journal: ${eventData.journal}`); 91 | // other event 92 | } else { 93 | console.log(eventData); 94 | } 95 | }); 96 | } 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | ## Introduction 3 | 4 | Thanks for your interest in contributing to **Elite Dangerous Journal Server**! 5 | 6 | We're excited to have you help out. 7 | 8 | Every bug report, pull request, question, and suggestion is important to us. 9 | 10 | Below you will find some information for bug reporting as well as some quick guidelines 11 | and steps for contributing. 12 | 13 | Be sure to check out the [Code of Conduct](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/CODE_OF_CONDUCT.md). 14 | 15 | ## Bug Reporting 16 | 17 | Thanks for taking the time to let us know about errors you encounter. This 18 | is a very important part of making this software better. 19 | 20 | We really appreciate your time. 21 | 22 | When creating an [Issue](https://github.com/DVDAGames/elite-dangerous-journal-server/issues), 23 | please try to follow the template we've provided and provide as much detail as possible 24 | about what was going on when the error occurred. 25 | 26 | ## Contributing Guidelines 27 | 28 | - This project follows the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) 29 | and as long as you `npm install` you should get the necessary `eslint` dependencies for checking 30 | your code; it's recommended to install the [eslint plugin](https://eslint.org/docs/user-guide/integrations) 31 | for your editor 32 | - This project makes use of [EditorConfig](http://editorconfig.org/) for consistent file conventions; 33 | ensure you have your editor configured to work with EditorConfig 34 | - This project follows the [semver](http://semver.org/) guidelines for versioning 35 | - This project uses a git pre-commit hook that will prevent commits if eslint finds errors 36 | 37 | ## Development Process 38 | 39 | **NOTE**: If this process seems like a lot, we plan to add some mock Journal data 40 | and better ways to test the code in the future. If you get lost at any point, just 41 | [open an issue](https://github.com/DVDAGames/elite-dangerous-journal-server/issues/new) 42 | and we'll help walk you through it. 43 | 44 | 1. Fork this repository and clone it locally 45 | 2. `cd` into your local repo 46 | 3. Run `npm install` 47 | 4. Make your changes and commit them to your forked repo 48 | 5. Open a new Terminal window and run the following command: 49 | ```shell 50 | node examples/client/discovery.js 51 | ``` 52 | 6. Open another Terminal window and run the following command: 53 | ```shell 54 | node examples/server/index.js 55 | ``` 56 | 7. Notice the successful communication between Terminal windows 57 | 8. Fire up *Elite Dangerous* and trigger some Journal events - entering and leaving the 58 | Galaxy Map fires `Soundtrack` events and can be done without ever leaving a station 59 | 9. Exit the client process; exit the server process 60 | 10. Run `npm run lint` to make sure there aren't any errors 61 | 11. Submit a [Pull Request](https://github.com/DVDAGames/elite-dangerous-journal-server/pulls) 62 | and follow our PR Template 63 | 12. Accept an Internet High Five from us for helping out 64 | 65 | ![](https://media.giphy.com/media/wrzf9P70YWLJK/giphy.gif) 66 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { homedir } = require('os'); 3 | 4 | /** 5 | * RegEx to match against the Journal filename conventions used by E:D 6 | * @type {RegEx} 7 | */ 8 | const JOURNAL_FILE_REGEX = /^Journal\.\d*?\.\d*?\.log$/; 9 | 10 | /** 11 | * Port to use for our WebSocket connections 12 | * @type {Number} 13 | */ 14 | const JOURNAL_SERVER_PORT = 31337; 15 | 16 | /** 17 | * Path that E:D saves Journal files to 18 | * @type {String} 19 | */ 20 | const JOURNAL_DIR = join(homedir(), 'Saved Games', 'Frontier Developments', 'Elite Dangerous'); 21 | 22 | /** 23 | * List of ancillary files to also include in the event broadcast 24 | * @type {Array} 25 | */ 26 | 27 | const ANCILLARY_FILES = ['Cargo.json', 'Market.json', 'ModulesInfo.json', 'Outfitting.json', 'Shipyard.json', 'Status.json']; 28 | 29 | /** 30 | * Default Journal Event subscription list for clients 31 | * @type {Array} 32 | */ 33 | const DEFAULT_EVENT_SUBSCRIPTIONS = ['ALL']; 34 | 35 | /** 36 | * Allow subscriptions to specific events? 37 | * @type {Boolean} 38 | */ 39 | const SERVER_ALLOW_SUBSCRIPTIONS = true; 40 | 41 | /** 42 | * Message Type String for registration from Client 43 | * @type {String} 44 | */ 45 | const SERVER_SUBSCRIPTION_MESSAGE_TYPE = 'subscribe'; 46 | 47 | /** 48 | * Zeroconf/Bonjour service name 49 | * @type {String} 50 | */ 51 | const JOURNAL_SERVER_SERVICE_NAME = 'Elite Dangerous Journal Server'; 52 | 53 | /** 54 | * Type of Zeroconf/Bonjour service we are publishing 55 | * @type {String} 56 | */ 57 | const JOURNAL_SERVER_SERVICE_TYPE = 'ws'; 58 | 59 | /** 60 | * Should Network Discovery be enabled 61 | * @type {Boolean} 62 | */ 63 | const SERVER_ALLOW_DISCOVERY = true; 64 | 65 | /** 66 | * Time to pass to watcher for polling interval 67 | * @type {Number} 68 | */ 69 | const JOURNAL_WATCH_INTERVAL = 100; 70 | 71 | /** 72 | * Interval to use for server heartbeat broadcast 73 | * @type {Number} 74 | * @description 1 minute 75 | */ 76 | const SERVER_HEARTBEAT_INTERVAL = 60000; 77 | 78 | /** 79 | * Should server allow clients to Register 80 | * @type {Boolean} 81 | */ 82 | const SERVER_ALLOW_REGISTRATION = false; 83 | 84 | /** 85 | * Should server force clients to Register 86 | * @type {Boolean} 87 | */ 88 | const SERVER_FORCE_REGISTRATION = false; 89 | 90 | /** 91 | * Message Type String for registration from Client 92 | * @type {String} 93 | */ 94 | const SERVER_REGISTRATION_MESSAGE_TYPE = 'register'; 95 | 96 | /** 97 | * Error message for client requiring registration 98 | * @type {String} 99 | */ 100 | const ERROR_MUST_REGISTER_MSG = 'Client must register with Server'; 101 | 102 | /** 103 | * Error code client requiring registration 104 | * @type {Number} 105 | */ 106 | const ERROR_MUST_REGISTER_CODE = 401; 107 | 108 | /** 109 | * Error message for invalid message types 110 | * @type {String} 111 | */ 112 | const ERROR_INVALID_MESSAGE_MSG = 'Server does not accept message type'; 113 | 114 | /** 115 | * Error code for invalid message types 116 | * @type {Number} 117 | */ 118 | const ERROR_INVALID_MESSAGE_CODE = 403; 119 | 120 | /** 121 | * Error message for invalid message types 122 | * @type {String} 123 | */ 124 | const ERROR_INVALID_PAYLOAD_MSG = 'Server does not accept payload'; 125 | 126 | /** 127 | * Error code for invalid message types 128 | * @type {Number} 129 | */ 130 | const ERROR_INVALID_PAYLOAD_CODE = 400; 131 | 132 | 133 | /** 134 | * Name of Journal Event that first gives us the CMDR name 135 | * @type {String} 136 | */ 137 | const EVENT_FOR_COMMANDER_NAME = 'LoadGame'; 138 | 139 | module.exports = { 140 | // non-configurable 141 | EVENT_FOR_COMMANDER_NAME, 142 | 143 | // errors 144 | ERROR_INVALID_MESSAGE_MSG, 145 | ERROR_INVALID_MESSAGE_CODE, 146 | ERROR_MUST_REGISTER_MSG, 147 | ERROR_MUST_REGISTER_CODE, 148 | ERROR_INVALID_PAYLOAD_MSG, 149 | ERROR_INVALID_PAYLOAD_CODE, 150 | 151 | // server 152 | JOURNAL_SERVER_PORT, 153 | 154 | // watcher 155 | JOURNAL_DIR, 156 | JOURNAL_FILE_REGEX, 157 | JOURNAL_WATCH_INTERVAL, 158 | ANCILLARY_FILES, 159 | 160 | // heartbeat 161 | SERVER_HEARTBEAT_INTERVAL, 162 | 163 | // discovery 164 | SERVER_ALLOW_DISCOVERY, 165 | JOURNAL_SERVER_SERVICE_NAME, 166 | JOURNAL_SERVER_SERVICE_TYPE, 167 | 168 | // subscriptions 169 | SERVER_ALLOW_SUBSCRIPTIONS, 170 | DEFAULT_EVENT_SUBSCRIPTIONS, 171 | SERVER_SUBSCRIPTION_MESSAGE_TYPE, 172 | 173 | // registration 174 | SERVER_ALLOW_REGISTRATION, 175 | SERVER_FORCE_REGISTRATION, 176 | SERVER_REGISTRATION_MESSAGE_TYPE, 177 | }; 178 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | Relevant new features, bugfixes, etc. for each version will be maintained here. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 7 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | *No unreleased changes* 12 | 13 | ## [3.2.0] - 2019-07-16 14 | ### Fixed 15 | - Added error handling for situations where the entire `status.json` file is not 16 | yet written to disk and we try to access it, which would result in a crash due to 17 | attempting to parse improperly formatted JSON. 18 | 19 | ## [3.1.0] - 2019-03-13 20 | ### Added 21 | - Added broadcasting of the contents of ancillary files (e.g. `status.json`) as events 22 | 23 | ### Housekeeping 24 | - Fixed several misspelled words throughout the project 25 | 26 | ## [3.0.1] - 2019-03-10 27 | ### Fixed 28 | - Ignore `ECONNRESET` error from WebSocket to prevent application crashing when 29 | it would normally just reconnect 30 | 31 | ## [3.0.0] - 2017-10-20 32 | ### Added 33 | - Added heartbeat pings between server and clients 34 | - Added ability for clients to register names 35 | - Added ability to force clients to register 36 | - Added ability to prevent subscriptions 37 | - Added ability to define custom default subscriptions 38 | - Added ability to configure error messages 39 | - Added error messages 40 | - Added some other directories and files to make things a bit easier to follow and less cluttered 41 | 42 | ### Changed 43 | - Reformatted config Object for constuctor 44 | - Updated `README.md` to reflect latest changes 45 | 46 | ### Fixed 47 | - Fixed issue with `lint` script looking at outdated, non-existant path 48 | 49 | ### Deprecated 50 | - Previous configuration format 51 | 52 | ## [2.5.1] - 2017-10-18 53 | ### Fixed 54 | - Fixed port `0` when running outside of test scripts 55 | - Actually using `httpServer` with `ws` correctly 56 | 57 | ### Notes 58 | 59 | This came up when testing launching a Journal Server inside of an Electron Application 60 | and things were failing without telling the http server to listen and then grabbing the 61 | port. 62 | 63 | ## [2.5.0] - 2017-10-18 64 | ### Added 65 | - Ability to use `0` for port to get randomly assigned port from OS 66 | 67 | ## [2.4.3] - 2017-10-17 68 | ### Fixed 69 | - Removed `process` declaration from `src/index.js` 70 | 71 | ## [2.4.2] - 2017-10-17 72 | ### Changed 73 | - Suppressed broadcast of Journal Events during initial indexing of Journal at startup 74 | 75 | ### Housekeeping 76 | - `.github` diretory for storing GitHub-specific files 77 | - `.github/issue_template.md` to help with bug reporting 78 | - `.github/pull_request_template.md` 79 | - `CODE_OF_CONDUCT.md` 80 | - `CONTRIBUTING.md` referenced Issue template and reduced Bug Reporting verbiage; 81 | added link to Issues page 82 | - Added `.github` to `.npmignore` so it isn't published 83 | - Added `CODE_OF_CONDUCT.md` to `.npmignore` 84 | - Reworded some of Contributing Guidelines 85 | - Added basic Journals to `examples/mocks` for testing 86 | 87 | ## [2.4.1] - 2017-10-16 88 | ### Added 89 | - Added `.npmignore` 90 | 91 | ### Changed 92 | - Incrementing patch version to republish with `.npmignore` update 93 | 94 | ## [2.4.0] - 2017-10-16 95 | ### Added 96 | - Ability to specify custom polling `interval` for Journal watcher 97 | 98 | ### Changed 99 | - Added `CHANGELOG.md` 100 | - Updated version in `package.json` and `README.md` 101 | 102 | ## [2.3.0] - 2017-10-16 103 | ### Added 104 | - Ability to specify custom `headers` Object to add to broadcasts 105 | - `pre-commit` package enforcing `npm run lint` before committing 106 | 107 | ### Changed 108 | - Fixed ESLint errors 109 | - Added `CONTRIBUTING.md` info 110 | - Changed server logs to say `"broadcast"` instead of `"emit"` 111 | 112 | ## [2.2.0] - 2017-10-16 113 | ### Added 114 | - Journal Server version number returned in broadcast headers 115 | - Service Name for Network Discovery is now configurable 116 | 117 | ### Changed 118 | - Better `README.md` documentation formatting 119 | - Better organization for `examples` directory 120 | 121 | ## [2.1.0] - 2017-10-15 122 | ### Added 123 | - Bonjour/Zeroconf for Network Discover of Journal Server 124 | - Graceful shutdown of Journal Server 125 | 126 | ## [2.0.1] - 2017-10-15 127 | ### Changed 128 | - Updated `README.md` with more accurate documentation 129 | 130 | ## [2.0.0] - 2017-10-14 131 | ### Added 132 | - Allow client to subscribe to specific Journal Events 133 | 134 | --- 135 | 136 | - [Unreleased](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/3.0.0...HEAD) 137 | - [3.0.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.5.1...3.0.0) 138 | - [2.5.1](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.5.0...2.5.1) 139 | - [2.4.3](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.4.3...2.5.0) 140 | - [2.4.3](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.4.2...2.4.3) 141 | - [2.4.2](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.4.1...2.4.2) 142 | - [2.4.1](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.4.0...2.4.1) 143 | - [2.4.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.3.0...2.4.0) 144 | - [2.3.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.2.0...2.3.0) 145 | - [2.2.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.1.0...2.2.0) 146 | - [2.1.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.0.1...2.1.0) 147 | - [2.0.1](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/2.0.0...2.0.1) 148 | - [2.0.0](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/1.0.2...2.0.0) 149 | - [1.0.2](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/1.0.1...1.0.2) 150 | - [1.0.1](https://github.com/DVDAGames/elite-dangerous-journal-server/compare/53322ee...1.0.1) 151 | -------------------------------------------------------------------------------- /examples/mocks/Journal.171017123456.01.log: -------------------------------------------------------------------------------- 1 | { "timestamp":"2017-10-15T04:34:03Z", "event":"Fileheader", "part":1, "language":"English\\UK", "gameversion":"2.4", "build":"r156431/r0 " } 2 | { "timestamp":"2017-10-15T04:34:04Z", "event":"Music", "MusicTrack":"NoTrack" } 3 | { "timestamp":"2017-10-15T04:34:04Z", "event":"Music", "MusicTrack":"MainMenu" } 4 | { "timestamp":"2017-10-15T04:34:40Z", "event":"Cargo", "Inventory":[ { "Name":"algae", "Count":1 } ] } 5 | { "timestamp":"2017-10-15T04:34:40Z", "event":"Loadout", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "Modules":[ { "Slot":"Armour", "Item":"CobraMkIII_Armour_Grade1", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"PowerPlant", "Item":"Int_Powerplant_Size4_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":59633 }, { "Slot":"MainEngines", "Item":"Int_Engine_Size4_Class1", "On":true, "Priority":0, "Health":1.000000, "Value":19878 }, { "Slot":"FrameShiftDrive", "Item":"Int_Hyperdrive_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"LifeSupport", "Item":"Int_LifeSupport_Size3_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":10133 }, { "Slot":"PowerDistributor", "Item":"Int_PowerDistributor_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"Radar", "Item":"Int_Sensors_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"FuelTank", "Item":"Int_FuelTank_Size4_Class3", "On":true, "Priority":1, "Health":1.000000, "Value":24734 }, { "Slot":"Slot01_Size4", "Item":"Int_CargoRack_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":34328 }, { "Slot":"Slot03_Size4", "Item":"Int_ShieldGenerator_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":19878 }, { "Slot":"Slot04_Size2", "Item":"Int_FuelScoop_Size2_Class5", "On":true, "Priority":1, "Health":1.000000, "Value":284844 }, { "Slot":"Slot05_Size2", "Item":"Int_StellarBodyDiscoveryScanner_Standard", "On":true, "Priority":1, "Health":1.000000, "Value":1000 }, { "Slot":"Slot06_Size2", "Item":"Int_Repairer_Size2_Class3", "On":true, "Priority":1, "AmmoInClip":2300, "Health":1.000000, "Value":162000 }, { "Slot":"PlanetaryApproachSuite", "Item":"Int_PlanetApproachSuite", "On":true, "Priority":1, "Health":1.000000, "Value":500 }, { "Slot":"ShipCockpit", "Item":"CobraMkIII_Cockpit", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"CargoHatch", "Item":"ModularCargoBayDoor", "On":true, "Priority":2, "Health":1.000000, "Value":0 } ] } 6 | { "timestamp":"2017-10-15T04:34:40Z", "event":"Materials", "Raw":[ ], "Manufactured":[ { "Name":"mechanicalscrap", "Count":4 }, { "Name":"chemicalmanipulators", "Count":3 }, { "Name":"fedproprietarycomposites", "Count":12 } ], "Encoded":[ { "Name":"shieldsoakanalysis", "Count":6 }, { "Name":"emissiondata", "Count":15 }, { "Name":"shieldpatternanalysis", "Count":9 }, { "Name":"shieldcyclerecordings", "Count":18 }, { "Name":"scandatabanks", "Count":3 }, { "Name":"shielddensityreports", "Count":12 }, { "Name":"decodedemissiondata", "Count":9 }, { "Name":"scrambledemissiondata", "Count":1 }, { "Name":"legacyfirmware", "Count":1 } ] } 7 | { "timestamp":"2017-10-15T04:34:40Z", "event":"LoadGame", "Commander":"JournalServer", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "FuelLevel":14.836306, "FuelCapacity":16.000000, "GameMode":"Open", "Credits":929382, "Loan":0 } 8 | { "timestamp":"2017-10-15T04:34:40Z", "event":"Rank", "Combat":1, "Trade":2, "Explore":1, "Empire":0, "Federation":0, "CQC":0 } 9 | { "timestamp":"2017-10-15T04:34:40Z", "event":"Progress", "Combat":81, "Trade":58, "Explore":2, "Empire":0, "Federation":100, "CQC":0 } 10 | { "timestamp":"2017-10-15T04:34:52Z", "event":"Location", "Docked":true, "StationName":"Tuan Orbital", "StationType":"Outpost", "StarSystem":"Potriti", "StarPos":[-39.625,-6.594,62.594], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Refinery;", "SystemEconomy_Localised":"Refinery", "SystemGovernment":"$government_Democracy;", "SystemGovernment_Localised":"Democracy", "SystemSecurity":"$SYSTEM_SECURITY_medium;", "SystemSecurity_Localised":"Medium Security", "Population":197263, "Body":"Tuan Orbital", "BodyType":"Station", "Factions":[ { "Name":"Potriti Expeditionary Institute", "FactionState":"None", "Government":"Cooperative", "Influence":0.161386, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Social Potriti Future", "FactionState":"None", "Government":"Democracy", "Influence":0.375248, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Bureau of Potriti", "FactionState":"CivilWar", "Government":"Dictatorship", "Influence":0.009901, "Allegiance":"Independent" }, { "Name":"Potriti Public Partners", "FactionState":"CivilWar", "Government":"Corporate", "Influence":0.038614, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Kremainn Corp.", "FactionState":"None", "Government":"Corporate", "Influence":0.201980, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"Lockdown", "Trend":0 } ], "RecoveringStates":[ { "State":"CivilUnrest", "Trend":0 } ] }, { "Name":"Potriti Gold Mafia", "FactionState":"CivilWar", "Government":"Anarchy", "Influence":0.016832, "Allegiance":"Independent", "PendingStates":[ { "State":"Bust", "Trend":0 }, { "State":"CivilUnrest", "Trend":1 } ] }, { "Name":"Potriti Flag", "FactionState":"CivilWar", "Government":"Dictatorship", "Influence":0.020792, "Allegiance":"Independent" }, { "Name":"The Order of Justice", "FactionState":"Boom", "Government":"Corporate", "Influence":0.175248, "Allegiance":"Independent" } ], "SystemFaction":"Social Potriti Future" } 11 | { "timestamp":"2017-10-15T04:34:53Z", "event":"Music", "MusicTrack":"NoTrack" } 12 | { "timestamp":"2017-10-15T04:34:53Z", "event":"Docked", "StationName":"Tuan Orbital", "StationType":"Outpost", "StarSystem":"Potriti", "StationFaction":"Social Potriti Future", "StationGovernment":"$government_Democracy;", "StationGovernment_Localised":"Democracy", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "BlackMarket", "Contacts", "Exploration", "Missions", "Rearm", "Refuel", "Repair", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Refinery;", "StationEconomy_Localised":"Refinery", "DistFromStarLS":278.495728 } 13 | { "timestamp":"2017-10-15T04:34:53Z", "event":"Music", "MusicTrack":"Exploration" } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Elite Dangerous Journal Server 3 | 4 | [![npm (scoped)](https://img.shields.io/npm/v/@dvdagames/elite-dangerous-journal-server.svg?style=flat-square)](https://www.npmjs.com/package/@dvdagames/elite-dangerous-journal-server) 5 | 6 | A simple WebSocket server for emiting *Elite Dangerous* Journal Events; it includes 7 | network discovery features so clients can easily find and connect to the server. 8 | 9 | The basic idea of this project is to watch changes to the Journal file as the 10 | player enjoys the game and emit every Journal update through a WebSocket to 11 | any connected clients that would like to listen for and react to these Journal 12 | updates. 13 | 14 | There are example clients and servers included in the repo's [examples](https://github.com/DVDAGames/elite-dangerous-journal-server/tree/master/examples) 15 | directory. 16 | 17 | Currently, the server only allows the client to subscribe to specific events and 18 | does not perform any action with other data sent by clients. In future iterations 19 | clients should be able to retrieve specific events from the Journal, all past events, 20 | etc. via a simple message to the Journal Server similar to the subscription message. 21 | 22 | Clients can subscribe to any of the Journal Events described in the 23 | [Journal Manual](https://forums.frontier.co.uk/showthread.php/275151-Commanders-log-manual-and-data-sample) 24 | by passing the desired event names as an Array to the server. There is an example below. 25 | 26 | 27 | ## Projects 28 | 29 | - [ED Tightbeam](https://github.com/DVDAGames/ed-tightbeam) is a work-in-progress Electron application to host a 30 | Journal Server so that users don't have to install Node or run scripts in a Terminal. 31 | 32 | If you're building something with `@dvdagames/elite-dangerous-journal-server` please let us know or submit a 33 | [Pull Request](https://github.com/DVDAGames/elite-dangerous-journal-server/pulls) adding your project to this list. 34 | 35 | 36 | ## Contributors 37 | 38 | Check out the [guide to contributing](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/CONTRIBUTING.md) 39 | if you'd like to be a [contributor](https://github.com/DVDAGames/elite-dangerous-journal-server/graphs/contributors). 40 | 41 | 42 | ## Changelog 43 | 44 | Check out the [CHANGELOG](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/CHANGELOG.md) for details. 45 | 46 | 47 | ## Usage 48 | 49 | ### Getting Started 50 | 51 | ```shell 52 | npm install --save @dvdagames/elite-dangerous-journal-server 53 | ``` 54 | 55 | ### Server 56 | 57 | The server Class does not require any parameters, but can optionally accept a single parameter. 58 | 59 | This parameter can take on two different forms: 60 | 1. a single port `Number` 61 | 2. a configuration `Object` 62 | 63 | #### Configuration Object Properties 64 | 65 | - **port**: `[Number]: 31337` listen for socket connections on a specific port; using a `0` will use `http`'s random port assignment 66 | - **id**: `[String]: uuid()` unique identifier for this Journal Server 67 | - **watcher**: `[Object]` config for file watcher 68 | - **path**: `[String]: "~/Saved Games/Frontier Developments/Elite Dangerous/"` path to *Elite Dangerous* Journal directory 69 | - **interval**: `[Number]: 100` what interval (in `ms`) should our watcher use for polling for Journal updates 70 | - **fileRegEx**: `[RegEx]: /^Journal\.\d*?\.\d*?\.log$/` what format should Journal filenames have 71 | - **discovery**: `[Object]` config for Bonjour/Zeroconf Network Discovery 72 | - **enabled**: `[Boolean]: true` should network discovery be enabled 73 | - **serviceName**: `[String]: "Elite Dangerous Journal Server"` name for network discovery service 74 | - **serviceType**: `[String]: ws` type of service to publish 75 | - **heartbeat**: `[Object]` config for heartbeat pings 76 | - **interval**: `[Number]: 30000` what interval (in `ms`) between heartbeat pings 77 | - **registration**: `[Object]` config for client registration settings 78 | - **enabled**: `[Boolean]: false` should registration be enabled; this just means 79 | that clients have to provide a name that can be used to refer to the client instead of only having the UUID we generate for each client 80 | - **force**: `[Boolean]: false` should clients have to register before receiving any data; this supresses all Journal Server headers in message responses and broadcasts until the client has registered 81 | - **messageType** `[String]: "register"` what should the client use for the `type` in their message when registering 82 | - **subscriptions**: `[Object]` config for client subscriptions settings 83 | - **enabled**: `[Boolean]: true` should client be able to choose what Broadcasts to receive 84 | - **subscribeTo**: `[Array]: ["ALL"]` what events to subscribe clients to by default; an empty Array (`[]`) will suppress all broadcasts unless subscribed to directly 85 | - **messageType**: `[String]: "subscribe"` what should the client use for the `type` in their message when updating subscriptions 86 | - **errors**: `[Object]` config for error messages to client; each error is an `Object` with a `message` and status `code` property 87 | - **mustRegister**: `[Object]` config for registration required error 88 | - **message**: `[String]: "Client must register with server"` 89 | - **code**: `[Number]: 401` 90 | - **invalidMessage**: `[Object]` config for invalid message type error 91 | - **message**: `[String]: "Server does not accept message type"` 92 | - **code**: `[Number]: 403` 93 | - **invalidPayload**: `[Object]` config for invalid payload error 94 | - **message**: `[String]: "Server does not accept payload"` 95 | - **code**: `[Number]: 400` 96 | - **headers**: `[Object]` an optional Object of arbitraty headers you'd like added to the broadcast; 97 | these properties will exist in the broadcast data outside of the `payload` property which 98 | will contain the Journal Event 99 | 100 | 101 | **NOTE**: If the `headers` property contains any of the default header properties 102 | that the Journal Server already plans to send, those headers will be overwritten by 103 | the default headers. 104 | 105 | 106 | #### Basic Server Example 107 | 108 | ```javascript 109 | const EliteDangerousJournalServer = require('@dvdagames/elite-dangerous-journal-server'); 110 | 111 | const JournalServer = new EliteDangerousJournalServer(); 112 | 113 | JournalServer.init(); 114 | ``` 115 | 116 | 117 | #### Custom Port 118 | 119 | ```javascript 120 | const EliteDangerousJournalServer = require('@dvdagames/elite-dangerous-journal-server'); 121 | 122 | const port = 12345; 123 | 124 | const JournalServer = new EliteDangerousJournalServer(port); 125 | 126 | JournalServer.init(); 127 | ``` 128 | 129 | 130 | #### Custom Config 131 | 132 | ```javascript 133 | const EliteDangerousJournalServer = require('@dvdagames/elite-dangerous-journal-server'); 134 | 135 | const port = 12345; 136 | const id = 'MY_UNIQUE_EDJS_ID'; 137 | const serviceName = 'My EDJS Instance'; 138 | const headers = { 139 | TEST: true 140 | }; 141 | 142 | const config = { 143 | port, 144 | id, 145 | discovery: { 146 | serviceName, 147 | }, 148 | headers, 149 | }; 150 | 151 | const JournalServer = new EliteDangerousJournalServer(config); 152 | 153 | JournalServer.init(); 154 | ``` 155 | 156 | 157 | ### Client 158 | 159 | Each connected client will listen to all events by default, but clients can choose 160 | which Journal Events the Journal Server will broadcast to them. 161 | 162 | The Journal Server broadcast will have the following data: 163 | 164 | #### Headers 165 | 166 | - **journalServer**: `[String]` the UUID of the Journal Server that sent the message 167 | - **serverVersion**: `[String]` the version number of the currently running Journal 168 | Server package 169 | - **journal**: `[String]` the name of the Journal file that is currently being used 170 | - **clientID**: `[String]` the UUID the Journal Server has assigned to this client 171 | - **clientName** `[String]` the name the client registered with; `null` until registered 172 | - **subscribedTo**: `[Array]` the events that this client is subscribed to 173 | - **commander**: `[String]` the currently loaded CMDR name; `null` until `LoadGame` 174 | event 175 | 176 | **NOTE**: If `registration.force` is enabled, no headers will be present in the broadcast 177 | until the client has registered. 178 | 179 | #### Payload 180 | 181 | - **payload**: `[Object]` the Journal Event that was triggered or the message from 182 | the Journal Server 183 | - The `payload` property will be the current Journal's header when clients's register 184 | or update subscriptions. 185 | - The `payload` property will be an Error Object like the following when there is an issue: 186 | ```json 187 | { 188 | "payload": { 189 | "error": true, 190 | "message": "Client must register with Server", 191 | "code": 401 192 | } 193 | } 194 | ``` 195 | - The `payload` property should contain the JSON-lines Object for the current Journal 196 | Event in all other cases 197 | 198 | 199 | #### Basic Example 200 | 201 | ```javascript 202 | const WebSocket = require('ws'); 203 | 204 | const socket = new WebSocket('ws://localhost:31337'); 205 | 206 | socket.on('message', (data) => { 207 | const broadcast = JSON.parse(data); 208 | 209 | const { payload } = broadcast; 210 | 211 | if (payload.event === 'Fileheader') { 212 | console.log(`${payload.timestamp} part ${payload.part}`); 213 | } else { 214 | console.log(`${payload.event} triggered`); 215 | } 216 | }); 217 | ``` 218 | 219 | 220 | #### Subscribing to Specific Journal Events 221 | 222 | ```javascript 223 | const WebSocket = require('ws'); 224 | 225 | const socket = new WebSocket('ws://localhost:31337'); 226 | 227 | socket.on('open', () => { 228 | const type = 'subscribe'; 229 | const payload = ['DockingRequested', 'DockingGranted', 'Docked', 'Undocked']; 230 | 231 | // only subscribe to Docking Events 232 | ws.send(JSON.stringify({ type, payload })); 233 | }); 234 | 235 | socket.on('message', (data) => { 236 | const broadcast = JSON.parse(data); 237 | 238 | const { payload } = broadcast; 239 | 240 | console.log(`Received: ${payload.event}`); 241 | }); 242 | ``` 243 | 244 | 245 | #### Network Discovery 246 | 247 | This is more advanced topic, please see the [discovery.js example](https://github.com/DVDAGames/elite-dangerous-journal-server/blob/master/examples/client/discovery.js) 248 | for more information on utilizing Network Discovery in your client. 249 | 250 | 251 | ## Acknowledgements 252 | 253 | - *Elite Dangerous* is © [Frontier Developments plc](https://www.frontier.co.uk/). 254 | - [Elite Dangerous Community Developers](https://edcd.github.io/) for documentation 255 | and discussions 256 | - [CMDR willyb321](https://github.com/willyb321) for direction on a few different issues, 257 | including `fs.watch()` issues with the Journals and using Bonjour for Network Discovery 258 | - [Frontier Forums Elite Dangerous Journal Discussion](https://forums.frontier.co.uk/showthread.php/275151-Commanders-log-manual-and-data-sample) 259 | for providing some info about what's in these files and how they work 260 | - [CMDR rolandbosa](https://github.com/rolandbosa) for adding some necessary new 261 | functionality long after I was no longer able to play the game 262 | -------------------------------------------------------------------------------- /examples/mocks/Journal.171016003403.02.log: -------------------------------------------------------------------------------- 1 | { "timestamp":"2017-10-14T18:39:51Z", "event":"Fileheader", "part":1, "language":"English\\UK", "gameversion":"2.4", "build":"r156431/r0 " } 2 | { "timestamp":"2017-10-14T18:39:51Z", "event":"Music", "MusicTrack":"NoTrack" } 3 | { "timestamp":"2017-10-14T18:39:51Z", "event":"Music", "MusicTrack":"MainMenu" } 4 | { "timestamp":"2017-10-14T18:41:37Z", "event":"Cargo", "Inventory":[ ] } 5 | { "timestamp":"2017-10-14T18:41:37Z", "event":"Loadout", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "Modules":[ { "Slot":"Armour", "Item":"CobraMkIII_Armour_Grade1", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"PowerPlant", "Item":"Int_Powerplant_Size4_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":59633 }, { "Slot":"MainEngines", "Item":"Int_Engine_Size4_Class1", "On":true, "Priority":0, "Health":1.000000, "Value":19878 }, { "Slot":"FrameShiftDrive", "Item":"Int_Hyperdrive_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"LifeSupport", "Item":"Int_LifeSupport_Size3_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":10133 }, { "Slot":"PowerDistributor", "Item":"Int_PowerDistributor_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"Radar", "Item":"Int_Sensors_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"FuelTank", "Item":"Int_FuelTank_Size4_Class3", "On":true, "Priority":1, "Health":1.000000, "Value":24734 }, { "Slot":"Slot01_Size4", "Item":"Int_CargoRack_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":34328 }, { "Slot":"Slot03_Size4", "Item":"Int_ShieldGenerator_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":19878 }, { "Slot":"Slot04_Size2", "Item":"Int_FuelScoop_Size2_Class5", "On":true, "Priority":1, "Health":1.000000, "Value":284844 }, { "Slot":"Slot05_Size2", "Item":"Int_StellarBodyDiscoveryScanner_Standard", "On":true, "Priority":1, "Health":1.000000, "Value":1000 }, { "Slot":"Slot06_Size2", "Item":"Int_Repairer_Size2_Class3", "On":true, "Priority":1, "AmmoInClip":2300, "Health":1.000000, "Value":162000 }, { "Slot":"PlanetaryApproachSuite", "Item":"Int_PlanetApproachSuite", "On":true, "Priority":1, "Health":1.000000, "Value":500 }, { "Slot":"ShipCockpit", "Item":"CobraMkIII_Cockpit", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"CargoHatch", "Item":"ModularCargoBayDoor", "On":true, "Priority":2, "Health":1.000000, "Value":0 } ] } 6 | { "timestamp":"2017-10-14T18:41:37Z", "event":"Materials", "Raw":[ ], "Manufactured":[ { "Name":"mechanicalscrap", "Count":4 }, { "Name":"chemicalmanipulators", "Count":3 }, { "Name":"fedproprietarycomposites", "Count":12 } ], "Encoded":[ { "Name":"shieldsoakanalysis", "Count":6 }, { "Name":"emissiondata", "Count":15 }, { "Name":"shieldpatternanalysis", "Count":9 }, { "Name":"shieldcyclerecordings", "Count":18 }, { "Name":"scandatabanks", "Count":3 }, { "Name":"shielddensityreports", "Count":12 }, { "Name":"decodedemissiondata", "Count":9 }, { "Name":"scrambledemissiondata", "Count":1 }, { "Name":"legacyfirmware", "Count":1 } ] } 7 | { "timestamp":"2017-10-14T18:41:37Z", "event":"LoadGame", "Commander":"JournalServer", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "FuelLevel":16.000000, "FuelCapacity":16.000000, "GameMode":"Open", "Credits":766731, "Loan":0 } 8 | { "timestamp":"2017-10-14T18:41:37Z", "event":"Rank", "Combat":1, "Trade":2, "Explore":0, "Empire":0, "Federation":0, "CQC":0 } 9 | { "timestamp":"2017-10-14T18:41:37Z", "event":"Progress", "Combat":81, "Trade":35, "Explore":42, "Empire":0, "Federation":100, "CQC":0 } 10 | { "timestamp":"2017-10-14T18:42:13Z", "event":"Location", "Docked":true, "StationName":"Cleve Hub", "StationType":"Orbis", "StarSystem":"Eravate", "StarPos":[-42.438,-3.156,59.656], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Agri;", "SystemEconomy_Localised":"Agriculture", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_high;", "SystemSecurity_Localised":"High Security", "Population":740380179, "Body":"Cleve Hub", "BodyType":"Station", "Factions":[ { "Name":"Eravate School of Commerce", "FactionState":"None", "Government":"Cooperative", "Influence":0.109109, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Independent Eravate Free", "FactionState":"Boom", "Government":"Democracy", "Influence":0.122122, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Eravate Network", "FactionState":"Boom", "Government":"Corporate", "Influence":0.151151, "Allegiance":"Federation", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Traditional Eravate Autocracy", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.079079, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Eravate Life Services", "FactionState":"Boom", "Government":"Corporate", "Influence":0.098098, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Official Eravate Flag", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.069069, "Allegiance":"Independent", "RecoveringStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Adle's Armada", "FactionState":"Boom", "Government":"Corporate", "Influence":0.371371, "Allegiance":"Federation" } ], "SystemFaction":"Adle's Armada", "FactionState":"Boom" } 11 | { "timestamp":"2017-10-14T18:42:14Z", "event":"Music", "MusicTrack":"NoTrack" } 12 | { "timestamp":"2017-10-14T18:42:15Z", "event":"Docked", "StationName":"Cleve Hub", "StationType":"Orbis", "StarSystem":"Eravate", "StationFaction":"Adle's Armada", "FactionState":"Boom", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Industrial;", "StationEconomy_Localised":"Industrial", "DistFromStarLS":37.236580 } 13 | { "timestamp":"2017-10-14T18:42:15Z", "event":"Music", "MusicTrack":"Starport" } 14 | { "timestamp":"2017-10-14T18:50:04Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Stephen Griggs;", "From_Localised":"Stephen Griggs", "Message":"$Commuter_HostileScan04;", "Message_Localised":"This is not the trading ship you are looking for.", "Channel":"npc" } 15 | { "timestamp":"2017-10-14T18:52:47Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Mij;", "From_Localised":"Mij", "Message":"$Commuter_HostileScan01;", "Message_Localised":"Please cease your unauthorised scan immediately.", "Channel":"npc" } 16 | { "timestamp":"2017-10-14T18:53:01Z", "event":"ReceiveText", "From":"$ShipName_Police_Federation;", "From_Localised":"Federal Security Service", "Message":"$OverwatchCriticalDamage09;", "Message_Localised":"My friends will avenge me!", "Channel":"npc" } 17 | { "timestamp":"2017-10-14T18:53:28Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Jimmyb;", "From_Localised":"Jimmyb", "Message":"$Commuter_HostileScan04;", "Message_Localised":"This is not the trading ship you are looking for.", "Channel":"npc" } 18 | { "timestamp":"2017-10-14T18:56:31Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Crimpton;", "From_Localised":"Crimpton", "Message":"$Commuter_HostileScan05;", "Message_Localised":"Your ship's ID has been logged, any active hostilities will result in criminal charges being brought against you.", "Channel":"npc" } 19 | { "timestamp":"2017-10-14T18:58:56Z", "event":"Music", "MusicTrack":"Exploration" } 20 | { "timestamp":"2017-10-14T18:58:58Z", "event":"Music", "MusicTrack":"MainMenu" } 21 | { "timestamp":"2017-10-14T19:00:56Z", "event":"Cargo", "Inventory":[ ] } 22 | { "timestamp":"2017-10-14T19:00:56Z", "event":"Loadout", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "Modules":[ { "Slot":"Armour", "Item":"CobraMkIII_Armour_Grade1", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"PowerPlant", "Item":"Int_Powerplant_Size4_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":59633 }, { "Slot":"MainEngines", "Item":"Int_Engine_Size4_Class1", "On":true, "Priority":0, "Health":1.000000, "Value":19878 }, { "Slot":"FrameShiftDrive", "Item":"Int_Hyperdrive_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"LifeSupport", "Item":"Int_LifeSupport_Size3_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":10133 }, { "Slot":"PowerDistributor", "Item":"Int_PowerDistributor_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"Radar", "Item":"Int_Sensors_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"FuelTank", "Item":"Int_FuelTank_Size4_Class3", "On":true, "Priority":1, "Health":1.000000, "Value":24734 }, { "Slot":"Slot01_Size4", "Item":"Int_CargoRack_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":34328 }, { "Slot":"Slot03_Size4", "Item":"Int_ShieldGenerator_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":19878 }, { "Slot":"Slot04_Size2", "Item":"Int_FuelScoop_Size2_Class5", "On":true, "Priority":1, "Health":1.000000, "Value":284844 }, { "Slot":"Slot05_Size2", "Item":"Int_StellarBodyDiscoveryScanner_Standard", "On":true, "Priority":1, "Health":1.000000, "Value":1000 }, { "Slot":"Slot06_Size2", "Item":"Int_Repairer_Size2_Class3", "On":true, "Priority":1, "AmmoInClip":2300, "Health":1.000000, "Value":162000 }, { "Slot":"PlanetaryApproachSuite", "Item":"Int_PlanetApproachSuite", "On":true, "Priority":1, "Health":1.000000, "Value":500 }, { "Slot":"ShipCockpit", "Item":"CobraMkIII_Cockpit", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"CargoHatch", "Item":"ModularCargoBayDoor", "On":true, "Priority":2, "Health":1.000000, "Value":0 } ] } 23 | { "timestamp":"2017-10-14T19:00:56Z", "event":"Materials", "Raw":[ ], "Manufactured":[ { "Name":"mechanicalscrap", "Count":4 }, { "Name":"chemicalmanipulators", "Count":3 }, { "Name":"fedproprietarycomposites", "Count":12 } ], "Encoded":[ { "Name":"shieldsoakanalysis", "Count":6 }, { "Name":"emissiondata", "Count":15 }, { "Name":"shieldpatternanalysis", "Count":9 }, { "Name":"shieldcyclerecordings", "Count":18 }, { "Name":"scandatabanks", "Count":3 }, { "Name":"shielddensityreports", "Count":12 }, { "Name":"decodedemissiondata", "Count":9 }, { "Name":"scrambledemissiondata", "Count":1 }, { "Name":"legacyfirmware", "Count":1 } ] } 24 | { "timestamp":"2017-10-14T19:00:56Z", "event":"LoadGame", "Commander":"JournalServer", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "FuelLevel":16.000000, "FuelCapacity":16.000000, "GameMode":"Open", "Credits":766731, "Loan":0 } 25 | { "timestamp":"2017-10-14T19:00:56Z", "event":"Rank", "Combat":1, "Trade":2, "Explore":0, "Empire":0, "Federation":0, "CQC":0 } 26 | { "timestamp":"2017-10-14T19:00:56Z", "event":"Progress", "Combat":81, "Trade":35, "Explore":42, "Empire":0, "Federation":100, "CQC":0 } 27 | { "timestamp":"2017-10-14T19:01:03Z", "event":"Location", "Docked":true, "StationName":"Cleve Hub", "StationType":"Orbis", "StarSystem":"Eravate", "StarPos":[-42.438,-3.156,59.656], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Agri;", "SystemEconomy_Localised":"Agriculture", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_high;", "SystemSecurity_Localised":"High Security", "Population":740380179, "Body":"Cleve Hub", "BodyType":"Station", "Factions":[ { "Name":"Eravate School of Commerce", "FactionState":"None", "Government":"Cooperative", "Influence":0.109109, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Independent Eravate Free", "FactionState":"Boom", "Government":"Democracy", "Influence":0.122122, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Eravate Network", "FactionState":"Boom", "Government":"Corporate", "Influence":0.151151, "Allegiance":"Federation", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Traditional Eravate Autocracy", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.079079, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Eravate Life Services", "FactionState":"Boom", "Government":"Corporate", "Influence":0.098098, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Official Eravate Flag", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.069069, "Allegiance":"Independent", "RecoveringStates":[ { "State":"Outbreak", "Trend":1 } ] }, { "Name":"Adle's Armada", "FactionState":"Boom", "Government":"Corporate", "Influence":0.371371, "Allegiance":"Federation" } ], "SystemFaction":"Adle's Armada", "FactionState":"Boom" } 28 | { "timestamp":"2017-10-14T19:01:03Z", "event":"Music", "MusicTrack":"NoTrack" } 29 | { "timestamp":"2017-10-14T19:01:03Z", "event":"Docked", "StationName":"Cleve Hub", "StationType":"Orbis", "StarSystem":"Eravate", "StationFaction":"Adle's Armada", "FactionState":"Boom", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Industrial;", "StationEconomy_Localised":"Industrial", "DistFromStarLS":37.232658 } 30 | { "timestamp":"2017-10-14T19:01:03Z", "event":"Music", "MusicTrack":"Starport" } 31 | { "timestamp":"2017-10-14T19:12:59Z", "event":"Music", "MusicTrack":"GalaxyMap" } 32 | { "timestamp":"2017-10-14T19:13:41Z", "event":"Music", "MusicTrack":"Starport" } 33 | { "timestamp":"2017-10-14T19:18:10Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Luannei;", "From_Localised":"Luannei", "Message":"$Commuter_HostileScan01;", "Message_Localised":"Please cease your unauthorised scan immediately.", "Channel":"npc" } 34 | { "timestamp":"2017-10-14T19:19:41Z", "event":"Music", "MusicTrack":"GalaxyMap" } 35 | { "timestamp":"2017-10-14T19:20:26Z", "event":"Music", "MusicTrack":"Starport" } 36 | { "timestamp":"2017-10-14T19:24:11Z", "event":"Undocked", "StationName":"Cleve Hub", "StationType":"Orbis" } 37 | { "timestamp":"2017-10-14T19:24:55Z", "event":"Music", "MusicTrack":"NoTrack" } 38 | { "timestamp":"2017-10-14T19:25:41Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Potriti", "StarClass":"F" } 39 | { "timestamp":"2017-10-14T19:25:42Z", "event":"ReceiveText", "From":"Cleve Hub", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 40 | { "timestamp":"2017-10-14T19:26:00Z", "event":"FSDJump", "StarSystem":"Potriti", "StarPos":[-39.625,-6.594,62.594], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Refinery;", "SystemEconomy_Localised":"Refinery", "SystemGovernment":"$government_Democracy;", "SystemGovernment_Localised":"Democracy", "SystemSecurity":"$SYSTEM_SECURITY_medium;", "SystemSecurity_Localised":"Medium Security", "Population":197263, "JumpDist":5.325, "FuelUsed":0.231761, "FuelLevel":15.768239, "Factions":[ { "Name":"Potriti Expeditionary Institute", "FactionState":"None", "Government":"Cooperative", "Influence":0.161386, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Social Potriti Future", "FactionState":"None", "Government":"Democracy", "Influence":0.375248, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Bureau of Potriti", "FactionState":"CivilWar", "Government":"Dictatorship", "Influence":0.009901, "Allegiance":"Independent" }, { "Name":"Potriti Public Partners", "FactionState":"CivilWar", "Government":"Corporate", "Influence":0.038614, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Kremainn Corp.", "FactionState":"None", "Government":"Corporate", "Influence":0.201980, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"Lockdown", "Trend":0 } ], "RecoveringStates":[ { "State":"CivilUnrest", "Trend":0 } ] }, { "Name":"Potriti Gold Mafia", "FactionState":"CivilWar", "Government":"Anarchy", "Influence":0.016832, "Allegiance":"Independent", "PendingStates":[ { "State":"Bust", "Trend":0 }, { "State":"CivilUnrest", "Trend":1 } ] }, { "Name":"Potriti Flag", "FactionState":"CivilWar", "Government":"Dictatorship", "Influence":0.020792, "Allegiance":"Independent" }, { "Name":"The Order of Justice", "FactionState":"Boom", "Government":"Corporate", "Influence":0.175248, "Allegiance":"Independent" } ], "SystemFaction":"Social Potriti Future" } 41 | { "timestamp":"2017-10-14T19:26:00Z", "event":"Music", "MusicTrack":"DestinationFromHyperspace" } 42 | { "timestamp":"2017-10-14T19:26:05Z", "event":"Music", "MusicTrack":"Supercruise" } 43 | { "timestamp":"2017-10-14T19:26:12Z", "event":"FuelScoop", "Scooped":0.061763, "Total":15.830002 } 44 | { "timestamp":"2017-10-14T19:28:03Z", "event":"SupercruiseExit", "StarSystem":"Potriti", "Body":"Tuan Orbital", "BodyType":"Station" } 45 | { "timestamp":"2017-10-14T19:28:03Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 46 | { "timestamp":"2017-10-14T19:28:08Z", "event":"Music", "MusicTrack":"Exploration" } 47 | { "timestamp":"2017-10-14T19:28:11Z", "event":"ReceiveText", "From":"Tuan Orbital", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 48 | { "timestamp":"2017-10-14T19:28:27Z", "event":"DockingRequested", "StationName":"Tuan Orbital" } 49 | { "timestamp":"2017-10-14T19:28:27Z", "event":"ReceiveText", "From":"Tuan Orbital", "Message":"$DockingChatter_Neutral;", "Message_Localised":"Ensure to observe starport protocol during your visit, pilot.", "Channel":"npc" } 50 | { "timestamp":"2017-10-14T19:28:29Z", "event":"ReceiveText", "From":"Tuan Orbital", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 51 | { "timestamp":"2017-10-14T19:28:29Z", "event":"DockingGranted", "LandingPad":2, "StationName":"Tuan Orbital" } 52 | { "timestamp":"2017-10-14T19:31:14Z", "event":"Docked", "StationName":"Tuan Orbital", "StationType":"Outpost", "StarSystem":"Potriti", "StationFaction":"Social Potriti Future", "StationGovernment":"$government_Democracy;", "StationGovernment_Localised":"Democracy", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "BlackMarket", "Contacts", "Exploration", "Missions", "Rearm", "Refuel", "Repair", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Refinery;", "StationEconomy_Localised":"Refinery", "DistFromStarLS":278.538788 } 53 | { "timestamp":"2017-10-14T20:55:11Z", "event":"Music", "MusicTrack":"GalaxyMap" } 54 | { "timestamp":"2017-10-14T20:58:21Z", "event":"Music", "MusicTrack":"Exploration" } 55 | { "timestamp":"2017-10-14T20:59:10Z", "event":"Music", "MusicTrack":"GalaxyMap" } 56 | { "timestamp":"2017-10-14T21:00:10Z", "event":"Music", "MusicTrack":"Exploration" } 57 | { "timestamp":"2017-10-14T21:00:50Z", "event":"Music", "MusicTrack":"GalaxyMap" } 58 | { "timestamp":"2017-10-14T21:01:34Z", "event":"Music", "MusicTrack":"Exploration" } 59 | { "timestamp":"2017-10-14T21:04:00Z", "event":"Music", "MusicTrack":"GalaxyMap" } 60 | { "timestamp":"2017-10-14T21:10:21Z", "event":"Music", "MusicTrack":"Exploration" } 61 | { "timestamp":"2017-10-14T21:26:51Z", "event":"Music", "MusicTrack":"GalaxyMap" } 62 | { "timestamp":"2017-10-14T21:26:54Z", "event":"Music", "MusicTrack":"Exploration" } 63 | { "timestamp":"2017-10-14T22:26:36Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Marcus Andersson;", "From_Localised":"Marcus Andersson", "Message":"$Commuter_HostileScan02;", "Message_Localised":"It is an offence to scan this vessel unless you are an authorised member of system security.", "Channel":"npc" } 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | // Node imports 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const { createServer } = require('http'); 7 | 8 | // package.json for version number and such 9 | const packageJSON = require('../package.json'); 10 | 11 | // npm module imports 12 | const WebSocket = require('ws'); 13 | const zeroconf = require('bonjour')(); 14 | const dir = require('node-dir'); 15 | const chokidar = require('chokidar'); 16 | const moment = require('moment'); 17 | const chalk = require('chalk'); 18 | const uuid = require('uuid/v1'); 19 | const { merge, omit, isNumber } = require('lodash'); 20 | 21 | // utilities 22 | const { formatClientName } = require('./utilities'); 23 | 24 | // configuration 25 | const CONFIG = require('./defaults'); 26 | const { EVENT_FOR_COMMANDER_NAME, ANCILLARY_FILES } = require('./constants'); 27 | 28 | 29 | /** 30 | * Creates a WebSocket server that broadcasts Elite Dangerous Journal Events 31 | * @class EliteDangerousJournalServer 32 | */ 33 | class EliteDangerousJournalServer { 34 | /** 35 | * Creates an instance of EliteDangerousJournalServer. 36 | * @param {Object} [config={}] Journal Server configuration 37 | * @param {Number} [config.port] port to use for sockets 38 | * @param {String} [config.id] unique id for Journal Server 39 | * @param {Object} [config.watcher] watcher configuration 40 | * @param {String} [config.watcher.path] path to watch; should be Journal directory 41 | * @param {Number} [config.watcher.interval] time for polling Journal Directory 42 | * @param {RegEx} [config.watcher.fileRegEx] RegEx for Journal file names 43 | * @param {Object} [config.discovery] network discovery configuration 44 | * @param {Boolean} [config.discovery.enabled] enable/disable network discovery 45 | * @param {String} [config.discovery.serviceName] name for Bonjour service 46 | * @param {String} [config.discovery.serviceType] type of service to broadcast 47 | * @param {Object} [config.heartbeat] Object for configuring WebSocket heartbeat 48 | * @param {Number} [config.heartbeat.interval] time between heartbeat pings 49 | * @param {Object} [config.registration] Object for configuring client registration 50 | * @param {Boolean} [config.registration.enabled] enable/disable registration 51 | * @param {Boolean} [config.registration.force] force clients to register 52 | * @param {String} [config.registration.messageType] what type of message should the 53 | * client send to register 54 | * @param {Object} [config.subscriptions] Object for configuring client subscriptions 55 | * @param {Boolean} [config.subscriptions.enabled] enable/disable subscriptions 56 | * @param {Array} [config.subscriptions.subscribeTo] Array of default events to subscribe to 57 | * @param {String} [config.subscriptions.messageType] what type of message should the 58 | * client send to update their subscriptions 59 | * @param {Object} [config.errors] Object for configuring error responses 60 | * @param {Object} [config.errors.mustRegister] Object for configuring registration error 61 | * @param {String} [config.errors.mustRegister.message] error message for registration 62 | * @param {String} [config.errors.mustRegister.code] error code to send 63 | * @param {Object} [config.errors.invaldMessage] Object for configuring messaging error 64 | * @param {String} [config.errors.invaldMessage.message] error message for messaging 65 | * @param {String} [config.errors.invaldMessage.code] error code to send 66 | * @param {Object} [config.errors.invaldPayload] Object for configuring payload error 67 | * @param {String} [config.errors.invaldPayload.message] error message for payload 68 | * @param {String} [config.errors.invaldPayload.code] error code to send 69 | * @param {Object} [config.headers] custom headers to merge with our broadcast headers 70 | * @memberof EliteDangerousJournalServer 71 | */ 72 | constructor(config = {}) { 73 | // if only a port was provided, create an Object we can use to merge 74 | const mergeConfig = (isNumber(config)) ? { port: config } : config; 75 | 76 | // merge provided config with default config 77 | this.config = merge({}, CONFIG, mergeConfig); 78 | 79 | // if no id was provided, generate one 80 | if (!this.config.id) { 81 | this.config.id = uuid(); 82 | } 83 | 84 | // initialize class properties 85 | this.currentLine = 0; 86 | this.journals = []; 87 | this.entries = []; 88 | this.currentHeader = {}; 89 | this.clientSubscriptions = {}; 90 | this.commander = null; 91 | this.validMessageTypes = []; 92 | 93 | const { subscriptions, registration } = this.config; 94 | 95 | if (registration.force) { 96 | this.config.registration.enabled = true; 97 | } 98 | 99 | if (subscriptions.enabled) { 100 | this.validMessageTypes.push(subscriptions.messageType); 101 | } 102 | 103 | if (registration.enabled) { 104 | this.validMessageTypes.push(registration.messageType); 105 | } 106 | 107 | this.ancillaryFiles = {}; 108 | ANCILLARY_FILES.forEach((filename) => { 109 | const { base, name } = path.parse(filename); 110 | this.ancillaryFiles[base.toLowerCase()] = { event: `X-${name}` }; 111 | }); 112 | } 113 | 114 | /** 115 | * Initializes an instance of EliteDangerousJournalServer 116 | * @memberof EliteDangerousJournalServer 117 | */ 118 | init() { 119 | // store start time for uptime calculations 120 | this.creation = moment(); 121 | 122 | console.log(`${chalk.gray(`o7 Wecome to ${this.config.discovery.serviceName} version ${packageJSON.version}`)}`); 123 | 124 | // get port and id from our config 125 | // destructuring them here just allows us to use the Object shorthand in our 126 | const { 127 | port, 128 | id, 129 | } = this.config; 130 | 131 | // start http server to attach our socket server to 132 | this.httpServer = createServer(); 133 | 134 | console.log(`${chalk.green('Journal Server')} ${chalk.blue(id)} ${chalk.green(`created on ${this.creation.format('YYYY-MM-DD HH:mm:ss')}`)}`); 135 | 136 | // initialize WebSocket Server 137 | this.server = new WebSocket.Server({ server: this.httpServer }); 138 | 139 | // start listening 140 | this.httpServer.listen(port, this.serverListening.bind(this)); 141 | } 142 | 143 | /** 144 | * Establishes Network Discovery after server is ready; initializes indexing 145 | * @memberof EliteDangerousJournalServer 146 | */ 147 | serverListening() { 148 | // reassign port from httpServer in case user passed 0 for port number 149 | this.config.port = this.httpServer.address().port; 150 | 151 | // get port, id, serviceName, discovery, and journalPath from our config 152 | // destructuring them here just allows us to use the Object shorthand in our 153 | // WebSocket.Server() and bonjour options 154 | const { 155 | port, 156 | id, 157 | discovery, 158 | watcher, 159 | heartbeat, 160 | } = this.config; 161 | 162 | console.log(`${chalk.green('Listening for Web Socket Connections on port')} ${chalk.blue(port)}${chalk.green('...')}`); 163 | 164 | if (discovery.enabled) { 165 | // publish service for discovery 166 | this.discovery = zeroconf.publish({ 167 | name: discovery.serviceName, 168 | type: discovery.serviceType, 169 | txt: { id, version: packageJSON.version }, 170 | port, 171 | }); 172 | 173 | console.log(`${chalk.green('Broadcasting service')} ${chalk.blue(discovery.serviceName)} ${chalk.green('for discovery...')}`); 174 | } 175 | 176 | // initialize our heartbeat ping interval 177 | setInterval(this.heartbeat.bind(this), heartbeat.interval); 178 | 179 | // index available Journal files 180 | dir.files(watcher.path, this.indexJournals.bind(this)); 181 | } 182 | 183 | /** 184 | * Pings all clients for heartbeat response 185 | * @memberof EliteDangerousJournalServer 186 | */ 187 | heartbeat() { 188 | // iterate through connected clients and send ping to each one 189 | this.server.clients.forEach((client) => { 190 | // get reference to client so we aren't mutating function argument directly 191 | // because eslint doesn't like that 192 | const socket = client; 193 | 194 | // get property set by our client's heartbeat 195 | const { isReceiving } = socket; 196 | 197 | // if our last heartbeat wasn't returned 198 | if (!isReceiving) { 199 | // get client id 200 | const { journalServerUUID } = socket; 201 | 202 | if (this.config.subscriptions.enabled) { 203 | // remove client subscriptions 204 | this.clientSubscriptions = omit(this.clientSubscriptions, journalServerUUID); 205 | } 206 | 207 | // terminate our socket connection 208 | socket.terminate(); 209 | } else { 210 | // reset isReceiving so we can test the client's connection 211 | socket.isReceiving = false; 212 | 213 | // send a ping 214 | socket.ping('', false, true); 215 | } 216 | }); 217 | } 218 | 219 | /** 220 | * Configures event handling for EliteDangerousJournalServer instance 221 | * @memberof EliteDangerousJournalServer 222 | */ 223 | bindEvents() { 224 | // listen for chokidar ready event 225 | this.journalWatcher.on('ready', this.watcherReady.bind(this)); 226 | 227 | // listen for socket connection 228 | this.server.on('connection', this.websocketConnection.bind(this)); 229 | 230 | // listen for process kill 231 | process.on('SIGINT', this.shutdown.bind(this)); 232 | } 233 | 234 | validateClientStatus(client) { 235 | const { registration: { force } } = this.config; 236 | 237 | const { journalServerClientName: clientName } = client; 238 | 239 | return (!force) || (force && clientName); 240 | } 241 | 242 | websocketError(client, errorObject = {}, suppressHeaders = false) { 243 | let errorData = { 244 | error: true, 245 | }; 246 | 247 | if (errorObject) { 248 | const { message, code } = errorObject; 249 | 250 | errorData = merge({}, errorData, { message, code }); 251 | } 252 | 253 | client.send(this.formatDataForSocket(errorData, client, suppressHeaders)); 254 | } 255 | 256 | /** 257 | * Handles listening for connections and messages from Clients 258 | * @param {Object} ws WebSocket Object from WebSocket Server connection 259 | * @memberof EliteDangerousJournalServer 260 | */ 261 | websocketConnection(ws) { 262 | // generate id for client connection 263 | const clientID = uuid(); 264 | 265 | // get refernce to socket to satisfy eslint rules about assigning to arguments 266 | const socket = ws; 267 | 268 | // get relevant configuration for subscriptions, registration, and error messaging 269 | const { 270 | subscriptions, 271 | registration, 272 | errors: { 273 | mustRegister, 274 | invalidMessage, 275 | invalidPayload, 276 | }, 277 | } = this.config; 278 | 279 | // attach the clientID to the socket client 280 | socket.journalServerUUID = clientID; 281 | 282 | // set name to null until registered 283 | // also check to make sure client isn't already been registered 284 | // this could be done by the application using this Journal Server 285 | socket.journalServerClientName = socket.journalServerClientName || null; 286 | 287 | // set our heartbeat property 288 | socket.isReceiving = true; 289 | 290 | // set up ping listener 291 | socket.on('pong', () => { 292 | // we're still listening 293 | socket.isReceiving = true; 294 | }); 295 | 296 | // handle socket errors 297 | socket.on('error', (err) => { 298 | // ignore that pesky ECONNRESET, socket will disconnect anyway, rethrow others 299 | if (err.code !== 'ECONNRESET') { 300 | throw err; 301 | } 302 | }); 303 | 304 | // handle client disconnection 305 | socket.on('close', () => { 306 | // get client name if one exists 307 | const { journalServerClientName: clientName = '' } = socket; 308 | 309 | if (subscriptions.enabled) { 310 | // remove client subscriptions 311 | this.clientSubscriptions = omit(this.clientSubscriptions, clientID); 312 | } 313 | 314 | console.log(`${chalk.cyan('Client')} ${chalk.red(clientName || clientID)} ${chalk.cyan('disconnected from Web Socket.')}`); 315 | console.log(`${chalk.green('Total clients:')} ${chalk.blue(this.server.clients.size)}`); 316 | }); 317 | 318 | console.log(`${chalk.cyan('Client')} ${chalk.red(clientID)} ${chalk.cyan('connected via Web Socket.')}`); 319 | console.log(`${chalk.green('Total clients:')} ${chalk.blue(this.server.clients.size)}`); 320 | 321 | // if we are allowing subscriptions or registration 322 | // we'll need to configure some things and listen for messages 323 | // otherwise we can just ignore messages from clients entirely 324 | if (subscriptions.enabled || registration.enabled) { 325 | // if subscriptions are enabled 326 | if (subscriptions.enabled) { 327 | // subscribe the client to our default broadcasts 328 | this.clientSubscriptions[clientID] = subscriptions.subscribeTo; 329 | } 330 | 331 | // handle messages from the client 332 | socket.on('message', (data) => { 333 | const { type, payload = {} } = JSON.parse(data); 334 | 335 | // make sure this is a message type we are handling 336 | if (this.validMessageTypes.indexOf(type) !== -1 && payload) { 337 | // if this is a registration message 338 | if (type === registration.messageType && registration.enabled) { 339 | // get client name and possible subscription data from payload 340 | const { subscribeTo = {} } = payload; 341 | 342 | let { clientName } = payload; 343 | 344 | clientName = formatClientName(clientName); 345 | 346 | if (clientName) { 347 | // add client name to socket client 348 | socket.journalServerClientName = clientName; 349 | 350 | console.log(`${chalk.cyan('Client')} ${chalk.red(clientID)} ${chalk.cyan('registered as')} ${chalk.red(clientName)}`); 351 | 352 | // if subscriptions are enabled and the client included a subscribeTo Array in payload 353 | if (subscriptions.enabled && Array.isArray(subscribeTo)) { 354 | // subscribe the client to desired events 355 | this.clientSubscriptions[clientID] = subscribeTo; 356 | 357 | console.log(`${chalk.cyan('Client')} ${chalk.red(clientName)} ${chalk.cyan('updated subscription')}`); 358 | } 359 | 360 | // send the client the current header 361 | return socket.send(this.formatDataForSocket(this.currentHeader, socket)); 362 | } 363 | 364 | if (!this.validateClientStatus(socket)) { 365 | return this.websocketError(socket, mustRegister, true); 366 | } 367 | 368 | return this.websocketError(socket, invalidPayload, true); 369 | // if this is a subscription message 370 | } else if (type === subscriptions.messageType && subscriptions.enabled) { 371 | // get client name from socket if it exists 372 | const { journalServerClientName: clientName } = socket; 373 | 374 | // check client status 375 | if (this.validateClientStatus(socket)) { 376 | if (!Array.isArray(payload)) { 377 | return this.websocketError(socket, invalidPayload); 378 | } 379 | 380 | // update subscriptions with desired Journal Events 381 | this.clientSubscriptions[clientID] = payload; 382 | 383 | // send the client their subscription data 384 | socket.send(this.formatDataForSocket(this.currentHeader, socket)); 385 | 386 | console.log(`${chalk.cyan('Client')} ${chalk.red(clientName || clientID)} ${chalk.cyan('updated subscription')}`); 387 | // if this client needs to register 388 | } else { 389 | // let the client know they need to register 390 | return this.websocketError(socket, mustRegister, true); 391 | } 392 | } 393 | // if this is an invalid message type 394 | } else { 395 | // let the client know this isn't a supported message type 396 | return this.websocketError(socket, invalidMessage); 397 | } 398 | 399 | return false; 400 | }); 401 | 402 | // check client status 403 | if (!this.validateClientStatus(socket)) { 404 | // let client know they need to register 405 | return this.websocketError(socket, mustRegister, true); 406 | } 407 | 408 | // send the client the current header on successful connection 409 | return socket.send(this.formatDataForSocket(this.currentHeader, socket)); 410 | } 411 | 412 | // send the client the current header on successful connection 413 | return socket.send(this.formatDataForSocket(this.currentHeader, socket)); 414 | } 415 | 416 | /** 417 | * Broadcasts a given message to every WebSocket Client 418 | * @param {any} data the message to broadcast 419 | * @memberof EliteDangerousJournalServer 420 | */ 421 | broadcast(data) { 422 | // make sure we were given a message 423 | if (data) { 424 | console.log(`${chalk.gray('Broadcasting event')} ${chalk.green(data.event)}:${chalk.yellow(data.timestamp)}`); 425 | 426 | const { subscriptions, registration } = this.config; 427 | 428 | // iterate through connected clients and send message to each one 429 | this.server.clients.forEach((client) => { 430 | // get id, name, and connection status from client 431 | const { 432 | readyState, 433 | journalServerUUID: clientID, 434 | journalServerClientName: clientName, 435 | } = client; 436 | 437 | // make sure client is listening 438 | if (readyState === WebSocket.OPEN) { 439 | // if client is registered or registration is not required 440 | if ((!registration.force) || (registration.force && clientName)) { 441 | // if subscriptions are enabled 442 | if (subscriptions.enabled && this.clientSubscriptions[clientID]) { 443 | const { subscribeTo: defaults } = subscriptions; 444 | // is the client subscribed to all broadcasts? 445 | const allSubbed = this.clientSubscriptions[clientID].indexOf(defaults) !== -1; 446 | 447 | // is the client subscribed to this broadcast? 448 | const thisSubbed = this.clientSubscriptions[clientID].indexOf(data.event) !== -1; 449 | 450 | // check subscription and emit event 451 | if ((allSubbed) || (thisSubbed)) { 452 | // broadcast event to the client 453 | client.send(this.formatDataForSocket(data, client)); 454 | } 455 | // if subscriptions are disabled; always emit the event 456 | } else { 457 | client.send(this.formatDataForSocket(data, client)); 458 | } 459 | } 460 | } 461 | }); 462 | } 463 | } 464 | 465 | /** 466 | * Normalizes provided data for transmission via WebSocket 467 | * @param {any} payload the data to format for transmission 468 | * @param {Object} client the WebSocket client 469 | * @param {Boolean} [suppressHeaders=false] should we suppress our headers 470 | * @returns {String} 471 | * @memberof EliteDangerousJournalServer 472 | */ 473 | formatDataForSocket(payload, client, suppressHeaders = false) { 474 | // get relevant data from client 475 | const { journalServerUUID: clientID, journalServerClientName: clientName } = client; 476 | 477 | // header data for Journal Server payload 478 | const journalServerHeaders = { 479 | journalServer: this.config.id, 480 | journal: path.basename(this.currentJournal), 481 | subscribedTo: this.clientSubscriptions[clientID], 482 | commander: this.commander, 483 | serverVersion: packageJSON.version, 484 | clientID, 485 | clientName, 486 | }; 487 | 488 | // merge custom headers with server headers and payload 489 | let send; 490 | 491 | if (suppressHeaders) { 492 | send = Object.assign({}, { payload }); 493 | } else { 494 | send = Object.assign({}, this.config.headers, journalServerHeaders, { payload }); 495 | } 496 | 497 | // stringify so we are just sending a String to client 498 | return JSON.stringify(send).trim(); 499 | } 500 | 501 | /** 502 | * Handles indexing Journal files and adding Watcher events 503 | * @memberof EliteDangerousJournalServer 504 | */ 505 | watcherReady() { 506 | // watch for new Journal files 507 | this.journalWatcher.on('add', this.newJournalCreated.bind(this)); 508 | 509 | // watch for changes to Journal files 510 | // because of the way content is appended by E:D we'll need to use 'raw' 511 | // instead of 'change' 512 | this.journalWatcher.on('raw', this.journalEvent.bind(this)); 513 | 514 | // retrieve content from the Journal to get header and seed our entries Array 515 | this.getJournalContents(true); 516 | } 517 | 518 | /** 519 | * Responds to creation of new Journal file 520 | * @param {any} journalPath the path to the new Journal file 521 | * @memberof EliteDangerousJournalServer 522 | */ 523 | newJournalCreated(journalPath) { 524 | const { watcher } = this.config; 525 | 526 | // ensure the new file was an actual Journal file 527 | if (watcher.fileRegEx.test(path.basename(journalPath))) { 528 | // reset current line position 529 | this.currentLine = 0; 530 | 531 | // stop watching old journal 532 | this.journalWatcher.unwatch(this.currentJournal); 533 | 534 | console.log(`${chalk.green('No longer broadcasting changes to')} ${chalk.magenta(this.currentJournal)}`); 535 | 536 | // add Journal path to start of Journals Array 537 | this.journals.unshift(journalPath); 538 | 539 | // make new Journal file the current Journal 540 | [this.currentJournal] = this.journals; 541 | 542 | // stop watching old journal 543 | this.journalWatcher.add(this.currentJournal); 544 | 545 | console.log(`${chalk.green('Now broadcasting changes to')} ${chalk.magenta(this.currentJournal)}`); 546 | 547 | // retrieve Journal entries from the Journal file 548 | this.getJournalContents(); 549 | } 550 | } 551 | 552 | /** 553 | * Responds to changes to Journal files 554 | * @param {String} event the type of event seen by chokidar 555 | * @param {any} filepath the path that was modified 556 | * @memberof EliteDangerousJournalServer 557 | */ 558 | journalEvent(event, filepath) { 559 | // make sure we're seeing a change to our current Journal 560 | if (event === 'change') { 561 | if (filepath === this.currentJournal) { 562 | this.getJournalUpdate(); 563 | } else { 564 | this.getFileUpdate(filepath); 565 | } 566 | } 567 | } 568 | 569 | /** 570 | * Reads changes to the stand-alone, ancillary files and broadcasts their content 571 | * @param {String} filepath of the file that was modified 572 | * @memberOf EliteDangerousJournalServer 573 | */ 574 | getFileUpdate(filepath) { 575 | const baseName = path.basename(filepath).toLowerCase(); 576 | if (this.ancillaryFiles[baseName]) { 577 | try { 578 | const parsedEvent = JSON.parse(fs.readFileSync(filepath, 'utf-8')); 579 | if (parsedEvent) { 580 | this.broadcast(Object.assign({}, parsedEvent, this.ancillaryFiles[baseName])); 581 | } 582 | } 583 | catch (err) { 584 | /* If the file is in the middle of being flushed to disk, we can 585 | * get 'SyntaxError: Unexpected end of JSON input' in JSON.parse(). 586 | */ 587 | if (err.name != 'SyntaxError') { 588 | throw err; 589 | } 590 | console.log(`${chalk.red('Ignoring error:')} ${chalk.magenta(err)}`); 591 | } 592 | } 593 | } 594 | 595 | /** 596 | * Reads the JSON-lines content from the Journal file and returns it as an Array 597 | * @returns {Array} 598 | * @memberof EliteDangerousJournalServer 599 | */ 600 | readJournalFileContents() { 601 | // get the JSON-lines data from the Journal file 602 | // TODO: Find a better way to do this without having to read whole file 603 | const lines = fs.readFileSync(this.journals[0], 'utf-8').split('\n'); 604 | 605 | return lines.filter(item => item.trim()); 606 | } 607 | 608 | /** 609 | * Responds to new content being added to a Journal file 610 | * @memberof EliteDangerousJournalServer 611 | */ 612 | getJournalUpdate() { 613 | // get content Array 614 | const lines = this.readJournalFileContents(); 615 | 616 | // remove entries we have already indexed 617 | lines.splice(0, this.currentLine); 618 | 619 | // get the new entries from this update 620 | this.getJournalEntries(lines); 621 | } 622 | 623 | /** 624 | * Finds the Journals in Journal Directory and sets up the Journal Array 625 | * @param {Object} error error response from dir.files 626 | * @param {Array} files array of file paths found by dir.files 627 | * @memberof EliteDangerousJournalServer 628 | */ 629 | indexJournals(error, files) { 630 | // if we can't index any files 631 | if (error) { 632 | console.log(`${chalk.red('Could not find Journals in')} ${chalk.magenta(this.config.watcher.path)}`); 633 | throw error; 634 | } 635 | 636 | // get correct journal order 637 | this.journals = this.sortJournals(files); 638 | 639 | // the first item in our Array should be our current journal 640 | [this.currentJournal] = this.journals; 641 | 642 | console.log(`${chalk.green('Indexed Journals in')} ${chalk.magenta(this.config.journalPath)}`); 643 | 644 | // get polling interval 645 | const { interval, path: journalPath } = this.config.watcher; 646 | 647 | // start watching our Journal directory for modifications 648 | this.journalWatcher = chokidar.watch([journalPath, this.currentJournal], { 649 | // because of the way E:D writes to Journals, we need to use polling 650 | usePolling: true, 651 | interval, 652 | }); 653 | 654 | console.log(`${chalk.green('Watching for changes to')} ${chalk.magenta(journalPath)}`); 655 | console.log(`${chalk.green('Watching for changes to')} ${chalk.magenta(this.currentJournal)}`); 656 | 657 | // set up our event handlers 658 | this.bindEvents(); 659 | } 660 | 661 | /** 662 | * filter and sort the files Array to get an Array of Journals sorted DESC by 663 | * creation date and Journal part 664 | * @param {Array} [journals=[]] array of files found in directory 665 | * @returns {Array} 666 | * @memberof EliteDangerousJournalServer 667 | */ 668 | sortJournals(journals = []) { 669 | const { watcher } = this.config; 670 | 671 | return journals 672 | // filter to make sure we are only looking at Journal files 673 | // this also has the added benefit of letting us sort the filtered Array 674 | // without mutating the original Array of files 675 | .filter(file => watcher.fileRegEx.test(path.basename(file))) 676 | // sort by datestamp and part to make sure we have the most recent Journal 677 | .sort((a, b) => { 678 | // split Journal names so we can look at datestamp and part as necessary 679 | const aFilenameArray = path.basename(a).split('.'); 680 | const bFilenameArray = path.basename(b).split('.'); 681 | 682 | // geerate moment Objects from our datestamp for comparison 683 | const aDatestamp = moment(aFilenameArray[1], 'YYMMDDHHmmss'); 684 | const bDatestamp = moment(bFilenameArray[1], 'YYMMDDHHmmss'); 685 | 686 | // sort DESC by datestamp 687 | if (bDatestamp.isAfter(aDatestamp)) { 688 | return 1; 689 | } 690 | 691 | return -1; 692 | }); 693 | } 694 | 695 | /** 696 | * Reads content of Journal file 697 | * @param {Boolean} [startup=false] is this the initial index of the Journal Server? 698 | * @memberof EliteDangerousJournalServer 699 | */ 700 | getJournalContents(startup = false) { 701 | // get Journal content Array 702 | let lines = this.readJournalFileContents(); 703 | 704 | // make sure there was content in the file 705 | if (lines.length) { 706 | // remove header 707 | lines = this.getJournalHeader(lines); 708 | 709 | // if this is a new Journal session, clear out previous entries 710 | if (this.currentHeader.part === 1) { 711 | this.entries = []; 712 | } 713 | 714 | // retrieve entries from the Journal content 715 | this.getJournalEntries(lines, startup); 716 | } 717 | } 718 | 719 | 720 | /** 721 | * Retrieves the header from the provided Journal content and returns headerless 722 | * Journal content Array 723 | * @param {Array} lines the content of our Journal file 724 | * @returns {Array} 725 | * @memberof EliteDangerousJournalServer 726 | */ 727 | getJournalHeader(lines) { 728 | // remove header from the content 729 | this.currentHeader = JSON.parse(lines.shift(0, 1)); 730 | 731 | // increment our line counter so we know our place in the Journal content 732 | this.currentLine = this.currentLine + 1; 733 | 734 | console.log(`${chalk.green('Stored new Journal header for')} ${chalk.magenta(path.basename(this.currentJournal))}`); 735 | 736 | // return our headerless content 737 | return lines; 738 | } 739 | 740 | /** 741 | * Iterates through Journal content and add and broadcast entries 742 | * @param {Array} [lines=[]] array of journal content read from Journal file 743 | * @param {Boolean} [mute=false] should we mute the entries 744 | * @memberof EliteDangerousJournalServer 745 | */ 746 | getJournalEntries(lines = [], mute = false) { 747 | // iterate through JSON-lines 748 | lines.forEach((entry) => { 749 | // get line as Object 750 | const formattedEntry = JSON.parse(entry); 751 | 752 | // make sure we have an actual entry and not an empty line or empty Object 753 | if (formattedEntry) { 754 | // add entry to our Array 755 | this.entries.push(formattedEntry); 756 | 757 | // get commander name from the LoadGame event 758 | // this could run if the names are the same, but since it's just assigning 759 | // a value to one property in the case of one event it should be okay 760 | if (formattedEntry.event === EVENT_FOR_COMMANDER_NAME) { 761 | this.commander = formattedEntry.Commander; 762 | } 763 | 764 | // increment line coutner so we know where we are in the content 765 | this.currentLine = this.currentLine + 1; 766 | 767 | // broadcast entry to clients if we aren't supposed to mute it 768 | if (!mute) { 769 | this.broadcast(formattedEntry); 770 | } 771 | } 772 | }); 773 | } 774 | 775 | /** 776 | * responds to process kill and gracefully shuts down 777 | * @memberof EliteDangerousJournalServer 778 | */ 779 | shutdown() { 780 | console.log(`${chalk.red('Journal Server shutting down...')}`); 781 | 782 | // check to see if we need to unpublish network discovery service 783 | if (this.config.discovery) { 784 | this.shutdownWithDiscovery(); 785 | } else { 786 | this.shutdownWithoutDiscovery(); 787 | } 788 | } 789 | 790 | /** 791 | * unpublishes our discoverable service 792 | * @memberof EliteDangerousJournalServer 793 | */ 794 | shutdownWithDiscovery() { 795 | // turn off discovery 796 | zeroconf.unpublishAll(() => { 797 | // destroy service 798 | zeroconf.destroy(); 799 | 800 | console.log(`${chalk.gray('Unpublishing discovery service...')}`); 801 | 802 | // continue with rest of shutdown procedure 803 | this.shutdownWithoutDiscovery(); 804 | }); 805 | } 806 | 807 | /** 808 | * close server connections and exit 809 | * @memberof EliteDangerousJournalServer 810 | */ 811 | shutdownWithoutDiscovery() { 812 | // destroy WebSocket Server 813 | this.server.close(); 814 | 815 | console.log(`${chalk.gray('Muting Web Socket listener...')}`); 816 | 817 | // destroy http server 818 | this.httpServer.close(); 819 | 820 | console.log(`${chalk.gray('Shutting down HTTP server...')}`); 821 | 822 | console.log(`${chalk.gray(`Server uptime was ${moment().diff(this.creation, 'hours')} hours`)}`); 823 | 824 | console.log(`${chalk.gray('Good bye. o7')}`); 825 | 826 | // end execution 827 | process.exit(); 828 | } 829 | } 830 | 831 | module.exports = EliteDangerousJournalServer; 832 | -------------------------------------------------------------------------------- /examples/mocks/Journal.171016003403.01.log: -------------------------------------------------------------------------------- 1 | { "timestamp":"2017-10-17T01:40:50Z", "event":"Fileheader", "part":1, "language":"English\\UK", "gameversion":"2.4", "build":"r156431/r0 " } 2 | { "timestamp":"2017-10-17T01:40:50Z", "event":"Music", "MusicTrack":"NoTrack" } 3 | { "timestamp":"2017-10-17T01:40:50Z", "event":"Music", "MusicTrack":"MainMenu" } 4 | { "timestamp":"2017-10-17T01:41:49Z", "event":"Cargo", "Inventory":[ ] } 5 | { "timestamp":"2017-10-17T01:41:49Z", "event":"Loadout", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "Modules":[ { "Slot":"Armour", "Item":"CobraMkIII_Armour_Grade1", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"PowerPlant", "Item":"Int_Powerplant_Size4_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":59633 }, { "Slot":"MainEngines", "Item":"Int_Engine_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"FrameShiftDrive", "Item":"Int_Hyperdrive_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"LifeSupport", "Item":"Int_LifeSupport_Size3_Class2", "On":true, "Priority":1, "Health":1.000000, "Value":10133 }, { "Slot":"PowerDistributor", "Item":"Int_PowerDistributor_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"Radar", "Item":"Int_Sensors_Size3_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":10133 }, { "Slot":"FuelTank", "Item":"Int_FuelTank_Size4_Class3", "On":true, "Priority":1, "Health":1.000000, "Value":24734 }, { "Slot":"Slot01_Size4", "Item":"Int_CargoRack_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":34328 }, { "Slot":"Slot02_Size4", "Item":"Int_CargoRack_Size4_Class1", "On":true, "Priority":1, "Health":1.000000, "Value":34328 }, { "Slot":"Slot03_Size4", "Item":"Int_ShieldGenerator_Size4_Class2", "On":true, "Priority":0, "Health":1.000000, "Value":59633 }, { "Slot":"Slot04_Size2", "Item":"Int_FuelScoop_Size2_Class5", "On":true, "Priority":1, "Health":1.000000, "Value":284844 }, { "Slot":"Slot05_Size2", "Item":"Int_StellarBodyDiscoveryScanner_Standard", "On":true, "Priority":1, "Health":1.000000, "Value":1000 }, { "Slot":"Slot06_Size2", "Item":"Int_Repairer_Size2_Class3", "On":true, "Priority":1, "AmmoInClip":2300, "Health":1.000000, "Value":162000 }, { "Slot":"PlanetaryApproachSuite", "Item":"Int_PlanetApproachSuite", "On":true, "Priority":1, "Health":1.000000, "Value":500 }, { "Slot":"ShipCockpit", "Item":"CobraMkIII_Cockpit", "On":true, "Priority":1, "Health":1.000000, "Value":0 }, { "Slot":"CargoHatch", "Item":"ModularCargoBayDoor", "On":true, "Priority":2, "Health":1.000000, "Value":0 } ] } 6 | { "timestamp":"2017-10-17T01:41:49Z", "event":"Materials", "Raw":[ ], "Manufactured":[ { "Name":"mechanicalscrap", "Count":4 }, { "Name":"chemicalmanipulators", "Count":3 }, { "Name":"fedproprietarycomposites", "Count":12 } ], "Encoded":[ { "Name":"shieldsoakanalysis", "Count":6 }, { "Name":"emissiondata", "Count":15 }, { "Name":"shieldpatternanalysis", "Count":9 }, { "Name":"shieldcyclerecordings", "Count":18 }, { "Name":"scandatabanks", "Count":3 }, { "Name":"shielddensityreports", "Count":12 }, { "Name":"decodedemissiondata", "Count":9 }, { "Name":"scrambledemissiondata", "Count":2 }, { "Name":"legacyfirmware", "Count":1 } ] } 7 | { "timestamp":"2017-10-17T01:41:49Z", "event":"LoadGame", "Commander":"JournalServer", "Ship":"CobraMkIII", "ShipID":1, "ShipName":"Flat Head", "ShipIdent":"UNSC-1", "FuelLevel":16.000000, "FuelCapacity":16.000000, "GameMode":"Open", "Credits":1876048, "Loan":0 } 8 | { "timestamp":"2017-10-17T01:41:49Z", "event":"Rank", "Combat":1, "Trade":3, "Explore":1, "Empire":0, "Federation":0, "CQC":0 } 9 | { "timestamp":"2017-10-17T01:41:49Z", "event":"Progress", "Combat":81, "Trade":23, "Explore":9, "Empire":0, "Federation":100, "CQC":0 } 10 | { "timestamp":"2017-10-17T01:41:51Z", "event":"ApproachSettlement", "Name":"Verrazzano's Inheritance" } 11 | { "timestamp":"2017-10-17T01:42:36Z", "event":"Location", "Docked":true, "StationName":"Verrazzano's Inheritance", "StationType":"SurfaceStation", "StarSystem":"Murung", "StarPos":[-31.500,23.688,75.938], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Industrial;", "SystemEconomy_Localised":"Industrial", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_high;", "SystemSecurity_Localised":"High Security", "Population":42877182, "Body":"Murung A 2", "BodyType":"Planet", "Factions":[ { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Independent Murung Values Party", "FactionState":"Boom", "Government":"Democracy", "Influence":0.115884, "Allegiance":"Independent" }, { "Name":"Natural Mentiens Defence Force", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.211788, "Allegiance":"Independent" }, { "Name":"Murung Services", "FactionState":"Boom", "Government":"Corporate", "Influence":0.332667, "Allegiance":"Federation" }, { "Name":"Regulatory State of Murung", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.167832, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Murung Vision Partners", "FactionState":"Boom", "Government":"Corporate", "Influence":0.171828, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] } ], "SystemFaction":"Murung Services", "FactionState":"Boom" } 12 | { "timestamp":"2017-10-17T01:42:38Z", "event":"Music", "MusicTrack":"NoTrack" } 13 | { "timestamp":"2017-10-17T01:42:38Z", "event":"Docked", "StationName":"Verrazzano's Inheritance", "StationType":"SurfaceStation", "StarSystem":"Murung", "StationFaction":"Regulatory State of Murung", "FactionState":"Boom", "StationGovernment":"$government_Dictatorship;", "StationGovernment_Localised":"Dictatorship", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Colony;", "StationEconomy_Localised":"Colony", "DistFromStarLS":50.830433 } 14 | { "timestamp":"2017-10-17T01:44:02Z", "event":"ReceiveText", "From":"$ShipName_Police_Independent;", "From_Localised":"System Authority Vessel", "Message":"$Police_EndPatrol01;", "Message_Localised":"Shift's over guys, I'm heading back to base.", "Channel":"npc" } 15 | { "timestamp":"2017-10-17T01:46:08Z", "event":"Music", "MusicTrack":"GalaxyMap" } 16 | { "timestamp":"2017-10-17T01:46:52Z", "event":"Music", "MusicTrack":"NoTrack" } 17 | { "timestamp":"2017-10-17T01:46:57Z", "event":"ReceiveText", "From":"$ShipName_Police_Independent;", "From_Localised":"System Authority Vessel", "Message":"$Police_StartPatrol03;", "Message_Localised":"Receiving five by five, I'm in the air now, joining patrol.", "Channel":"npc" } 18 | { "timestamp":"2017-10-17T01:47:00Z", "event":"MissionAccepted", "Faction":"Natural Mentiens Defence Force", "Name":"Mission_Delivery_Boom", "LocalisedName":"Boom time delivery of 21 units of Biowaste", "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":21, "TargetFaction":"57 Zeta Serpentis General Holdings", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Expiry":"2017-10-18T01:45:04Z", "Influence":"Low", "Reputation":"Low", "Reward":237006, "MissionID":228681274 } 19 | { "timestamp":"2017-10-17T01:47:13Z", "event":"MissionAccepted", "Faction":"Natural Mentiens Defence Force", "Name":"Mission_Delivery_Boom", "LocalisedName":"Boom time delivery of 2 units of Biowaste", "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":2, "TargetFaction":"57 Zeta Serpentis General Holdings", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Expiry":"2017-10-18T01:45:04Z", "Influence":"Low", "Reputation":"Low", "Reward":48678, "MissionID":228681347 } 20 | { "timestamp":"2017-10-17T01:47:41Z", "event":"MissionAccepted", "Faction":"Independent Murung Values Party", "Name":"Mission_Delivery_Boom", "LocalisedName":"Boom time delivery of 6 units of Biowaste", "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":6, "TargetFaction":"Belu Silver Federal Industry", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Expiry":"2017-10-18T01:45:04Z", "Influence":"Low", "Reputation":"Low", "Reward":157815, "MissionID":228681523 } 21 | { "timestamp":"2017-10-17T01:49:26Z", "event":"Undocked", "StationName":"Verrazzano's Inheritance", "StationType":"SurfaceStation" } 22 | { "timestamp":"2017-10-17T01:50:15Z", "event":"ReceiveText", "From":"Verrazzano's Inheritance", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 23 | { "timestamp":"2017-10-17T01:50:34Z", "event":"ReceiveText", "From":"$ShipName_Police_Independent;", "From_Localised":"System Authority Vessel", "Message":"$Police_StartPatrol03;", "Message_Localised":"Receiving five by five, I'm in the air now, joining patrol.", "Channel":"npc" } 24 | { "timestamp":"2017-10-17T01:50:45Z", "event":"Music", "MusicTrack":"GalaxyMap" } 25 | { "timestamp":"2017-10-17T01:51:08Z", "event":"Music", "MusicTrack":"Exploration" } 26 | { "timestamp":"2017-10-17T01:51:33Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Alrai Sector DL-Y d82", "StarClass":"F" } 27 | { "timestamp":"2017-10-17T01:51:39Z", "event":"Music", "MusicTrack":"NoTrack" } 28 | { "timestamp":"2017-10-17T01:51:52Z", "event":"FSDJump", "StarSystem":"Alrai Sector DL-Y d82", "StarPos":[-30.875,19.156,69.938], "SystemAllegiance":"", "SystemEconomy":"$economy_None;", "SystemEconomy_Localised":"None", "SystemGovernment":"$government_None;", "SystemGovernment_Localised":"None", "SystemSecurity":"$GAlAXY_MAP_INFO_state_anarchy;", "SystemSecurity_Localised":"Anarchy", "Population":0, "JumpDist":7.545, "FuelUsed":0.607748, "FuelLevel":15.392252 } 29 | { "timestamp":"2017-10-17T01:51:52Z", "event":"Music", "MusicTrack":"Supercruise" } 30 | { "timestamp":"2017-10-17T01:52:03Z", "event":"FuelScoop", "Scooped":0.018194, "Total":15.410442 } 31 | { "timestamp":"2017-10-17T01:52:27Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"57 Zeta Serpentis", "StarClass":"F" } 32 | { "timestamp":"2017-10-17T01:52:33Z", "event":"Music", "MusicTrack":"NoTrack" } 33 | { "timestamp":"2017-10-17T01:52:46Z", "event":"FSDJump", "StarSystem":"57 Zeta Serpentis", "StarPos":[-30.938,12.000,69.281], "SystemAllegiance":"Independent", "SystemEconomy":"$economy_Agri;", "SystemEconomy_Localised":"Agriculture", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_high;", "SystemSecurity_Localised":"High Security", "Population":2773411214, "JumpDist":7.187, "FuelUsed":0.540381, "FuelLevel":14.870062, "Factions":[ { "Name":"Independent 57 Zeta Serpentis Resistance", "FactionState":"CivilWar", "Government":"Democracy", "Influence":0.044910, "Allegiance":"Federation" }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Democrats of Utse", "FactionState":"None", "Government":"Democracy", "Influence":0.154691, "Allegiance":"Federation", "RecoveringStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"57 Zeta Serpentis Incorporated", "FactionState":"CivilWar", "Government":"Corporate", "Influence":0.043912, "Allegiance":"Independent" }, { "Name":"Belu Silver Federal Industry", "FactionState":"Boom", "Government":"Corporate", "Influence":0.262475, "Allegiance":"Federation" }, { "Name":"57 Zeta Serpentis Dominion", "FactionState":"CivilWar", "Government":"Dictatorship", "Influence":0.043912, "Allegiance":"Independent", "PendingStates":[ { "State":"Outbreak", "Trend":0 } ] }, { "Name":"57 Zeta Serpentis General Holdings", "FactionState":"Boom", "Government":"Corporate", "Influence":0.406188, "Allegiance":"Independent" }, { "Name":"57 Zeta Serpentis Gold Ring", "FactionState":"CivilWar", "Government":"Anarchy", "Influence":0.043912, "Allegiance":"Independent", "PendingStates":[ { "State":"Boom", "Trend":0 } ] } ], "SystemFaction":"57 Zeta Serpentis General Holdings", "FactionState":"Boom" } 34 | { "timestamp":"2017-10-17T01:52:46Z", "event":"Music", "MusicTrack":"DestinationFromHyperspace" } 35 | { "timestamp":"2017-10-17T01:52:51Z", "event":"Music", "MusicTrack":"Supercruise" } 36 | { "timestamp":"2017-10-17T01:53:14Z", "event":"FuelScoop", "Scooped":0.809298, "Total":15.679351 } 37 | { "timestamp":"2017-10-17T01:55:19Z", "event":"Music", "MusicTrack":"SystemMap" } 38 | { "timestamp":"2017-10-17T01:55:54Z", "event":"Music", "MusicTrack":"Supercruise" } 39 | { "timestamp":"2017-10-17T02:00:46Z", "event":"Music", "MusicTrack":"GalaxyMap" } 40 | { "timestamp":"2017-10-17T02:01:04Z", "event":"Music", "MusicTrack":"Supercruise" } 41 | { "timestamp":"2017-10-17T02:11:57Z", "event":"SupercruiseExit", "StarSystem":"57 Zeta Serpentis", "Body":"Musabayev Dock", "BodyType":"Station" } 42 | { "timestamp":"2017-10-17T02:11:57Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 43 | { "timestamp":"2017-10-17T02:11:58Z", "event":"ReceiveText", "From":"Musabayev Dock", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 44 | { "timestamp":"2017-10-17T02:12:02Z", "event":"Music", "MusicTrack":"NoTrack" } 45 | { "timestamp":"2017-10-17T02:12:12Z", "event":"DockingRequested", "StationName":"Musabayev Dock" } 46 | { "timestamp":"2017-10-17T02:12:12Z", "event":"ReceiveText", "From":"Musabayev Dock", "Message":"$DockingChatter_Neutral;", "Message_Localised":"Ensure to observe starport protocol during your visit, pilot.", "Channel":"npc" } 47 | { "timestamp":"2017-10-17T02:12:14Z", "event":"ReceiveText", "From":"Musabayev Dock", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 48 | { "timestamp":"2017-10-17T02:12:14Z", "event":"DockingGranted", "LandingPad":13, "StationName":"Musabayev Dock" } 49 | { "timestamp":"2017-10-17T02:13:10Z", "event":"Music", "MusicTrack":"Starport" } 50 | { "timestamp":"2017-10-17T02:13:37Z", "event":"Docked", "StationName":"Musabayev Dock", "StationType":"Orbis", "StarSystem":"57 Zeta Serpentis", "StationFaction":"57 Zeta Serpentis General Holdings", "FactionState":"Boom", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Agri;", "StationEconomy_Localised":"Agriculture", "DistFromStarLS":438565.156250 } 51 | { "timestamp":"2017-10-17T02:13:59Z", "event":"MissionCompleted", "Faction":"57 Zeta Serpentis General Holdings", "Name":"Mission_Delivery_Boom_name", "MissionID":228681274, "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":21, "TargetFaction":"57 Zeta Serpentis General Holdings", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Reward":237006 } 52 | { "timestamp":"2017-10-17T02:14:02Z", "event":"MissionCompleted", "Faction":"57 Zeta Serpentis General Holdings", "Name":"Mission_Delivery_Boom_name", "MissionID":228681347, "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":2, "TargetFaction":"57 Zeta Serpentis General Holdings", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Reward":48678, "CommodityReward":[ { "Name":"ArticulationMotors", "Count":4 } ] } 53 | { "timestamp":"2017-10-17T02:14:02Z", "event":"MaterialDiscovered", "Category":"Manufactured", "Name":"gridresistors", "DiscoveryNumber":4 } 54 | { "timestamp":"2017-10-17T02:14:02Z", "event":"MaterialCollected", "Category":"Manufactured", "Name":"gridresistors", "Count":1 } 55 | { "timestamp":"2017-10-17T02:14:22Z", "event":"MissionCompleted", "Faction":"Belu Silver Federal Industry", "Name":"Mission_Delivery_Boom_name", "MissionID":228681523, "Commodity":"$Biowaste_Name;", "Commodity_Localised":"Biowaste", "Count":6, "TargetFaction":"Belu Silver Federal Industry", "DestinationSystem":"57 Zeta Serpentis", "DestinationStation":"Musabayev Dock", "Reward":157815 } 56 | { "timestamp":"2017-10-17T02:15:37Z", "event":"Music", "MusicTrack":"GalaxyMap" } 57 | { "timestamp":"2017-10-17T02:16:13Z", "event":"Music", "MusicTrack":"Starport" } 58 | { "timestamp":"2017-10-17T02:17:26Z", "event":"Music", "MusicTrack":"GalaxyMap" } 59 | { "timestamp":"2017-10-17T02:17:38Z", "event":"Music", "MusicTrack":"Starport" } 60 | { "timestamp":"2017-10-17T02:18:02Z", "event":"Music", "MusicTrack":"GalaxyMap" } 61 | { "timestamp":"2017-10-17T02:18:38Z", "event":"Music", "MusicTrack":"Starport" } 62 | { "timestamp":"2017-10-17T02:19:26Z", "event":"RefuelAll", "Cost":43, "Amount":0.841908 } 63 | { "timestamp":"2017-10-17T02:20:37Z", "event":"Undocked", "StationName":"Musabayev Dock", "StationType":"Orbis" } 64 | { "timestamp":"2017-10-17T02:21:19Z", "event":"Music", "MusicTrack":"NoTrack" } 65 | { "timestamp":"2017-10-17T02:21:46Z", "event":"Scanned", "ScanType":"Cargo" } 66 | { "timestamp":"2017-10-17T02:21:46Z", "event":"ReceiveText", "From":"$ShipName_Police_Independent;", "From_Localised":"System Authority Vessel", "Message":"$Police_ThankYouPassedStopAndSearch06;", "Message_Localised":"We're done here pilot, move along.", "Channel":"npc" } 67 | { "timestamp":"2017-10-17T02:21:52Z", "event":"Music", "MusicTrack":"GalaxyMap" } 68 | { "timestamp":"2017-10-17T02:21:57Z", "event":"Music", "MusicTrack":"NoTrack" } 69 | { "timestamp":"2017-10-17T02:22:00Z", "event":"Music", "MusicTrack":"GalaxyMap" } 70 | { "timestamp":"2017-10-17T02:22:06Z", "event":"Music", "MusicTrack":"NoTrack" } 71 | { "timestamp":"2017-10-17T02:22:27Z", "event":"ReceiveText", "From":"Musabayev Dock", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 72 | { "timestamp":"2017-10-17T02:22:30Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Sinann", "StarClass":"G" } 73 | { "timestamp":"2017-10-17T02:22:48Z", "event":"FSDJump", "StarSystem":"Sinann", "StarPos":[-40.656,15.875,64.625], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Military;", "SystemEconomy_Localised":"Military", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_low;", "SystemSecurity_Localised":"Low Security", "Population":16834, "JumpDist":11.452, "FuelUsed":1.244203, "FuelLevel":14.755797, "Factions":[ { "Name":"LP 510-10 Solutions", "FactionState":"None", "Government":"Corporate", "Influence":0.076076, "Allegiance":"Federation", "PendingStates":[ { "State":"CivilUnrest", "Trend":0 }, { "State":"Lockdown", "Trend":1 } ] }, { "Name":"Revolutionary Party of Cebalrai", "FactionState":"Boom", "Government":"Democracy", "Influence":0.087087, "Allegiance":"Federation" }, { "Name":"Sinann Silver Raiders", "FactionState":"Boom", "Government":"Anarchy", "Influence":0.117117, "Allegiance":"Independent" }, { "Name":"GAT 2 Exchange", "FactionState":"Lockdown", "Government":"Corporate", "Influence":0.467467, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"CivilUnrest", "Trend":0 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Sinann Group", "FactionState":"CivilWar", "Government":"Corporate", "Influence":0.040040, "Allegiance":"Federation" }, { "Name":"Co-operative of Sinann", "FactionState":"CivilWar", "Government":"Cooperative", "Influence":0.038038, "Allegiance":"Independent" }, { "Name":"BD+03 3531a Central Commodities", "FactionState":"Boom", "Government":"Corporate", "Influence":0.174174, "Allegiance":"Federation" } ], "SystemFaction":"GAT 2 Exchange", "FactionState":"Lockdown" } 74 | { "timestamp":"2017-10-17T02:22:49Z", "event":"Music", "MusicTrack":"Supercruise" } 75 | { "timestamp":"2017-10-17T02:23:00Z", "event":"FuelScoop", "Scooped":0.038542, "Total":14.794333 } 76 | { "timestamp":"2017-10-17T02:23:13Z", "event":"Music", "MusicTrack":"SystemMap" } 77 | { "timestamp":"2017-10-17T02:23:35Z", "event":"Music", "MusicTrack":"Supercruise" } 78 | { "timestamp":"2017-10-17T02:24:22Z", "event":"Music", "MusicTrack":"SystemMap" } 79 | { "timestamp":"2017-10-17T02:24:25Z", "event":"Music", "MusicTrack":"Supercruise" } 80 | { "timestamp":"2017-10-17T02:25:10Z", "event":"Music", "MusicTrack":"SystemMap" } 81 | { "timestamp":"2017-10-17T02:25:19Z", "event":"Music", "MusicTrack":"Supercruise" } 82 | { "timestamp":"2017-10-17T02:25:30Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Bester;", "From_Localised":"Bester", "Message":"$Protester_Patrol02;", "Message_Localised":"Oh no, we won't go! Oh no, we won't go!!", "Channel":"npc" } 83 | { "timestamp":"2017-10-17T02:26:05Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Bester;", "From_Localised":"Bester", "Message":"$Protester_Patrol05;", "Message_Localised":"Rise up the workers, unite and fight!", "Channel":"npc" } 84 | { "timestamp":"2017-10-17T02:26:34Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Bester;", "From_Localised":"Bester", "Message":"$Protester_Patrol02;", "Message_Localised":"Oh no, we won't go! Oh no, we won't go!!", "Channel":"npc" } 85 | { "timestamp":"2017-10-17T02:26:40Z", "event":"SupercruiseExit", "StarSystem":"Sinann", "Body":"Jones Point", "BodyType":"Station" } 86 | { "timestamp":"2017-10-17T02:26:41Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 87 | { "timestamp":"2017-10-17T02:26:46Z", "event":"Music", "MusicTrack":"Exploration" } 88 | { "timestamp":"2017-10-17T02:26:48Z", "event":"ReceiveText", "From":"Jones Point", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 89 | { "timestamp":"2017-10-17T02:26:54Z", "event":"DockingRequested", "StationName":"Jones Point" } 90 | { "timestamp":"2017-10-17T02:26:54Z", "event":"ReceiveText", "From":"Jones Point", "Message":"$DockingChatter_Neutral;", "Message_Localised":"Ensure to observe starport protocol during your visit, pilot.", "Channel":"npc" } 91 | { "timestamp":"2017-10-17T02:26:56Z", "event":"ReceiveText", "From":"Jones Point", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 92 | { "timestamp":"2017-10-17T02:26:56Z", "event":"DockingGranted", "LandingPad":2, "StationName":"Jones Point" } 93 | { "timestamp":"2017-10-17T02:29:21Z", "event":"Docked", "StationName":"Jones Point", "StationType":"Outpost", "StarSystem":"Sinann", "StationFaction":"GAT 2 Exchange", "FactionState":"Lockdown", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "BlackMarket", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "MissionsGenerated", "Facilitator", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Refinery;", "StationEconomy_Localised":"Refinery", "DistFromStarLS":1628.200439 } 94 | { "timestamp":"2017-10-17T02:29:32Z", "event":"RefuelAll", "Cost":69, "Amount":1.353552 } 95 | { "timestamp":"2017-10-17T02:30:36Z", "event":"Undocked", "StationName":"Jones Point", "StationType":"Outpost" } 96 | { "timestamp":"2017-10-17T02:31:32Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Alrai Sector DL-Y d110", "StarClass":"G" } 97 | { "timestamp":"2017-10-17T02:31:35Z", "event":"ReceiveText", "From":"Jones Point", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 98 | { "timestamp":"2017-10-17T02:31:38Z", "event":"Music", "MusicTrack":"NoTrack" } 99 | { "timestamp":"2017-10-17T02:31:51Z", "event":"FSDJump", "StarSystem":"Alrai Sector DL-Y d110", "StarPos":[-45.375,9.781,59.594], "SystemAllegiance":"", "SystemEconomy":"$economy_None;", "SystemEconomy_Localised":"None", "SystemGovernment":"$government_None;", "SystemGovernment_Localised":"None", "SystemSecurity":"$GAlAXY_MAP_INFO_state_anarchy;", "SystemSecurity_Localised":"Anarchy", "Population":0, "JumpDist":9.204, "FuelUsed":0.752669, "FuelLevel":15.247332 } 100 | { "timestamp":"2017-10-17T02:31:51Z", "event":"Music", "MusicTrack":"Supercruise" } 101 | { "timestamp":"2017-10-17T02:32:04Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Claire York;", "From_Localised":"Claire York", "Message":"$Pirate_HunterHostileSC_Relevant02;", "Message_Localised":"I knew I'd find you eventually, all that tasty cargo!", "Channel":"npc" } 102 | { "timestamp":"2017-10-17T02:32:06Z", "event":"FuelScoop", "Scooped":0.168304, "Total":15.415632 } 103 | { "timestamp":"2017-10-17T02:32:10Z", "event":"Music", "MusicTrack":"SystemMap" } 104 | { "timestamp":"2017-10-17T02:32:11Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Claire York;", "From_Localised":"Claire York", "Message":"$Pirate_StartInterdiction01;", "Message_Localised":"You're mine now, Commander. ", "Channel":"npc" } 105 | { "timestamp":"2017-10-17T02:32:11Z", "event":"Music", "MusicTrack":"Supercruise" } 106 | { "timestamp":"2017-10-17T02:32:33Z", "event":"EscapeInterdiction", "Interdictor":"Claire York", "IsPlayer":false } 107 | { "timestamp":"2017-10-17T02:32:42Z", "event":"Music", "MusicTrack":"SystemMap" } 108 | { "timestamp":"2017-10-17T02:32:44Z", "event":"Music", "MusicTrack":"Supercruise" } 109 | { "timestamp":"2017-10-17T02:33:15Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"LP 571-80", "StarClass":"M" } 110 | { "timestamp":"2017-10-17T02:33:21Z", "event":"Music", "MusicTrack":"NoTrack" } 111 | { "timestamp":"2017-10-17T02:33:34Z", "event":"FSDJump", "StarSystem":"LP 571-80", "StarPos":[-43.969,2.563,51.750], "SystemAllegiance":"", "SystemEconomy":"$economy_None;", "SystemEconomy_Localised":"None", "SystemGovernment":"$government_None;", "SystemGovernment_Localised":"None", "SystemSecurity":"$GAlAXY_MAP_INFO_state_anarchy;", "SystemSecurity_Localised":"Anarchy", "Population":0, "JumpDist":10.752, "FuelUsed":1.069729, "FuelLevel":14.345902 } 112 | { "timestamp":"2017-10-17T02:33:34Z", "event":"Music", "MusicTrack":"Supercruise" } 113 | { "timestamp":"2017-10-17T02:33:48Z", "event":"FuelScoop", "Scooped":0.213075, "Total":14.558994 } 114 | { "timestamp":"2017-10-17T02:33:51Z", "event":"Music", "MusicTrack":"SystemMap" } 115 | { "timestamp":"2017-10-17T02:33:53Z", "event":"Music", "MusicTrack":"Supercruise" } 116 | { "timestamp":"2017-10-17T02:34:32Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"GD 219", "StarClass":"DA" } 117 | { "timestamp":"2017-10-17T02:34:37Z", "event":"Music", "MusicTrack":"NoTrack" } 118 | { "timestamp":"2017-10-17T02:34:50Z", "event":"FSDJump", "StarSystem":"GD 219", "StarPos":[-48.656,0.125,41.688], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_HighTech;", "SystemEconomy_Localised":"High Tech", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_medium;", "SystemSecurity_Localised":"Medium Security", "Population":3972954, "JumpDist":11.365, "FuelUsed":1.204445, "FuelLevel":13.354549, "Factions":[ { "Name":"LTT 15985 Jet Dynamic Solutions", "FactionState":"War", "Government":"Corporate", "Influence":0.009990, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Ekuru for Equality", "FactionState":"None", "Government":"Democracy", "Influence":0.034965, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Ngolibardu Electronics Solutions", "FactionState":"None", "Government":"Corporate", "Influence":0.592408, "Allegiance":"Federation", "RecoveringStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"GD 219 Purple Partnership", "FactionState":"Boom", "Government":"Anarchy", "Influence":0.085914, "Allegiance":"Independent", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"Liberals of GD 219", "FactionState":"War", "Government":"Democracy", "Influence":0.010989, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"Outbreak", "Trend":0 } ] }, { "Name":"DG Transport Company", "FactionState":"Boom", "Government":"Democracy", "Influence":0.085914, "Allegiance":"Federation", "PendingStates":[ { "State":"CivilWar", "Trend":0 } ] }, { "Name":"LHS 3447 Dynamic & Co", "FactionState":"Boom", "Government":"Corporate", "Influence":0.179820, "Allegiance":"Federation", "PendingStates":[ { "State":"CivilUnrest", "Trend":1 } ] } ], "SystemFaction":"Ngolibardu Electronics Solutions" } 119 | { "timestamp":"2017-10-17T02:34:50Z", "event":"Music", "MusicTrack":"Supercruise" } 120 | { "timestamp":"2017-10-17T02:35:16Z", "event":"Music", "MusicTrack":"SystemMap" } 121 | { "timestamp":"2017-10-17T02:35:22Z", "event":"Music", "MusicTrack":"Supercruise" } 122 | { "timestamp":"2017-10-17T02:38:03Z", "event":"SupercruiseExit", "StarSystem":"GD 219", "Body":"McKee Ring", "BodyType":"Station" } 123 | { "timestamp":"2017-10-17T02:38:03Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 124 | { "timestamp":"2017-10-17T02:38:08Z", "event":"Music", "MusicTrack":"NoTrack" } 125 | { "timestamp":"2017-10-17T02:38:11Z", "event":"ReceiveText", "From":"McKee Ring", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 126 | { "timestamp":"2017-10-17T02:39:46Z", "event":"DockingRequested", "StationName":"McKee Ring" } 127 | { "timestamp":"2017-10-17T02:39:46Z", "event":"ReceiveText", "From":"McKee Ring", "Message":"$DockingChatter_Friendly;", "Message_Localised":"It's great to see you again, Commander.", "Channel":"npc" } 128 | { "timestamp":"2017-10-17T02:39:47Z", "event":"ReceiveText", "From":"McKee Ring", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 129 | { "timestamp":"2017-10-17T02:39:47Z", "event":"DockingGranted", "LandingPad":1, "StationName":"McKee Ring" } 130 | { "timestamp":"2017-10-17T02:40:36Z", "event":"Music", "MusicTrack":"Starport" } 131 | { "timestamp":"2017-10-17T02:40:52Z", "event":"Docked", "StationName":"McKee Ring", "StationType":"Coriolis", "StarSystem":"GD 219", "StationFaction":"Ngolibardu Electronics Solutions", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_HighTech;", "StationEconomy_Localised":"High Tech", "DistFromStarLS":1464.595703 } 132 | { "timestamp":"2017-10-17T02:41:02Z", "event":"RefuelAll", "Cost":143, "Amount":2.827688 } 133 | { "timestamp":"2017-10-17T02:42:32Z", "event":"SellExplorationData", "Systems":[ "Sinann", "Alrai Sector DL-Y d82", "Alrai Sector DL-Y d110" ], "Discovered":[ ], "BaseValue":9998, "Bonus":0 } 134 | { "timestamp":"2017-10-17T02:43:22Z", "event":"MissionAccepted", "Faction":"LTT 15985 Jet Dynamic Solutions", "Name":"Mission_Courier_War", "LocalisedName":"Strategic data transfer", "TargetFaction":"Ngolibardu Purple Drug Empire", "DestinationSystem":"Ngolibardu", "DestinationStation":"Vernadsky Port", "Expiry":"2017-10-18T02:19:29Z", "Influence":"Low", "Reputation":"Med", "Reward":5680, "MissionID":228701842 } 135 | { "timestamp":"2017-10-17T02:43:58Z", "event":"Undocked", "StationName":"McKee Ring", "StationType":"Coriolis" } 136 | { "timestamp":"2017-10-17T02:44:15Z", "event":"Music", "MusicTrack":"NoTrack" } 137 | { "timestamp":"2017-10-17T02:45:10Z", "event":"ReceiveText", "From":"McKee Ring", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 138 | { "timestamp":"2017-10-17T02:45:18Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Ngolibardu", "StarClass":"M" } 139 | { "timestamp":"2017-10-17T02:45:37Z", "event":"FSDJump", "StarSystem":"Ngolibardu", "StarPos":[-48.813,-7.750,39.813], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Refinery;", "SystemEconomy_Localised":"Refinery", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_medium;", "SystemSecurity_Localised":"Medium Security", "Population":2461357, "JumpDist":8.097, "FuelUsed":0.560477, "FuelLevel":15.439523, "Factions":[ { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Ekuru for Equality", "FactionState":"War", "Government":"Democracy", "Influence":0.067000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Ngolibardu Electronics Solutions", "FactionState":"None", "Government":"Corporate", "Influence":0.427000, "Allegiance":"Federation", "RecoveringStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Ngolibardu Purple Drug Empire", "FactionState":"Boom", "Government":"Anarchy", "Influence":0.033000, "Allegiance":"Independent" }, { "Name":"Wolf 865 Group", "FactionState":"Boom", "Government":"Corporate", "Influence":0.225000, "Allegiance":"Federation" }, { "Name":"Liberals of Ngolibardu", "FactionState":"None", "Government":"Democracy", "Influence":0.086000, "Allegiance":"Independent" }, { "Name":"League of Ngolibardu", "FactionState":"None", "Government":"Dictatorship", "Influence":0.026000, "Allegiance":"Independent", "PendingStates":[ { "State":"Boom", "Trend":1 } ], "RecoveringStates":[ { "State":"War", "Trend":0 } ] }, { "Name":"Ross 163 Blue Dynamic Limited", "FactionState":"None", "Government":"Corporate", "Influence":0.098000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ], "RecoveringStates":[ { "State":"War", "Trend":0 } ] }, { "Name":"Ngolibardu Creative & Co", "FactionState":"War", "Government":"Corporate", "Influence":0.038000, "Allegiance":"Independent", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"Outbreak", "Trend":0 } ] } ], "SystemFaction":"Ngolibardu Electronics Solutions" } 140 | { "timestamp":"2017-10-17T02:45:37Z", "event":"Music", "MusicTrack":"DestinationFromHyperspace" } 141 | { "timestamp":"2017-10-17T02:45:42Z", "event":"Music", "MusicTrack":"Supercruise" } 142 | { "timestamp":"2017-10-17T02:45:52Z", "event":"FuelScoop", "Scooped":0.276664, "Total":15.716187 } 143 | { "timestamp":"2017-10-17T02:45:59Z", "event":"Music", "MusicTrack":"SystemMap" } 144 | { "timestamp":"2017-10-17T02:46:06Z", "event":"Music", "MusicTrack":"Supercruise" } 145 | { "timestamp":"2017-10-17T02:49:27Z", "event":"SupercruiseExit", "StarSystem":"Ngolibardu", "Body":"Vernadsky Port", "BodyType":"Station" } 146 | { "timestamp":"2017-10-17T02:49:28Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 147 | { "timestamp":"2017-10-17T02:49:33Z", "event":"Music", "MusicTrack":"NoTrack" } 148 | { "timestamp":"2017-10-17T02:49:36Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 149 | { "timestamp":"2017-10-17T02:49:46Z", "event":"DockingRequested", "StationName":"Vernadsky Port" } 150 | { "timestamp":"2017-10-17T02:49:46Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$DockingChatter_Friendly;", "Message_Localised":"It's great to see you again, Commander.", "Channel":"npc" } 151 | { "timestamp":"2017-10-17T02:49:47Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 152 | { "timestamp":"2017-10-17T02:49:47Z", "event":"DockingGranted", "LandingPad":34, "StationName":"Vernadsky Port" } 153 | { "timestamp":"2017-10-17T02:50:48Z", "event":"Scanned", "ScanType":"Cargo" } 154 | { "timestamp":"2017-10-17T02:50:48Z", "event":"ReceiveText", "From":"$ShipName_Police_Federation;", "From_Localised":"Federal Security Service", "Message":"$Police_ThankYouPassedStopAndSearch07;", "Message_Localised":"Your scan is clear. Carry on with your journey, Commander.", "Channel":"npc" } 155 | { "timestamp":"2017-10-17T02:50:48Z", "event":"Music", "MusicTrack":"Starport" } 156 | { "timestamp":"2017-10-17T02:51:24Z", "event":"Docked", "StationName":"Vernadsky Port", "StationType":"Coriolis", "StarSystem":"Ngolibardu", "StationFaction":"Ngolibardu Electronics Solutions", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Refinery;", "StationEconomy_Localised":"Refinery", "DistFromStarLS":1933.718262 } 157 | { "timestamp":"2017-10-17T02:51:44Z", "event":"MissionCompleted", "Faction":"Ngolibardu Purple Drug Empire", "Name":"Mission_Courier_War_name", "MissionID":228701842, "TargetFaction":"Ngolibardu Purple Drug Empire", "DestinationSystem":"Ngolibardu", "DestinationStation":"Vernadsky Port", "Reward":5680 } 158 | { "timestamp":"2017-10-17T02:53:33Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Carl-Johan Kjellander;", "From_Localised":"Carl-Johan Kjellander", "Message":"$Commuter_AuthorityScan01;", "Message_Localised":"Is there a problem here, officer?", "Channel":"npc" } 159 | { "timestamp":"2017-10-17T02:53:58Z", "event":"MissionAccepted", "Faction":"Ngolibardu Electronics Solutions", "Name":"Mission_Salvage_RankFed", "LocalisedName":"Commercial Samples Federal Navy retrieval mission", "Commodity":"$ComercialSamples_Name;", "Commodity_Localised":"Commercial Samples", "Count":3, "DestinationSystem":"Wolf 865", "Expiry":"2017-10-18T06:49:31Z", "Influence":"Med", "Reputation":"Low", "Reward":106208, "MissionID":228705770 } 160 | { "timestamp":"2017-10-17T02:54:03Z", "event":"RefuelAll", "Cost":22, "Amount":0.415480 } 161 | { "timestamp":"2017-10-17T02:54:26Z", "event":"ModuleRetrieve", "Slot":"MediumHardpoint2", "RetrievedItem":"$hpt_multicannon_fixed_medium_name;", "RetrievedItem_Localised":"Multi-Cannon", "Ship":"cobramkiii", "ShipID":1 } 162 | { "timestamp":"2017-10-17T02:54:37Z", "event":"ModuleRetrieve", "Slot":"MediumHardpoint1", "RetrievedItem":"$hpt_cannon_gimbal_small_name;", "RetrievedItem_Localised":"Cannon", "Ship":"cobramkiii", "ShipID":1 } 163 | { "timestamp":"2017-10-17T02:54:47Z", "event":"ModuleRetrieve", "Slot":"SmallHardpoint1", "RetrievedItem":"$hpt_pulselaser_fixed_small_name;", "RetrievedItem_Localised":"Pulse Laser", "Ship":"cobramkiii", "ShipID":1 } 164 | { "timestamp":"2017-10-17T02:54:57Z", "event":"ModuleRetrieve", "Slot":"SmallHardpoint2", "RetrievedItem":"$hpt_pulselaser_fixed_small_name;", "RetrievedItem_Localised":"Pulse Laser", "Ship":"cobramkiii", "ShipID":1 } 165 | { "timestamp":"2017-10-17T02:55:13Z", "event":"ModuleRetrieve", "Slot":"TinyHardpoint1", "RetrievedItem":"$hpt_plasmapointdefence_turret_tiny_name;", "RetrievedItem_Localised":"Point Defence", "Ship":"cobramkiii", "ShipID":1 } 166 | { "timestamp":"2017-10-17T02:55:57Z", "event":"ModuleRetrieve", "Slot":"PowerDistributor", "RetrievedItem":"$int_powerdistributor_size3_class5_name;", "RetrievedItem_Localised":"Power Distributor", "Ship":"cobramkiii", "ShipID":1, "SwapOutItem":"$int_powerdistributor_size3_class2_name;", "SwapOutItem_Localised":"Power Distributor", "Cost":0 } 167 | { "timestamp":"2017-10-17T02:56:39Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Martin Wittek;", "From_Localised":"Martin Wittek", "Message":"$Commuter_AuthorityScan04;", "Message_Localised":"Is this really necessary?", "Channel":"npc" } 168 | { "timestamp":"2017-10-17T02:56:47Z", "event":"ModuleStore", "Slot":"Slot04_Size2", "StoredItem":"$int_fuelscoop_size2_class5_name;", "StoredItem_Localised":"Fuel Scoop", "Ship":"cobramkiii", "ShipID":1 } 169 | { "timestamp":"2017-10-17T02:56:54Z", "event":"ModuleStore", "Slot":"Slot02_Size4", "StoredItem":"$int_cargorack_size4_class1_name;", "StoredItem_Localised":"Cargo Rack", "Ship":"cobramkiii", "ShipID":1 } 170 | { "timestamp":"2017-10-17T02:57:13Z", "event":"ModuleStore", "Slot":"Slot06_Size2", "StoredItem":"$int_repairer_size2_class3_name;", "StoredItem_Localised":"AFM Unit", "Ship":"cobramkiii", "ShipID":1 } 171 | { "timestamp":"2017-10-17T03:00:19Z", "event":"Undocked", "StationName":"Vernadsky Port", "StationType":"Coriolis" } 172 | { "timestamp":"2017-10-17T03:00:59Z", "event":"Music", "MusicTrack":"NoTrack" } 173 | { "timestamp":"2017-10-17T03:01:30Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$STATION_NoFireZone_exited;", "Message_Localised":"No fire zone left.", "Channel":"npc" } 174 | { "timestamp":"2017-10-17T03:01:40Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Wolf 865", "StarClass":"K" } 175 | { "timestamp":"2017-10-17T03:01:59Z", "event":"FSDJump", "StarSystem":"Wolf 865", "StarPos":[-50.469,-9.063,34.656], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Industrial;", "SystemEconomy_Localised":"Industrial", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_high;", "SystemSecurity_Localised":"High Security", "Population":24209148, "JumpDist":5.572, "FuelUsed":0.271536, "FuelLevel":15.728464, "Factions":[ { "Name":"LP 455-12 Labour", "FactionState":"Outbreak", "Government":"Democracy", "Influence":0.158000, "Allegiance":"Federation", "RecoveringStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Revolutionary Wolf 865 Revolutionary Party", "FactionState":"None", "Government":"Democracy", "Influence":0.099000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Ekuru for Equality", "FactionState":"None", "Government":"Democracy", "Influence":0.054000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Wolf 865 Focus", "FactionState":"None", "Government":"Dictatorship", "Influence":0.064000, "Allegiance":"Independent", "RecoveringStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Wolf 865 Group", "FactionState":"Boom", "Government":"Corporate", "Influence":0.529000, "Allegiance":"Federation" }, { "Name":"Pirates of Wolf 865", "FactionState":"Boom", "Government":"Anarchy", "Influence":0.026000, "Allegiance":"Independent" }, { "Name":"Wolf 865 Liberty Party", "FactionState":"Boom", "Government":"Dictatorship", "Influence":0.070000, "Allegiance":"Independent" } ], "SystemFaction":"Wolf 865 Group", "FactionState":"Boom" } 176 | { "timestamp":"2017-10-17T03:01:59Z", "event":"Music", "MusicTrack":"DestinationFromHyperspace" } 177 | { "timestamp":"2017-10-17T03:02:04Z", "event":"Music", "MusicTrack":"Supercruise" } 178 | { "timestamp":"2017-10-17T03:04:27Z", "event":"SupercruiseExit", "StarSystem":"Wolf 865", "Body":"Wolf 865 A", "BodyType":"Star" } 179 | { "timestamp":"2017-10-17T03:04:27Z", "event":"Music", "MusicTrack":"Exploration" } 180 | { "timestamp":"2017-10-17T03:05:10Z", "event":"NavBeaconScan", "NumBodies":24 } 181 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A", "DistanceFromArrivalLS":0.000000, "StarType":"K", "StellarMass":0.808594, "Radius":609623744.000000, "AbsoluteMagnitude":5.906601, "Age_MY":9078, "SurfaceTemperature":5189.000000, "Luminosity":"V", "SemiMajorAxis":10387813564416.000000, "Eccentricity":0.008671, "OrbitalInclination":-43.180649, "Periapsis":214.379608, "OrbitalPeriod":125143375872.000000, "RotationPeriod":562461.687500, "AxialTilt":0.000000, "Rings":[ { "Name":"Wolf 865 A A Belt", "RingClass":"eRingClass_MetalRich", "MassMT":1.8379e+13, "InnerRad":9.796e+08, "OuterRad":2.2481e+09 }, { "Name":"Wolf 865 A B Belt", "RingClass":"eRingClass_Rocky", "MassMT":1.6418e+16, "InnerRad":7.2549e+09, "OuterRad":2.925e+11 } ] } 182 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A A Belt Cluster 2", "DistanceFromArrivalLS":4.450503 } 183 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 6 a", "DistanceFromArrivalLS":3004.684082, "TidalLock":false, "TerraformState":"", "PlanetClass":"Icy body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"major water geysers volcanism", "MassEM":0.003769, "Radius":1366015.625000, "SurfaceGravity":0.804957, "SurfaceTemperature":75.959801, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"sulphur", "Percent":26.526022 }, { "Name":"carbon", "Percent":22.305635 }, { "Name":"phosphorus", "Percent":14.280452 }, { "Name":"iron", "Percent":12.138479 }, { "Name":"nickel", "Percent":9.181037 }, { "Name":"chromium", "Percent":5.459077 }, { "Name":"selenium", "Percent":4.151547 }, { "Name":"germanium", "Percent":3.472878 }, { "Name":"cadmium", "Percent":0.942608 }, { "Name":"molybdenum", "Percent":0.792636 }, { "Name":"ruthenium", "Percent":0.749629 } ], "SemiMajorAxis":164510544.000000, "Eccentricity":0.000025, "OrbitalInclination":0.005080, "Periapsis":269.349609, "OrbitalPeriod":90235.210938, "RotationPeriod":-78251.875000, "AxialTilt":-2.159792 } 184 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A B Belt Cluster 1", "DistanceFromArrivalLS":237.092957 } 185 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 6", "DistanceFromArrivalLS":3004.392090, "TidalLock":false, "TerraformState":"", "PlanetClass":"Sudarsky class I gas giant", "Atmosphere":"", "AtmosphereComposition":[ { "Name":"Hydrogen", "Percent":74.217857 }, { "Name":"Helium", "Percent":25.782139 } ], "Volcanism":"", "MassEM":54.153793, "Radius":49333436.000000, "SurfaceGravity":8.868611, "SurfaceTemperature":86.852692, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":994913920.000000, "Eccentricity":0.007675, "OrbitalInclination":-8.952840, "Periapsis":237.714539, "OrbitalPeriod":61522392.000000, "RotationPeriod":156156.062500, "AxialTilt":0.147851, "Rings":[ { "Name":"Wolf 865 A 6 A Ring", "RingClass":"eRingClass_MetalRich", "MassMT":4.9613e+10, "InnerRad":8.14e+07, "OuterRad":9.2416e+07 }, { "Name":"Wolf 865 A 6 B Ring", "RingClass":"eRingClass_Icy", "MassMT":3.5528e+11, "InnerRad":9.2516e+07, "OuterRad":1.4665e+08 } ], "ReserveLevel":"DepletedResources" } 186 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 2 b", "DistanceFromArrivalLS":1559.381714, "TidalLock":false, "TerraformState":"", "PlanetClass":"Rocky body", "Atmosphere":"", "AtmosphereType":"SulphurDioxide", "AtmosphereComposition":[ { "Name":"SulphurDioxide", "Percent":90.000000 }, { "Name":"Silicates", "Percent":5.000000 }, { "Name":"Oxygen", "Percent":2.999999 } ], "Volcanism":"rocky magma volcanism", "MassEM":0.000790, "Radius":650170.375000, "SurfaceGravity":0.744868, "SurfaceTemperature":133.814972, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":547881344.000000, "Eccentricity":0.000089, "OrbitalInclination":-0.312739, "Periapsis":102.018867, "OrbitalPeriod":174025.375000, "RotationPeriod":-174026.250000, "AxialTilt":-2.959640 } 187 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 2 a", "DistanceFromArrivalLS":1560.832642, "TidalLock":true, "TerraformState":"", "PlanetClass":"Rocky body", "Atmosphere":"carbon dioxide atmosphere", "AtmosphereType":"CarbonDioxide", "AtmosphereComposition":[ { "Name":"CarbonDioxide", "Percent":98.990746 }, { "Name":"SulphurDioxide", "Percent":0.989908 } ], "Volcanism":"metallic magma volcanism", "MassEM":0.000450, "Radius":540226.375000, "SurfaceGravity":0.615248, "SurfaceTemperature":224.787262, "SurfacePressure":74305.304688, "Landable":false, "SemiMajorAxis":345780768.000000, "Eccentricity":0.000414, "OrbitalInclination":-0.229632, "Periapsis":210.826401, "OrbitalPeriod":87253.750000, "RotationPeriod":87254.187500, "AxialTilt":-0.306810 } 188 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A B Belt Cluster 4", "DistanceFromArrivalLS":84.693260 } 189 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 3", "DistanceFromArrivalLS":1589.467651, "TidalLock":true, "TerraformState":"", "PlanetClass":"Sudarsky class I gas giant", "Atmosphere":"", "AtmosphereComposition":[ { "Name":"Hydrogen", "Percent":74.217865 }, { "Name":"Helium", "Percent":25.782146 } ], "Volcanism":"", "MassEM":24.658531, "Radius":42252580.000000, "SurfaceGravity":5.505162, "SurfaceTemperature":119.163658, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":8689778688.000000, "Eccentricity":0.002806, "OrbitalInclination":-7.313694, "Periapsis":81.454422, "OrbitalPeriod":11496452.000000, "RotationPeriod":11757036.000000, "AxialTilt":-0.242899, "Rings":[ { "Name":"Wolf 865 A 3 A Ring", "RingClass":"eRingClass_Rocky", "MassMT":1.8298e+10, "InnerRad":6.8639e+07, "OuterRad":7.2929e+07 }, { "Name":"Wolf 865 A 3 B Ring", "RingClass":"eRingClass_Rocky", "MassMT":1.4913e+11, "InnerRad":7.3029e+07, "OuterRad":1.0133e+08 } ], "ReserveLevel":"DepletedResources" } 190 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 5", "DistanceFromArrivalLS":2081.432129, "TidalLock":false, "TerraformState":"", "PlanetClass":"Sudarsky class I gas giant", "Atmosphere":"", "AtmosphereComposition":[ { "Name":"Hydrogen", "Percent":74.217865 }, { "Name":"Helium", "Percent":25.782146 } ], "Volcanism":"", "MassEM":165.484970, "Radius":66066080.000000, "SurfaceGravity":15.111616, "SurfaceTemperature":117.221260, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":9892797440.000000, "Eccentricity":0.117505, "OrbitalInclination":0.497989, "Periapsis":350.480682, "OrbitalPeriod":32046524.000000, "RotationPeriod":4814548.500000, "AxialTilt":0.059476, "Rings":[ { "Name":"Wolf 865 A 5 A Ring", "RingClass":"eRingClass_Icy", "MassMT":6.4567e+11, "InnerRad":1.2641e+08, "OuterRad":1.9113e+08 } ], "ReserveLevel":"DepletedResources" } 191 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 5 a", "DistanceFromArrivalLS":2082.064453, "TidalLock":true, "TerraformState":"", "PlanetClass":"Icy body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"major water geysers volcanism", "MassEM":0.004023, "Radius":1398461.250000, "SurfaceGravity":0.819916, "SurfaceTemperature":95.781967, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"sulphur", "Percent":26.387199 }, { "Name":"carbon", "Percent":22.188900 }, { "Name":"phosphorus", "Percent":14.205715 }, { "Name":"iron", "Percent":11.884565 }, { "Name":"nickel", "Percent":8.988987 }, { "Name":"chromium", "Percent":5.344883 }, { "Name":"manganese", "Percent":4.908204 }, { "Name":"selenium", "Percent":4.129820 }, { "Name":"ruthenium", "Percent":0.733948 }, { "Name":"tin", "Percent":0.708798 }, { "Name":"mercury", "Percent":0.518979 } ], "SemiMajorAxis":193413216.000000, "Eccentricity":0.000066, "OrbitalInclination":0.030886, "Periapsis":219.197723, "OrbitalPeriod":65805.335938, "RotationPeriod":65806.757813, "AxialTilt":0.227259 } 192 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 B", "DistanceFromArrivalLS":128292.226563, "StarType":"M", "StellarMass":0.296875, "Radius":299017440.000000, "AbsoluteMagnitude":10.080856, "Age_MY":9078, "SurfaceTemperature":2631.000000, "Luminosity":"Va", "SemiMajorAxis":28405225488384.000000, "Eccentricity":0.008671, "OrbitalInclination":-43.180649, "Periapsis":34.379608, "OrbitalPeriod":125143375872.000000, "RotationPeriod":145279.125000, "AxialTilt":0.000000 } 193 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 1", "DistanceFromArrivalLS":14.362702, "TidalLock":true, "TerraformState":"", "PlanetClass":"Metal rich body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"", "MassEM":0.528581, "Radius":4010470.500000, "SurfaceGravity":13.098761, "SurfaceTemperature":1155.093384, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"iron", "Percent":34.777977 }, { "Name":"nickel", "Percent":26.304604 }, { "Name":"chromium", "Percent":15.640812 }, { "Name":"zinc", "Percent":9.451352 }, { "Name":"vanadium", "Percent":8.540260 }, { "Name":"niobium", "Percent":2.376889 }, { "Name":"tungsten", "Percent":1.909661 }, { "Name":"polonium", "Percent":0.998453 } ], "SemiMajorAxis":4305841664.000000, "Eccentricity":0.000004, "OrbitalInclination":0.000823, "Periapsis":334.959869, "OrbitalPeriod":171018.078125, "RotationPeriod":171356.546875, "AxialTilt":-0.467038 } 194 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A B Belt Cluster 2", "DistanceFromArrivalLS":535.746704 } 195 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A B Belt Cluster 3", "DistanceFromArrivalLS":372.091919 } 196 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 7", "DistanceFromArrivalLS":2988.461914, "TidalLock":false, "TerraformState":"", "PlanetClass":"Icy body", "Atmosphere":"thick methane rich atmosphere", "AtmosphereType":"MethaneRich", "AtmosphereComposition":[ { "Name":"Ammonia", "Percent":33.221291 }, { "Name":"Methane", "Percent":33.221291 }, { "Name":"Nitrogen", "Percent":33.221291 } ], "Volcanism":"major water geysers volcanism", "MassEM":4.457116, "Radius":12037930.000000, "SurfaceGravity":12.259133, "SurfaceTemperature":427.807068, "SurfacePressure":1885042432.000000, "Landable":false, "SemiMajorAxis":12089293824.000000, "Eccentricity":0.007675, "OrbitalInclination":-8.952840, "Periapsis":57.714535, "OrbitalPeriod":61522392.000000, "RotationPeriod":86075.218750, "AxialTilt":0.080231, "Rings":[ { "Name":"Wolf 865 A 7 A Ring", "RingClass":"eRingClass_Icy", "MassMT":4.014e+09, "InnerRad":1.9863e+07, "OuterRad":5.9714e+07 } ], "ReserveLevel":"DepletedResources" } 197 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 3 b", "DistanceFromArrivalLS":1589.432983, "TidalLock":false, "TerraformState":"", "PlanetClass":"Rocky body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"minor rocky magma volcanism", "MassEM":0.000352, "Radius":497636.406250, "SurfaceGravity":0.567002, "SurfaceTemperature":120.390610, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"iron", "Percent":19.753044 }, { "Name":"sulphur", "Percent":19.158863 }, { "Name":"carbon", "Percent":16.110619 }, { "Name":"nickel", "Percent":14.940375 }, { "Name":"phosphorus", "Percent":10.314295 }, { "Name":"manganese", "Percent":8.157806 }, { "Name":"germanium", "Percent":5.647391 }, { "Name":"selenium", "Percent":2.998524 }, { "Name":"niobium", "Percent":1.350015 }, { "Name":"mercury", "Percent":0.862583 }, { "Name":"technetium", "Percent":0.706487 } ], "SemiMajorAxis":203142656.000000, "Eccentricity":0.001660, "OrbitalInclination":0.009662, "Periapsis":12.737552, "OrbitalPeriod":183498.015625, "RotationPeriod":-183501.109375, "AxialTilt":-2.398958 } 198 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 2 c", "DistanceFromArrivalLS":1562.962280, "TidalLock":true, "TerraformState":"", "PlanetClass":"Rocky body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"", "MassEM":0.002380, "Radius":937377.125000, "SurfaceGravity":1.079809, "SurfaceTemperature":129.415833, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"iron", "Percent":18.447765 }, { "Name":"sulphur", "Percent":17.968889 }, { "Name":"carbon", "Percent":15.109976 }, { "Name":"nickel", "Percent":13.953116 }, { "Name":"phosphorus", "Percent":9.673666 }, { "Name":"chromium", "Percent":8.296573 }, { "Name":"manganese", "Percent":7.618740 }, { "Name":"germanium", "Percent":5.296628 }, { "Name":"cadmium", "Percent":1.432553 }, { "Name":"yttrium", "Percent":1.101863 }, { "Name":"tin", "Percent":1.100228 } ], "SemiMajorAxis":948834496.000000, "Eccentricity":0.002101, "OrbitalInclination":-0.003973, "Periapsis":167.144760, "OrbitalPeriod":396614.375000, "RotationPeriod":396616.406250, "AxialTilt":0.022759 } 199 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 4", "DistanceFromArrivalLS":2069.390625, "TidalLock":false, "TerraformState":"", "PlanetClass":"Sudarsky class I gas giant", "Atmosphere":"", "AtmosphereComposition":[ { "Name":"Hydrogen", "Percent":74.217865 }, { "Name":"Helium", "Percent":25.782145 } ], "Volcanism":"", "MassEM":254.378204, "Radius":70726152.000000, "SurfaceGravity":20.268856, "SurfaceTemperature":129.294739, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":6435780096.000000, "Eccentricity":0.117505, "OrbitalInclination":0.497989, "Periapsis":170.480682, "OrbitalPeriod":32046524.000000, "RotationPeriod":60672.480469, "AxialTilt":0.077130, "Rings":[ { "Name":"Wolf 865 A 4 A Ring", "RingClass":"eRingClass_Rocky", "MassMT":4.8332e+10, "InnerRad":1.0994e+08, "OuterRad":1.1672e+08 }, { "Name":"Wolf 865 A 4 B Ring", "RingClass":"eRingClass_Icy", "MassMT":1.1006e+12, "InnerRad":1.1682e+08, "OuterRad":2.2058e+08 } ], "ReserveLevel":"DepletedResources" } 200 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A A Belt Cluster 1", "DistanceFromArrivalLS":7.326874 } 201 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 2", "DistanceFromArrivalLS":1560.150513, "TidalLock":false, "TerraformState":"", "PlanetClass":"Sudarsky class II gas giant", "Atmosphere":"", "AtmosphereComposition":[ { "Name":"Hydrogen", "Percent":74.217857 }, { "Name":"Helium", "Percent":25.782139 } ], "Volcanism":"", "MassEM":537.862488, "Radius":76116952.000000, "SurfaceGravity":37.001377, "SurfaceTemperature":190.623138, "SurfacePressure":0.000000, "Landable":false, "SemiMajorAxis":398395968.000000, "Eccentricity":0.002806, "OrbitalInclination":-7.313694, "Periapsis":261.454437, "OrbitalPeriod":11496452.000000, "RotationPeriod":90098.453125, "AxialTilt":0.302248, "Rings":[ { "Name":"Wolf 865 A 2 A Ring", "RingClass":"eRingClass_Rocky", "MassMT":1.7478e+12, "InnerRad":1.4581e+08, "OuterRad":2.8312e+08 } ], "ReserveLevel":"DepletedResources" } 202 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A A Belt Cluster 3", "DistanceFromArrivalLS":6.286581 } 203 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 6 b", "DistanceFromArrivalLS":3004.017090, "TidalLock":true, "TerraformState":"", "PlanetClass":"Icy body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"major water geysers volcanism", "MassEM":0.000844, "Radius":834471.687500, "SurfaceGravity":0.483024, "SurfaceTemperature":75.802429, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"sulphur", "Percent":26.817122 }, { "Name":"carbon", "Percent":22.550423 }, { "Name":"phosphorus", "Percent":14.437167 }, { "Name":"iron", "Percent":12.078519 }, { "Name":"nickel", "Percent":9.135686 }, { "Name":"chromium", "Percent":5.432111 }, { "Name":"selenium", "Percent":4.197106 }, { "Name":"germanium", "Percent":3.467921 }, { "Name":"molybdenum", "Percent":0.788720 }, { "Name":"tungsten", "Percent":0.663232 }, { "Name":"technetium", "Percent":0.432000 } ], "SemiMajorAxis":204705424.000000, "Eccentricity":0.001502, "OrbitalInclination":0.038804, "Periapsis":62.940151, "OrbitalPeriod":125250.484375, "RotationPeriod":111855.257813, "AxialTilt":0.432905 } 204 | { "timestamp":"2017-10-17T03:05:10Z", "event":"Scan", "BodyName":"Wolf 865 A 3 a", "DistanceFromArrivalLS":1589.233276, "TidalLock":true, "TerraformState":"", "PlanetClass":"Rocky body", "Atmosphere":"", "AtmosphereType":"None", "Volcanism":"minor metallic magma volcanism", "MassEM":0.000419, "Radius":527358.625000, "SurfaceGravity":0.600384, "SurfaceTemperature":120.468018, "SurfacePressure":0.000000, "Landable":true, "Materials":[ { "Name":"iron", "Percent":19.660549 }, { "Name":"sulphur", "Percent":19.150194 }, { "Name":"carbon", "Percent":16.103331 }, { "Name":"nickel", "Percent":14.870416 }, { "Name":"phosphorus", "Percent":10.309629 }, { "Name":"manganese", "Percent":8.119605 }, { "Name":"germanium", "Percent":5.644836 }, { "Name":"arsenic", "Percent":2.524448 }, { "Name":"niobium", "Percent":1.343694 }, { "Name":"antimony", "Percent":1.193738 }, { "Name":"tungsten", "Percent":1.079562 } ], "SemiMajorAxis":149597984.000000, "Eccentricity":0.001579, "OrbitalInclination":-0.246184, "Periapsis":25.209635, "OrbitalPeriod":115962.687500, "RotationPeriod":115964.640625, "AxialTilt":0.100387 } 205 | { "timestamp":"2017-10-17T03:06:54Z", "event":"StartJump", "JumpType":"Supercruise" } 206 | { "timestamp":"2017-10-17T03:06:59Z", "event":"SupercruiseEntry", "StarSystem":"Wolf 865" } 207 | { "timestamp":"2017-10-17T03:06:59Z", "event":"Music", "MusicTrack":"Supercruise" } 208 | { "timestamp":"2017-10-17T03:10:37Z", "event":"SupercruiseExit", "StarSystem":"Wolf 865", "Body":"Wolf 865 A 3 a", "BodyType":"Planet" } 209 | { "timestamp":"2017-10-17T03:10:38Z", "event":"Music", "MusicTrack":"Exploration" } 210 | { "timestamp":"2017-10-17T03:14:03Z", "event":"Music", "MusicTrack":"SystemMap" } 211 | { "timestamp":"2017-10-17T03:14:11Z", "event":"Music", "MusicTrack":"Exploration" } 212 | { "timestamp":"2017-10-17T03:14:26Z", "event":"StartJump", "JumpType":"Supercruise" } 213 | { "timestamp":"2017-10-17T03:14:31Z", "event":"SupercruiseEntry", "StarSystem":"Wolf 865" } 214 | { "timestamp":"2017-10-17T03:14:31Z", "event":"Music", "MusicTrack":"Supercruise" } 215 | { "timestamp":"2017-10-17T03:16:03Z", "event":"USSDrop", "USSType":"$USS_Type_Salvage;", "USSType_Localised":"Degraded emissions detected", "USSThreat":0 } 216 | { "timestamp":"2017-10-17T03:16:04Z", "event":"SupercruiseExit", "StarSystem":"Wolf 865", "Body":"Wolf 865 A 3 a", "BodyType":"Planet" } 217 | { "timestamp":"2017-10-17T03:16:04Z", "event":"Music", "MusicTrack":"Exploration" } 218 | { "timestamp":"2017-10-17T03:18:03Z", "event":"CollectCargo", "Type":"ComercialSamples", "Stolen":false } 219 | { "timestamp":"2017-10-17T03:18:37Z", "event":"CollectCargo", "Type":"ComercialSamples", "Stolen":false } 220 | { "timestamp":"2017-10-17T03:18:51Z", "event":"CollectCargo", "Type":"HydrogenFuel", "Stolen":false } 221 | { "timestamp":"2017-10-17T03:19:13Z", "event":"CollectCargo", "Type":"ComercialSamples", "Stolen":false } 222 | { "timestamp":"2017-10-17T03:19:14Z", "event":"MissionRedirected", "MissionID":228705770, "Name":"Mission_Salvage_RankFed_name", "NewDestinationStation":"Vernadsky Port", "NewDestinationSystem":"Ngolibardu", "OldDestinationStation":"", "OldDestinationSystem":"Wolf 865" } 223 | { "timestamp":"2017-10-17T03:19:31Z", "event":"CollectCargo", "Type":"Scrap", "Stolen":false } 224 | { "timestamp":"2017-10-17T03:20:09Z", "event":"CollectCargo", "Type":"Biowaste", "Stolen":false } 225 | { "timestamp":"2017-10-17T03:21:30Z", "event":"StartJump", "JumpType":"Hyperspace", "StarSystem":"Ngolibardu", "StarClass":"M" } 226 | { "timestamp":"2017-10-17T03:21:36Z", "event":"Music", "MusicTrack":"NoTrack" } 227 | { "timestamp":"2017-10-17T03:21:49Z", "event":"FSDJump", "StarSystem":"Ngolibardu", "StarPos":[-48.813,-7.750,39.813], "SystemAllegiance":"Federation", "SystemEconomy":"$economy_Refinery;", "SystemEconomy_Localised":"Refinery", "SystemGovernment":"$government_Corporate;", "SystemGovernment_Localised":"Corporate", "SystemSecurity":"$SYSTEM_SECURITY_medium;", "SystemSecurity_Localised":"Medium Security", "Population":2461357, "JumpDist":5.572, "FuelUsed":0.286836, "FuelLevel":15.441628, "Factions":[ { "Name":"Pilots Federation Local Branch", "FactionState":"None", "Government":"Democracy", "Influence":0.000000, "Allegiance":"PilotsFederation" }, { "Name":"Ekuru for Equality", "FactionState":"War", "Government":"Democracy", "Influence":0.067000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ] }, { "Name":"Ngolibardu Electronics Solutions", "FactionState":"None", "Government":"Corporate", "Influence":0.427000, "Allegiance":"Federation", "RecoveringStates":[ { "State":"Boom", "Trend":0 } ] }, { "Name":"Ngolibardu Purple Drug Empire", "FactionState":"Boom", "Government":"Anarchy", "Influence":0.033000, "Allegiance":"Independent" }, { "Name":"Wolf 865 Group", "FactionState":"Boom", "Government":"Corporate", "Influence":0.225000, "Allegiance":"Federation" }, { "Name":"Liberals of Ngolibardu", "FactionState":"None", "Government":"Democracy", "Influence":0.086000, "Allegiance":"Independent" }, { "Name":"League of Ngolibardu", "FactionState":"None", "Government":"Dictatorship", "Influence":0.026000, "Allegiance":"Independent", "PendingStates":[ { "State":"Boom", "Trend":1 } ], "RecoveringStates":[ { "State":"War", "Trend":0 } ] }, { "Name":"Ross 163 Blue Dynamic Limited", "FactionState":"None", "Government":"Corporate", "Influence":0.098000, "Allegiance":"Federation", "PendingStates":[ { "State":"Boom", "Trend":1 } ], "RecoveringStates":[ { "State":"War", "Trend":0 } ] }, { "Name":"Ngolibardu Creative & Co", "FactionState":"War", "Government":"Corporate", "Influence":0.038000, "Allegiance":"Independent", "PendingStates":[ { "State":"Boom", "Trend":1 }, { "State":"Outbreak", "Trend":0 } ] } ], "SystemFaction":"Ngolibardu Electronics Solutions" } 228 | { "timestamp":"2017-10-17T03:21:49Z", "event":"Music", "MusicTrack":"Supercruise" } 229 | { "timestamp":"2017-10-17T03:25:15Z", "event":"SupercruiseExit", "StarSystem":"Ngolibardu", "Body":"Vernadsky Port", "BodyType":"Station" } 230 | { "timestamp":"2017-10-17T03:25:15Z", "event":"Music", "MusicTrack":"DestinationFromSupercruise" } 231 | { "timestamp":"2017-10-17T03:25:20Z", "event":"Music", "MusicTrack":"NoTrack" } 232 | { "timestamp":"2017-10-17T03:25:24Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$STATION_NoFireZone_entered;", "Message_Localised":"No fire zone entered.", "Channel":"npc" } 233 | { "timestamp":"2017-10-17T03:25:37Z", "event":"DockingRequested", "StationName":"Vernadsky Port" } 234 | { "timestamp":"2017-10-17T03:25:37Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$DockingChatter_Friendly;", "Message_Localised":"It's great to see you again, Commander.", "Channel":"npc" } 235 | { "timestamp":"2017-10-17T03:25:38Z", "event":"ReceiveText", "From":"Vernadsky Port", "Message":"$STATION_docking_granted;", "Message_Localised":"Docking request granted.", "Channel":"npc" } 236 | { "timestamp":"2017-10-17T03:25:38Z", "event":"DockingGranted", "LandingPad":6, "StationName":"Vernadsky Port" } 237 | { "timestamp":"2017-10-17T03:26:19Z", "event":"Scanned", "ScanType":"Cargo" } 238 | { "timestamp":"2017-10-17T03:26:25Z", "event":"Music", "MusicTrack":"Starport" } 239 | { "timestamp":"2017-10-17T03:26:53Z", "event":"Docked", "StationName":"Vernadsky Port", "StationType":"Coriolis", "StarSystem":"Ngolibardu", "StationFaction":"Ngolibardu Electronics Solutions", "StationGovernment":"$government_Corporate;", "StationGovernment_Localised":"Corporate", "StationAllegiance":"Federation", "StationServices":[ "Dock", "Autodock", "Commodities", "Contacts", "Exploration", "Missions", "Outfitting", "CrewLounge", "Rearm", "Refuel", "Repair", "Shipyard", "Tuning", "MissionsGenerated", "FlightController", "StationOperations", "Powerplay", "SearchAndRescue" ], "StationEconomy":"$economy_Refinery;", "StationEconomy_Localised":"Refinery", "DistFromStarLS":1933.756836 } 240 | { "timestamp":"2017-10-17T03:27:10Z", "event":"MissionCompleted", "Faction":"Ngolibardu Electronics Solutions", "Name":"Mission_Salvage_RankFed_name", "MissionID":228705770, "Commodity":"$ComercialSamples_Name;", "Commodity_Localised":"Commercial Samples", "Count":3, "DestinationSystem":"Wolf 865", "Reward":106208 } 241 | { "timestamp":"2017-10-17T03:27:10Z", "event":"Promotion", "Federation":1 } 242 | { "timestamp":"2017-10-17T03:27:23Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Shiro Hagen;", "From_Localised":"Shiro Hagen", "Message":"$Commuter_HostileScan02;", "Message_Localised":"It is an offence to scan this vessel unless you are an authorised member of system security.", "Channel":"npc" } 243 | { "timestamp":"2017-10-17T03:28:53Z", "event":"RefuelAll", "Cost":49, "Amount":0.963160 } 244 | { "timestamp":"2017-10-17T03:29:15Z", "event":"MarketSell", "Type":"hydrogenfuel", "Count":1, "SellPrice":95, "TotalSale":95, "AvgPricePaid":0 } 245 | { "timestamp":"2017-10-17T03:29:39Z", "event":"MarketSell", "Type":"articulationmotors", "Count":4, "SellPrice":4567, "TotalSale":18268, "AvgPricePaid":0 } 246 | { "timestamp":"2017-10-17T03:29:54Z", "event":"MarketSell", "Type":"scrap", "Count":1, "SellPrice":67, "TotalSale":67, "AvgPricePaid":0 } 247 | { "timestamp":"2017-10-17T03:29:58Z", "event":"MarketSell", "Type":"biowaste", "Count":1, "SellPrice":10, "TotalSale":10, "AvgPricePaid":0 } 248 | { "timestamp":"2017-10-17T03:31:49Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Shiro Hagen;", "From_Localised":"Shiro Hagen", "Message":"$Commuter_HostileScan03;", "Message_Localised":"We have no valuable cargo onboard, please halt your scan.", "Channel":"npc" } 249 | { "timestamp":"2017-10-17T03:42:23Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Alexander Saunders;", "From_Localised":"Alexander Saunders", "Message":"$Commuter_AuthorityScan01;", "Message_Localised":"Is there a problem here, officer?", "Channel":"npc" } 250 | { "timestamp":"2017-10-17T03:46:31Z", "event":"ReceiveText", "From":"$npc_name_decorate:#name=Warren;", "From_Localised":"Warren", "Message":"$Commuter_AuthorityScan03;", "Message_Localised":"Will this take long? I’ve got a schedule to keep. ", "Channel":"npc" } 251 | { "timestamp":"2017-10-17T03:50:24Z", "event":"Music", "MusicTrack":"Exploration" } 252 | --------------------------------------------------------------------------------