You've already forked factorio-blueprint-editor
mirror of
https://github.com/teoxoy/factorio-blueprint-editor.git
synced 2025-11-23 22:15:01 +02:00
467 lines
16 KiB
JavaScript
467 lines
16 KiB
JavaScript
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!')
|
|
}
|
|
})
|
|
})
|
|
}
|