const fse = require('fs-extra') const nsg = require('node-sprite-generator') const Jimp = require('jimp') const util = require('util') //const factorioDirectory = 'C:/SteamLibrary/steamapps/common/Factorio/data/' const factorioDirectory = 'C:/_Programs/Steam/steamapps/common/Factorio/data/' const outDir = '../src/bundles/' const spritesheetsOutDir = '../src/spritesheets/' function nameMapping(imagePath) { const sP = imagePath.split('/') return sP.splice(sP.length - 2).join('/').split('.')[0] } let rawData = JSON.parse(fse.readFileSync('./temp.json').toString() .replace(/"(up|down|left|right|north|south|west|east)"/g, function(match, capture) { if (capture === 'north' || capture === 'up') return '"0"' if (capture === 'east' || capture === 'left') return '"2"' if (capture === 'south' || capture === 'down') return '"4"' if (capture === 'west' || capture === 'right') return '"6"' })) let tiles = {} for (const k in rawData.tile) { if (rawData.tile[k].minable) tiles[k] = rawData.tile[k] } console.log('Tiles: ' + Object.keys(tiles).length) fse.writeFileSync(outDir + 'tileBundle.json', JSON.stringify(tiles, null, 2).replace(/__base__|__core__/g, 'factorio-data')) console.log('Recipes: ' + Object.keys(rawData.recipe).length) fse.writeFileSync(outDir + 'recipeBundle.json', JSON.stringify(rawData.recipe, null, 2).replace(/__base__|__core__/g, 'factorio-data')) let inventory = [] let items = {} let placeableEntities = ['curved-rail'] const blacklistedGroups = [ 'environment', 'enemies', 'other' ] for (const k in rawData['item-group']) { const group = rawData['item-group'][k] if (!blacklistedGroups.includes(group.name)) { group.subgroups = [] inventory.push(group) } } for (const k in rawData['item-subgroup']) { const subgroup = rawData['item-subgroup'][k] subgroup.items = [] for (const group of inventory) { if (group.name === subgroup.group) { group.subgroups.push(subgroup) break } } } function findAllItems(data) { if (data.constructor === Object) { if (data.hasOwnProperty('subgroup')) { addItem(data) } else { for (const k in data) { if (data.hasOwnProperty(k)) { findAllItems(data[k]) } } } } } findAllItems(rawData) for (const k in rawData['fluid']) { const fluid = rawData['fluid'][k] fluid.subgroup = 'fluid' addItem(fluid) } function addItem(item) { if ((item.flags && item.flags.includes('hidden')) || !(item.icon || item.icons) || !item.order || item.collision_box) return for (let j = 0; j < inventory.length; j++) { for (let k = 0; k < inventory[j].subgroups.length; k++) { if (inventory[j].subgroups[k].name === item.subgroup) { inventory[j].subgroups[k].items.push(item) if (item.place_result) placeableEntities.push(item.place_result) items[item.name] = item return } } } } console.log('Items: ' + Object.keys(items).length) fse.writeFileSync(outDir + 'itemBundle.json', JSON.stringify(items, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) { return '"icon:' + nameMapping(capture) + '"' })) // sort and remove extra info from inventoryBundle inventory.sort(sortByOrder) for (let i = 0; i < inventory.length; i++) { inventory[i].subgroups.sort(sortByOrder) for (let j = 0; j < inventory[i].subgroups.length; j++) { inventory[i].subgroups[j].items.sort(sortByOrder) for (let k = 0; k < inventory[i].subgroups[j].items.length; k++) { removeExtraInfo(inventory[i].subgroups[j].items[k]) } removeExtraInfo(inventory[i].subgroups[j]) } removeExtraInfo(inventory[i]) } function sortByOrder(a, b) { // https://forums.factorio.com/viewtopic.php?f=25&t=3236#p23818 // https://forums.factorio.com/viewtopic.php?f=25&t=24163#p152955 if (a.order < b.order) return -1 if (a.order > b.order) return 1 return 0 } function removeExtraInfo(obj) { for (const k of Object.keys(obj)) { if (!['subgroups', 'items', 'name', 'icon', 'icons'].includes(k)) delete obj[k] } } fse.writeFileSync(outDir + 'inventoryBundle.json', JSON.stringify(inventory, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) { return '"icon:' + nameMapping(capture) + '"' })) let paths = [] for (let i = 0, l = inventory.length; i < l; i++) { paths.push(factorioDirectory + inventory[i].icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core')) for (let j = 0, l2 = inventory[i].subgroups.length; j < l2; j++) { for (let k = 0, l3 = inventory[i].subgroups[j].items.length; k < l3; k++) { const item = inventory[i].subgroups[j].items[k] if (item.icon) { paths.push(factorioDirectory + item.icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core')) } else { for (let l = 0; l < item.icons.length; l++) { paths.push(factorioDirectory + item.icons[l].icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core')) } } } } } paths = Array.from(new Set(paths).values()) console.log('Icon sprites: ' + paths.length) nsg({ src: paths, spritePath: spritesheetsOutDir + 'iconSpritesheet.png', stylesheet: './json-icon.tpl', stylesheetPath: spritesheetsOutDir + 'iconSpritesheet.json', stylesheetOptions: { prefix: 'icon:', nameMapping: nameMapping }, compositor: 'jimp', layout: 'packed', layoutOptions: { padding: 2 } }, function(err) { if (err) console.log(err) else console.log('Icon sprite atlas generated!') }) let entities = {} function findAllEntities(data) { if (data.constructor === Object) { if (placeableEntities.includes(data.name) && data.hasOwnProperty('collision_box') && (!data.flags.includes('placeable-off-grid') || data.name === 'land-mine')) { entities[data.name] = data } else { for (let k in data) { if (data.hasOwnProperty(k)) { findAllEntities(data[k]) } } } } } findAllEntities(rawData) const regexNameMatches = [ 'combinator', 'underground-belt', 'transport-belt', 'splitter', 'inserter', 'turret', 'mining-drill', 'pump' ] let nameMatches = [ 'assembling-machine-2', 'assembling-machine-3', 'pipe-to-ground', 'oil-refinery', 'chemical-plant', 'heat-exchanger', 'boiler', 'train-stop' ] for (let k in entities) { // Size const box = entities[k].selection_box entities[k].size = { width: Math.ceil(Math.abs(box[0][0]) + Math.abs(box[1][0])), height: Math.ceil(Math.abs(box[0][1]) + Math.abs(box[1][1])) } // Move out splitters and underground-belts from transport-belt fast_replaceable_group if (k.search('splitter') !== -1) { entities[k].fast_replaceable_group = 'splitter' } if (k.search('underground-belt') !== -1) { entities[k].fast_replaceable_group = 'underground-belt' } // Possible Rotations for (let j = 0; j < regexNameMatches.length; j++) { if (k.includes(regexNameMatches[j])) { nameMatches.push(k) } } } // Actual land size of the offshore pump entities['offshore-pump'].size = { width: 1, height: 1 } for (let i = 0; i < nameMatches.length; i++) { entities[nameMatches[i]].possible_rotations = [0, 2, 4, 6] } entities['storage-tank'].possible_rotations = [0, 2] entities['gate'].possible_rotations = [0, 2] entities['steam-engine'].possible_rotations = [0, 2] entities['steam-turbine'].possible_rotations = [0, 2] entities['straight-rail'].possible_rotations = [0, 2] entities['rail-signal'].possible_rotations = [0, 1, 2, 3, 4, 5, 6, 7] entities['rail-chain-signal'].possible_rotations = [0, 1, 2, 3, 4, 5, 6, 7] // End Possible Rotations // switch dir 2 and 6 for pipe-to-ground let dir2 = Object.assign({}, entities['pipe-to-ground'].pictures['2']) entities['pipe-to-ground'].pictures['2'] = entities['pipe-to-ground'].pictures['6'] entities['pipe-to-ground'].pictures['6'] = dir2 // shift.y-1 for dir 4 wall patch of gate let wp4 = entities['gate'].wall_patch['4'].layers[0] wp4.shift = [wp4.shift[0], wp4.shift[1] - 1] if (wp4.hr_version) { wp4.hr_version.shift = [wp4.hr_version.shift[0], wp4.hr_version.shift[1] - 1] } // fix shifts entities['storage-tank'].pictures.window_background.shift = [0, 1] entities['storage-tank'].pictures.window_background.hr_version.shift = [0, 1] add_to_shift([0, -0.6875], entities['artillery-turret'].base_picture.layers[0]) add_to_shift([0, -0.6875], entities['artillery-turret'].cannon_barrel_pictures.layers[0]) add_to_shift([0, -0.6875], entities['artillery-turret'].cannon_base_pictures.layers[0]) function add_to_shift(shift, tab) { if (tab.shift) { tab.shift = [shift[0] + tab.shift[0], shift[1] + tab.shift[1]] } else { tab.shift = shift } if (tab.hr_version) { if (tab.hr_version.shift) { tab.hr_version.shift = [shift[0] + tab.hr_version.shift[0], shift[1] + tab.hr_version.shift[1]] } else { tab.hr_version.shift = shift } } return tab } console.log('Entities: ' + Object.keys(entities).length) fse.writeFileSync(outDir + 'entityBundle.json', JSON.stringify(entities, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) { return '"entity:' + nameMapping(capture) + '"' })) graphicsBundle() async function graphicsBundle() { let paths = [] let hrPaths = [] let re = /"filename":\s*"([^.]+?\.png)"/g let str = JSON.stringify(entities) let match const excludeKeywords = [ 'explosion', 'cloud', 'smoke', 'fire', 'muzzle-flash', '-light\.padding', 'steam\.png', '-shadow\.png', '-shadow-', 'load-standup', 'flamethrower-turret-gun(-[^e]|[^-])', 'pump-[a-z]+?-liquid', 'pump-[a-z]+?-glass', 'accumulator-[a-z]+?-animation', 'connector\/(hr-)?.-.-', 'heated', 'gun-turret-gun-[m12]', 'roboport-recharging', 'segment-visualisation', 'graphics\/[^/]*$', '-light\.png', '-lights-color', 'boiling-green', 'power-switch-electricity', 'electric-furnace-heater', 'integration', 'arrows', 'hole', 'rocket-over', 'working', 'hand-closed' ] const excludeKeywordsRegex = new RegExp(excludeKeywords.join('|'), 'g') while ((match = re.exec(str)) !== null) { let path = match[1].replace(/__base__/g, 'base').replace(/__core__/g, 'core') if (match[1].search(excludeKeywordsRegex) === -1) { if (match[1].search(/\/hr-/g) === -1) { if (!paths.includes(path)) { paths.push(path) } } else { if (!hrPaths.includes(path)) { hrPaths.push(path) } } } } paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-1.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-5.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-9.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-13.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-1.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-5.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-9.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-13.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-1.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-5.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-9.png') paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-13.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-1.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-5.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-9.png') hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-13.png') console.log('Entity images: ' + paths.length) console.log('Entity HR images: ' + hrPaths.length) let cropImages = [ ['artillery-wagon-cannon', 4, 4], ['flamethrower-turret-gun-extension', 5, 1], ['gun-turret-gun-extension', 5, 1], ['laser-turret-gun-start', 15, 1], ['burner-mining-drill', 4, 8], ['electric-mining-drill', 8, 8], ['pumpjack-horsehead', 8, 5], ['assembling-machine-[1-3]\.png', 8, 4], ['centrifuge', 8, 8], ['lab.png', 11, 3], ['[^e]-pump-', 8, 4], ['splitter', 8, 4], ['radar', 8, 8], ['steam-engine', 8, 4], ['steam-turbine', 4, 2], ['transport-belt', 16, 1], ['laser-turret-gun', 8, 1], ['beacon-antenna', 8, 4], ['roboport-door-', 16, 1], ['gate(-rail(-base)?)?-[a-z]+?(-(left|right))?\.png', 8, 2], ['arm', 4, 3], ['rail-signal\.png', 3, 1], ['rail-chain-signal\.png', 4, 1], ['power-switch', 2, 3] ] let addedHrPaths = [] let imagesToCrop = [] for (let i = 0; i < paths.length; i++) { let pArr = paths[i].split('/') if (pArr[pArr.length - 1] === 'electric-furnace-base.png') { pArr[pArr.length - 1] = 'electric-furnace.png' } pArr[pArr.length - 1] = 'hr-' + pArr[pArr.length - 1] let hrVersion = pArr.join('/') if (hrPaths.includes(hrVersion)) { paths[i] = hrVersion addedHrPaths.push(hrVersion) } paths[i] = factorioDirectory + paths[i] // Crop spritesheet for (let j = 0, len2 = cropImages.length; j < len2; j++) { if (paths[i].search(new RegExp(cropImages[j][0], 'g')) !== -1) { let p = './temp/' + nameMapping(paths[i]) + '.png' imagesToCrop.push({ path: paths[i], outPath: p, cropImgIndex: j }) paths[i] = p break } } } for (let i = 0; i < hrPaths.length; i++) { if (!addedHrPaths.includes(hrPaths[i])) { paths.push(factorioDirectory + hrPaths[i]) } } //Jison.write doesn't return a Promise //https://github.com/oliver-moran/jimp/issues/90 Jimp.prototype.writeAsync = util.promisify(Jimp.prototype.write) let res = Promise.all(imagesToCrop.map(data => Jimp.read(data.path).then(img => img .crop(0, 0, img.bitmap.width / cropImages[data.cropImgIndex][1], img.bitmap.height / cropImages[data.cropImgIndex][2]) .writeAsync(data.outPath) ))) res.then(() => { console.log('Final entity images: ' + paths.length) nsg({ src: paths, spritePath: spritesheetsOutDir + 'entitySpritesheet.png', stylesheet: './json-entity.tpl', stylesheetPath: spritesheetsOutDir + 'entitySpritesheet.json', stylesheetOptions: { prefix: 'entity:', nameMapping: nameMapping }, compositor: 'jimp', layout: 'packed', layoutOptions: { padding: 2 } }, function(err) { if (err) { console.log(err) } else { fse.remove('./temp') console.log('Entity sprite atlas generated!') } }) }) }