From 4c70806a5a02df536ab432bbc6708ed7c5954b94 Mon Sep 17 00:00:00 2001 From: Matthew Turney Date: Sun, 14 Jun 2020 09:55:34 -0500 Subject: [PATCH] Support SLEF import format for importing builds. Inara uses the [SLEF] format to export builds. This format is mostly just a wrapper around the standard journal loadout format and includes support for source app metadata and exporting of multiple loadouts at one time. This change adds support for this format in the manual importer. Eventually it would be good to support this in the import route as well so Inara (or any other apps) can link directly to coriolis. [SLEF]: https://inara.cz/inara-impexp-slef/ --- __tests__/fixtures/slef-multiple-builds.json | 366 ++++++++++++++++++ .../slef-multiple-expected-builds.json | 8 + __tests__/fixtures/slef-single-build.json | 188 +++++++++ __tests__/test-import.js | 39 +- src/app/components/ModalImport.jsx | 39 +- 5 files changed, 633 insertions(+), 7 deletions(-) create mode 100644 __tests__/fixtures/slef-multiple-builds.json create mode 100644 __tests__/fixtures/slef-multiple-expected-builds.json create mode 100644 __tests__/fixtures/slef-single-build.json diff --git a/__tests__/fixtures/slef-multiple-builds.json b/__tests__/fixtures/slef-multiple-builds.json new file mode 100644 index 00000000..d6b95126 --- /dev/null +++ b/__tests__/fixtures/slef-multiple-builds.json @@ -0,0 +1,366 @@ +[ + { + "header": { + "appName": "Inara", + "appVersion": "1.0", + "appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/", + "appCustomProperties": { + "inaraCommanderID": 123, + "inaraShipID": 123 + } + }, + "data": { + "Ship": "krait_mkii", + "ShipID": 7, + "ShipName": "pancake hammer", + "ShipIdent": "PH-01", + "HullValue": 44160710, + "ModulesValue": 111274094, + "Rebuy": 7771743, + "Modules": [ + { + "Slot": "largehardpoint1", + "Item": "hpt_mininglaser_fixed_small", + "On": true + }, + { + "Slot": "largehardpoint2", + "Item": "hpt_cannon_gimbal_large", + "On": true, + "Engineering": { + "BlueprintName": "weapon_overcharged", + "Level": 2, + "Quality": 1, + "ExperimentalEffect": "special_auto_loader" + } + }, + { + "Slot": "largehardpoint3", + "Item": "hpt_cannon_gimbal_large", + "On": true, + "Engineering": { + "BlueprintName": "weapon_overcharged", + "Level": 2, + "Quality": 1, + "ExperimentalEffect": "special_auto_loader" + } + }, + { + "Slot": "mediumhardpoint1", + "Item": "hpt_basicmissilerack_fixed_medium", + "On": true, + "Engineering": { + "BlueprintName": "weapon_highcapacity", + "Level": 5, + "Quality": 1 + } + }, + { + "Slot": "mediumhardpoint2", + "Item": "hpt_basicmissilerack_fixed_medium", + "On": true + }, + { + "Slot": "tinyhardpoint1", + "Item": "hpt_heatsinklauncher_turret_tiny", + "On": true + }, + { + "Slot": "tinyhardpoint2", + "Item": "hpt_cloudscanner_size0_class3", + "On": true + }, + { + "Slot": "tinyhardpoint3", + "Item": "hpt_shieldbooster_size0_class5", + "On": true + }, + { + "Slot": "tinyhardpoint4", + "Item": "hpt_shieldbooster_size0_class5", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot01_size6", + "Item": "int_cargorack_size6_class1", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot02_size6", + "Item": "int_cargorack_size6_class1", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot03_size5", + "Item": "int_guardianfsdbooster_size5", + "On": true + }, + { + "Slot": "slot04_size5", + "Item": "int_fighterbay_size5_class1", + "On": true + }, + { + "Slot": "slot05_size4", + "Item": "int_shieldgenerator_size4_class5", + "On": true + }, + { + "Slot": "slot06_size3", + "Item": "int_dronecontrol_collection_size3_class4", + "On": true + }, + { + "Slot": "slot07_size3", + "Item": "int_dronecontrol_collection_size3_class4", + "On": true + }, + { + "Slot": "slot08_size2", + "Item": "int_refinery_size2_class2", + "On": true + }, + { + "Slot": "slot09_size1", + "Item": "int_dronecontrol_prospector_size1_class4", + "On": true + }, + { + "Slot": "powerplant", + "Item": "int_powerplant_size7_class5", + "On": true, + "Priority": 1 + }, + { + "Slot": "mainengines", + "Item": "int_engine_size6_class5", + "On": true + }, + { + "Slot": "frameshiftdrive", + "Item": "int_hyperdrive_size5_class5", + "On": true, + "Engineering": { + "BlueprintName": "fsd_longrange", + "Level": 2, + "Quality": 0.861 + } + }, + { + "Slot": "lifesupport", + "Item": "int_lifesupport_size4_class2", + "On": true, + "Priority": 3 + }, + { + "Slot": "powerdistributor", + "Item": "int_powerdistributor_size7_class5", + "On": true + }, + { + "Slot": "radar", + "Item": "int_sensors_size6_class2", + "On": true + }, + { + "Slot": "fueltank", + "Item": "int_fueltank_size5_class3", + "On": true, + "Priority": 1 + }, + { + "Slot": "armour", + "Item": "krait_mkii_armour_grade3", + "On": true, + "Priority": 1, + "Engineering": { + "BlueprintName": "armour_heavyduty", + "Level": 5, + "Quality": 1 + } + } + ] + } + }, + { + "header": { + "appName": "Inara", + "appVersion": "1.0", + "appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/", + "appCustomProperties": { + "inaraCommanderID": 123, + "inaraShipID": 123 + } + }, + "data": { + "Ship": "diamondbackxl", + "ShipID": 11, + "ShipName": "star Hopper", + "ShipIdent": "PH-02", + "HullValue": 1615649, + "ModulesValue": 16981039, + "Rebuy": 929837, + "Modules": [ + { + "Slot": "tinyhardpoint1", + "Item": "hpt_heatsinklauncher_turret_tiny", + "On": true, + "Value": 3072 + }, + { + "Slot": "slot01_size4", + "Item": "int_fuelscoop_size4_class5", + "On": true, + "Priority": 3, + "Value": 2862364 + }, + { + "Slot": "slot02_size4", + "Item": "int_guardianfsdbooster_size4", + "On": true, + "Value": 2847499 + }, + { + "Slot": "slot03_size3", + "Item": "int_shieldgenerator_size3_class2", + "On": true, + "Value": 18812, + "Engineering": { + "BlueprintName": "shieldgenerator_thermic", + "Level": 3, + "Quality": 1, + "ExperimentalEffect": "special_shield_health" + } + }, + { + "Slot": "slot04_size3", + "Item": "int_repairer_size3_class5", + "On": true, + "Value": 2302911 + }, + { + "Slot": "slot05_size2", + "Item": "int_buggybay_size2_class2", + "On": true, + "Priority": 3, + "Value": 21600 + }, + { + "Slot": "slot06_size2", + "Item": "int_cargorack_size2_class1", + "On": true, + "Priority": 1, + "Value": 2852 + }, + { + "Slot": "slot07_size1", + "Item": "int_supercruiseassist", + "On": true, + "Priority": 3, + "Value": 9121 + }, + { + "Slot": "slot08_size1", + "Item": "int_detailedsurfacescanner_tiny", + "On": true, + "Value": 250000, + "Engineering": { + "BlueprintName": "sensor_expanded", + "Level": 5, + "Quality": 1 + } + }, + { + "Slot": "powerplant", + "Item": "int_powerplant_size4_class5", + "On": true, + "Priority": 1, + "Value": 1441233, + "Engineering": { + "BlueprintName": "powerplant_boosted", + "Level": 1, + "Quality": 1 + } + }, + { + "Slot": "mainengines", + "Item": "int_engine_size4_class5", + "On": true, + "Value": 1610080, + "Engineering": { + "BlueprintName": "engine_dirty", + "Level": 5, + "Quality": 1, + "ExperimentalEffect": "special_engine_lightweight" + } + }, + { + "Slot": "frameshiftdrive", + "Item": "int_hyperdrive_size5_class5", + "On": true, + "Value": 5103953, + "Engineering": { + "BlueprintName": "fsd_longrange", + "Level": 5, + "Quality": 1, + "ExperimentalEffect": "special_fsd_lightweight" + } + }, + { + "Slot": "lifesupport", + "Item": "int_lifesupport_size3_class2", + "On": true, + "Value": 10133, + "Engineering": { + "BlueprintName": "misc_lightweight", + "Level": 3, + "Quality": 1 + } + }, + { + "Slot": "powerdistributor", + "Item": "int_powerdistributor_size4_class5", + "On": true, + "Value": 389022, + "Engineering": { + "BlueprintName": "powerdistributor_highfrequency", + "Level": 4, + "Quality": 1 + } + }, + { + "Slot": "radar", + "Item": "int_sensors_size3_class2", + "On": true, + "Value": 10133, + "Engineering": { + "BlueprintName": "sensor_lightweight", + "Level": 5, + "Quality": 1 + } + }, + { + "Slot": "fueltank", + "Item": "int_fueltank_size5_class3", + "On": true, + "Priority": 1, + "Value": 97754 + }, + { + "Slot": "armour", + "Item": "diamondbackxl_armour_grade1", + "On": true, + "Priority": 1, + "Engineering": { + "BlueprintName": "armour_heavyduty", + "Level": 5, + "Quality": 1 + } + } + ] + } + } +] diff --git a/__tests__/fixtures/slef-multiple-expected-builds.json b/__tests__/fixtures/slef-multiple-expected-builds.json new file mode 100644 index 00000000..19c2bd7f --- /dev/null +++ b/__tests__/fixtures/slef-multiple-expected-builds.json @@ -0,0 +1,8 @@ +{ + "krait_mkii": { + "Imported pancake hammer": "A2pptkFflidussf52l1o1o2g2g020g040405051Ofr45C9C91oP3.Iw18eQ==.AwRgzKIkA===." + }, + "diamondback_explorer": { + "Imported star Hopper": "A0pataFflddfsdf5---02---321P430iv6013w2i.Iw18SQ==.AwRm44GYpKg=." + } +} diff --git a/__tests__/fixtures/slef-single-build.json b/__tests__/fixtures/slef-single-build.json new file mode 100644 index 00000000..1b578b83 --- /dev/null +++ b/__tests__/fixtures/slef-single-build.json @@ -0,0 +1,188 @@ +[ + { + "header": { + "appName": "Inara", + "appVersion": "1.0", + "appURL": "https:\/\/inara.cz\/cmdr-fleet\/123\/123\/", + "appCustomProperties": { + "inaraCommanderID": 123, + "inaraShipID": 123 + } + }, + "data": { + "Ship": "krait_mkii", + "ShipID": 7, + "ShipName": "pancake hammer", + "ShipIdent": "PH-01", + "HullValue": 44160710, + "ModulesValue": 111274094, + "Rebuy": 7771743, + "Modules": [ + { + "Slot": "largehardpoint1", + "Item": "hpt_mininglaser_fixed_small", + "On": true + }, + { + "Slot": "largehardpoint2", + "Item": "hpt_cannon_gimbal_large", + "On": true, + "Engineering": { + "BlueprintName": "weapon_overcharged", + "Level": 2, + "Quality": 1, + "ExperimentalEffect": "special_auto_loader" + } + }, + { + "Slot": "largehardpoint3", + "Item": "hpt_cannon_gimbal_large", + "On": true, + "Engineering": { + "BlueprintName": "weapon_overcharged", + "Level": 2, + "Quality": 1, + "ExperimentalEffect": "special_auto_loader" + } + }, + { + "Slot": "mediumhardpoint1", + "Item": "hpt_basicmissilerack_fixed_medium", + "On": true, + "Engineering": { + "BlueprintName": "weapon_highcapacity", + "Level": 5, + "Quality": 1 + } + }, + { + "Slot": "mediumhardpoint2", + "Item": "hpt_basicmissilerack_fixed_medium", + "On": true + }, + { + "Slot": "tinyhardpoint1", + "Item": "hpt_heatsinklauncher_turret_tiny", + "On": true + }, + { + "Slot": "tinyhardpoint2", + "Item": "hpt_cloudscanner_size0_class3", + "On": true + }, + { + "Slot": "tinyhardpoint3", + "Item": "hpt_shieldbooster_size0_class5", + "On": true + }, + { + "Slot": "tinyhardpoint4", + "Item": "hpt_shieldbooster_size0_class5", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot01_size6", + "Item": "int_cargorack_size6_class1", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot02_size6", + "Item": "int_cargorack_size6_class1", + "On": true, + "Priority": 1 + }, + { + "Slot": "slot03_size5", + "Item": "int_guardianfsdbooster_size5", + "On": true + }, + { + "Slot": "slot04_size5", + "Item": "int_fighterbay_size5_class1", + "On": true + }, + { + "Slot": "slot05_size4", + "Item": "int_shieldgenerator_size4_class5", + "On": true + }, + { + "Slot": "slot06_size3", + "Item": "int_dronecontrol_collection_size3_class4", + "On": true + }, + { + "Slot": "slot07_size3", + "Item": "int_dronecontrol_collection_size3_class4", + "On": true + }, + { + "Slot": "slot08_size2", + "Item": "int_refinery_size2_class2", + "On": true + }, + { + "Slot": "slot09_size1", + "Item": "int_dronecontrol_prospector_size1_class4", + "On": true + }, + { + "Slot": "powerplant", + "Item": "int_powerplant_size7_class5", + "On": true, + "Priority": 1 + }, + { + "Slot": "mainengines", + "Item": "int_engine_size6_class5", + "On": true + }, + { + "Slot": "frameshiftdrive", + "Item": "int_hyperdrive_size5_class5", + "On": true, + "Engineering": { + "BlueprintName": "fsd_longrange", + "Level": 2, + "Quality": 0.861 + } + }, + { + "Slot": "lifesupport", + "Item": "int_lifesupport_size4_class2", + "On": true, + "Priority": 3 + }, + { + "Slot": "powerdistributor", + "Item": "int_powerdistributor_size7_class5", + "On": true + }, + { + "Slot": "radar", + "Item": "int_sensors_size6_class2", + "On": true + }, + { + "Slot": "fueltank", + "Item": "int_fueltank_size5_class3", + "On": true, + "Priority": 1 + }, + { + "Slot": "armour", + "Item": "krait_mkii_armour_grade3", + "On": true, + "Priority": 1, + "Engineering": { + "BlueprintName": "armour_heavyduty", + "Level": 5, + "Quality": 1 + } + } + ] + } + } +] diff --git a/__tests__/test-import.js b/__tests__/test-import.js index 89e77f6b..c7c484cc 100644 --- a/__tests__/test-import.js +++ b/__tests__/test-import.js @@ -206,7 +206,7 @@ describe('Import Modal', function() { }); }); - describe('Import Detaild Builds Array', function() { + describe('Import Detailed Builds Array', function() { beforeEach(reset); @@ -328,4 +328,41 @@ describe('Import Modal', function() { }); }); + describe('Imports SLEF data', () => { + beforeEach(reset); + + it('imports a single valid SLEF build', () => { + const importData = require('./fixtures/slef-single-build.json'); + pasteText(JSON.stringify(importData)); + + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + expect(modal.state.singleBuild).toBe(true); + clickProceed(); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/krait_mkii?code=A2pptkFflidussf52l1o1o2g2g020g040405051Ofr45C9C91oP3.Iw18eQ%3D%3D.AwRgzKIkA%3D%3D%3D.&bn=Imported%20pancake%20hammer'); + }); + + it('imports multiple SLEF builds', () => { + const importData = require('./fixtures/slef-multiple-builds.json'); + const expectedBuilds = require('./fixtures/slef-multiple-expected-builds.json'); + pasteText(JSON.stringify(importData)); + + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + expect(modal.state.singleBuild).toBe(false); + clickProceed(); + expect(modal.state.processed).toBeTruthy(); + clickImport(); + + const builds = Persist.getBuilds(); + + for (const shipModel in builds) { + for (const buildName in builds[shipModel]) { + expect(builds[shipModel][buildName]) + .toEqual(expectedBuilds[shipModel][buildName]); + } + } + }); + }); }); diff --git a/src/app/components/ModalImport.jsx b/src/app/components/ModalImport.jsx index cf48e138..4dc34b7c 100644 --- a/src/app/components/ModalImport.jsx +++ b/src/app/components/ModalImport.jsx @@ -11,7 +11,8 @@ import * as ModuleUtils from '../shipyard/ModuleUtils'; import { fromDetailedBuild } from '../shipyard/Serializer'; import { Download } from './SvgIcons'; import { outfitURL } from '../utils/UrlGenerators'; -import * as CompanionApiUtils from '../utils/CompanionApiUtils'; +import { shipFromJson, shipModelFromJson } from '../utils/CompanionApiUtils'; +import { shipFromLoadoutJSON } from '../utils/JournalUtils'; const zlib = require('pako'); @@ -214,8 +215,8 @@ export default class ModalImport extends TranslatedComponent { * @throws {string} if parse/import fails */ _importCompanionApiBuild(build) { - const shipModel = CompanionApiUtils.shipModelFromJson(build); - const ship = CompanionApiUtils.shipFromJson(build); + const shipModel = shipModelFromJson(build); + const ship = shipFromJson(build); let builds = {}; builds[shipModel] = {}; @@ -321,6 +322,30 @@ export default class ModalImport extends TranslatedComponent { this.setState({ builds, singleBuild: true }); } + /** + * Import SLEF formatted builds. Sets state to a map of the builds on success + * and flags if there was only a single build. + * + * @param {string} importData - Array of the list of builds. + * @throws {string} If parse / import fails + */ + _importSlefBuilds(importData) { + const builds = importData.reduce((memo, { data }) => { + const shipModel = shipModelFromJson(data); + const ship = shipFromLoadoutJSON(data); + const shipTemplate = Ships[shipModel]; + const shipName = data.ShipName || shipTemplate.properties.name; + + const key = `Imported ${shipName}`; + memo[shipModel] = {}; + memo[shipModel][key] = ship.toString(); + + return memo; + }, {}); + + this.setState({ builds, singleBuild: Object.keys(builds).length === 1 }); + } + /** * Validate the import string / text box contents * @param {SyntheticEvent} event Event @@ -355,8 +380,10 @@ export default class ModalImport extends TranslatedComponent { throw 'Must be an object or array!'; } - if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information - this._importCompanionApiBuild(importData); // Single sihp definition + if (importData?.[0]?.header?.appName) { // has SLEF envelope? + this._importSlefBuilds(importData); + } else if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information + this._importCompanionApiBuild(importData); // Single ship definition } else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information this._importCompanionApiBuild(importData.ship); // Complete API dump } else if (importData instanceof Array) { // Must be detailed export json @@ -542,7 +569,7 @@ export default class ModalImport extends TranslatedComponent { {comparisonRows} - ); + ); } if(this.state.canEdit) {