Update the convert-dump script to allow mod scripts to assign groups and to scrub inout lines to remove/replace text from the Minetweaker output.
550 lines
16 KiB
CoffeeScript
Executable File
550 lines
16 KiB
CoffeeScript
Executable File
#!/usr/bin/env coffee
|
|
|
|
_ = require 'underscore'
|
|
fs = require 'fs-extra'
|
|
path = require 'path'
|
|
util = require 'util'
|
|
|
|
# Global Data ##########################################################################################################
|
|
|
|
DEBUG = false
|
|
MOD = null
|
|
MOD_SCRIPT = null
|
|
WORLD_DIR = null
|
|
INDENT = 0
|
|
|
|
# Constants ############################################################################################################
|
|
|
|
MINETWEAKER_LOG = 'minetweaker.log'
|
|
ICON_DIR = path.join 'dumps', 'itempanel_icons'
|
|
ITEMPANEL_CSV = path.join 'dumps', 'itempanel.csv'
|
|
|
|
# Process Arguments ####################################################################################################
|
|
|
|
process.argv.shift() # coffee
|
|
process.argv.shift() # script file
|
|
|
|
while process.argv.length > 0
|
|
switch process.argv[0]
|
|
when '--world'
|
|
process.argv.shift()
|
|
WORLD_DIR = process.argv[0]
|
|
|
|
when '--mod'
|
|
process.argv.shift()
|
|
MOD_SCRIPT = process.argv[0]
|
|
|
|
process.argv.shift()
|
|
|
|
if not WORLD_DIR? then throw new Error '--world is required'
|
|
if not MOD_SCRIPT? then throw new Error '--mod is required'
|
|
|
|
# Classes ##############################################################################################################
|
|
|
|
class MinecraftId
|
|
|
|
constructor: (@mod, @name, @meta=null, @variant=null)->
|
|
@meta = null if not @meta or @meta in ['0', '*']
|
|
@meta = parseInt @meta
|
|
@meta = null if Number.isNaN @meta
|
|
|
|
compareTo: (other)->
|
|
return -1 unless other?
|
|
|
|
if @mod isnt other.mod
|
|
return if @mod < other.mod then -1 else +1
|
|
if @name isnt other.name
|
|
return if @name < other.name then -1 else +1
|
|
if @meta isnt other.meta
|
|
if not @meta? then return -1
|
|
if not other.meta? then return +1
|
|
return if @meta < other.meta then -1 else +1
|
|
if @variant isnt other.variant
|
|
if not @variant? then return -1
|
|
if not other.variant? then return +1
|
|
return if @variant < other.variant then -1 else +1
|
|
|
|
return 0
|
|
|
|
createVariant: (variant)->
|
|
return new MinecraftId @mod, @name, @meta, variant
|
|
|
|
equals: (other)->
|
|
return @compareTo(other) is 0
|
|
|
|
matches: (other)->
|
|
return false unless other?
|
|
return false unless @mod is other.mod
|
|
return false unless @name is other.name
|
|
return true if other.meta is null
|
|
return false unless @meta is other.meta
|
|
return true
|
|
|
|
toString: ->
|
|
if not @_string
|
|
@_string = "#{@mod}:#{@name}"
|
|
if @meta? then @_string += ":#{@meta}"
|
|
if @variant? then @_string += ":#{@variant}"
|
|
return @_string
|
|
|
|
# Helper Functions #####################################################################################################
|
|
|
|
addItem = (item)->
|
|
if not item? then throw new Error "cannot add null item"
|
|
|
|
MOD.items.push item
|
|
|
|
computePattern = (recipe, inputGrid)->
|
|
pattern = [['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
|
|
cells = _.compact _.flatten inputGrid
|
|
recipe.inputNames = (_.uniq (cell.item.displayName for cell in cells)).sort()
|
|
|
|
for row, rowIndex in inputGrid
|
|
for gridCell, columnIndex in row
|
|
continue unless gridCell?
|
|
|
|
itemNumber = recipe.inputNames.indexOf gridCell.item.displayName
|
|
|
|
if itemNumber >= 0
|
|
pattern[rowIndex][columnIndex] = "#{itemNumber}"
|
|
|
|
p = pattern
|
|
recipe.pattern = "#{p[0][0]}#{p[0][1]}#{p[0][2]} #{p[1][0]}#{p[1][1]}#{p[1][2]} #{p[2][0]}#{p[2][1]}#{p[2][2]}"
|
|
|
|
computeGridSize = (grid)->
|
|
grid.width = grid.height = 0
|
|
|
|
grid.rawHeight = grid.length
|
|
grid.rawWidth = 0
|
|
for row, rowIndex in grid
|
|
grid.rawWidth = Math.max grid.rawWidth, row.length
|
|
|
|
for cell, colIndex in row
|
|
continue unless cell?
|
|
grid.height = Math.max grid.height, rowIndex + 1
|
|
grid.width = Math.max grid.width, colIndex + 1
|
|
|
|
grid.isLarge = grid.width > 2 or grid.height > 2
|
|
|
|
computeSlug = (text)->
|
|
return null unless text?
|
|
|
|
result = text.toLowerCase()
|
|
result = result.replace /[^a-zA-Z0-9_]/g, '_'
|
|
result = result.replace /__+/g, '_'
|
|
result = result.replace /^_/, ''
|
|
result = result.replace /_$/, ''
|
|
return result
|
|
|
|
debugLog = (text)->
|
|
return unless DEBUG
|
|
console.error text
|
|
|
|
findItem = (search)->
|
|
for item in MOD.items
|
|
matches = true
|
|
for field, value of search
|
|
if field is 'minecraftId'
|
|
if not item.minecraftId.equals(search.minecraftId)
|
|
matches = false
|
|
break
|
|
|
|
else if item[field] isnt value
|
|
matches = false
|
|
break
|
|
|
|
return item if matches
|
|
|
|
return null
|
|
|
|
lookUpItems = (itemIds)->
|
|
for row in itemIds
|
|
for gridCell in row
|
|
continue unless gridCell?
|
|
|
|
item = findItem minecraftId:gridCell.minecraftId
|
|
if not item?
|
|
console.error "could not find an item for id: #{gridCell.minecraftId}"
|
|
return null
|
|
|
|
gridCell.item = item
|
|
|
|
normalizeMinecraftId = (minecraftId)->
|
|
return null unless minecraftId
|
|
return minecraftId unless minecraftId.mod is 'ore'
|
|
entries = MOD.oreDict[minecraftId.toString()]
|
|
return minecraftId unless entries?
|
|
return minecraftId unless entries.length > 0
|
|
return entries[0]
|
|
|
|
normalizeRecipeGrid = (grid)->
|
|
result = grid
|
|
|
|
if grid.rawWidth is 2 and grid.rawHeight is 3
|
|
result = [
|
|
[ grid[0][0], grid[0][1], grid[1][0] ],
|
|
[ grid[1][1], grid[2][0], grid[2][1] ],
|
|
[ null, null, null ]
|
|
]
|
|
else if grid.rawWidth is 1 and grid.rawHeight is 3
|
|
result = [
|
|
[ grid[0][0], grid[1][0], grid[2][0] ],
|
|
[ null, null, null ]
|
|
[ null, null, null ]
|
|
]
|
|
else if grid.rawWidth is 1 and grid.rawHeight is 2
|
|
result = [
|
|
[ grid[0][0], grid[1][0], null ],
|
|
[ null, null, null ]
|
|
[ null, null, null ]
|
|
]
|
|
|
|
computeGridSize result
|
|
|
|
return result
|
|
|
|
parseMinecraftId = (minecraftId)->
|
|
match = /^([^:]*):([^:]*)(:([0-9*]*))?$/.exec minecraftId
|
|
return null unless match?
|
|
return new MinecraftId match[1], match[2], match[4]
|
|
|
|
parseRecipeInputs = (recipeType, text)->
|
|
allRows = []
|
|
currentRow = null
|
|
currentItem = ''
|
|
|
|
completeCurrentItem = ->
|
|
currentItem = currentItem.trim()
|
|
if currentItem isnt 'null'
|
|
match = currentItem.match /^ *([^ ]*)( \* ([0-9]+))?/
|
|
quantity = parseInt match[3]
|
|
if _.isNaN(quantity) then quantity = 1
|
|
if match?
|
|
currentRow.push(
|
|
minecraftId: normalizeMinecraftId parseMinecraftId match[1]
|
|
quantity: quantity
|
|
)
|
|
else
|
|
currentRow.push null
|
|
currentItem = ''
|
|
|
|
inputText = if recipeType is 'Shapeless' then text[0..text.length] else text[1..text.length - 2]
|
|
for c in inputText.split ''
|
|
if c is '['
|
|
currentRow = []
|
|
else if c is ']'
|
|
completeCurrentItem()
|
|
allRows.push currentRow
|
|
currentRow = null
|
|
else if c is ','
|
|
if currentRow?
|
|
completeCurrentItem()
|
|
else if c in ['<', '>']
|
|
# do nothing
|
|
else
|
|
if currentItem?
|
|
currentItem += c
|
|
|
|
if recipeType is 'Shapeless'
|
|
a = _.flatten allRows
|
|
allRows = [
|
|
[ a[0], a[1], a[4] ]
|
|
[ a[2], a[3], a[5] ]
|
|
[ a[6], a[7], a[8] ]
|
|
]
|
|
|
|
computeGridSize allRows
|
|
|
|
return allRows
|
|
|
|
# Stage Functions ######################################################################################################
|
|
|
|
assignGroups = ->
|
|
for item in MOD.items
|
|
MOD.assignGroup item
|
|
|
|
copyImages = ->
|
|
for item in MOD.items
|
|
continue unless item.iconFile?
|
|
|
|
itemPath = path.join 'data', MOD.slug, 'items', item.slug
|
|
fs.ensureDir itemPath
|
|
fs.copySync item.iconFile, path.join(itemPath, 'icon.png'), clobber:true
|
|
|
|
discardNonModItems = ->
|
|
result = []
|
|
for item in MOD.items
|
|
if MOD.contains item
|
|
result.push item
|
|
|
|
MOD.items = result
|
|
|
|
loadModHelper = ->
|
|
MOD = require "./mods/#{MOD_SCRIPT}"
|
|
|
|
MOD.slug ?= MOD_SCRIPT
|
|
MOD.version ?= '1.0'
|
|
|
|
MOD.items ?= []
|
|
MOD.oreDict ?= {}
|
|
|
|
MOD.adjustRecipe ?= (recipe)-> return recipe
|
|
MOD.assignGroup ?= (item)-> return null
|
|
MOD.contains ?= (id)-> return true
|
|
MOD.correctItem ?= (item)-> # do nothing
|
|
MOD.makeCorrections ?= -> # do nothing
|
|
MOD.normalizeName ?= (item, index=1)-> return item.gameName
|
|
MOD.scrubMinetweakerLine ?= (lineText)-> return lineText
|
|
MOD.shouldNormalizeGrid ?= (item, grid)-> return false
|
|
|
|
makeCorrections = ->
|
|
for item in MOD.items
|
|
MOD.correctItem item
|
|
|
|
MOD.makeCorrections()
|
|
|
|
removeDuplicates = ->
|
|
result = []
|
|
|
|
lastDisplayName = null
|
|
for item in MOD.items
|
|
continue if item.displayName is lastDisplayName
|
|
result.push item
|
|
lastDisplayName = item.displayName
|
|
|
|
MOD.items = result
|
|
|
|
scanFurnaceRecipes = ->
|
|
fileText = fs.readFileSync path.join(WORLD_DIR, MINETWEAKER_LOG), 'utf-8'
|
|
for line in fileText.split '\n'
|
|
line = MOD.scrubMinetweakerLine line
|
|
match = line.match /furnace.addRecipe\(<([^>]*)>, <([^>]*)>/
|
|
continue unless match?
|
|
|
|
outputId = parseMinecraftId match[1]
|
|
outputItem = findItem minecraftId:outputId
|
|
if not outputItem?
|
|
console.error "Skipping furnace recipe for #{match[1]} because no matching item could be found"
|
|
continue
|
|
|
|
inputId = parseMinecraftId match[2]
|
|
inputItem = findItem minecraftId:inputId
|
|
if not inputItem?
|
|
console.error "Skipping furnace recipe for #{match[1]} because input item #{match[2]} could not be found"
|
|
continue
|
|
|
|
recipe =
|
|
inputNames: [ inputItem.gameName, 'furnace fuel' ]
|
|
quantity: 1
|
|
extras: []
|
|
tools: [ 'Furnace' ]
|
|
pattern: '.0. ... .1.'
|
|
|
|
outputItem.recipes ?= []
|
|
outputItem.recipes.push recipe
|
|
|
|
scanIconFiles = ->
|
|
iconFiles = fs.readdirSync path.join WORLD_DIR, ICON_DIR
|
|
for fileName in iconFiles
|
|
match = fileName.match /^(.*?)(_([0-9]+))?\.png$/
|
|
continue unless match?
|
|
|
|
baseItem = findItem iconFileName:match[1]
|
|
if not baseItem?
|
|
console.error "Could not find item for icon file: #{fileName}"
|
|
continue
|
|
|
|
index = parseInt match[3]
|
|
index = if Number.isNaN(index) then 1 else index
|
|
|
|
if index is 1
|
|
item = baseItem
|
|
item.displayName = MOD.normalizeName baseItem, index
|
|
else
|
|
displayName = MOD.normalizeName baseItem, index
|
|
if displayName
|
|
item =
|
|
minecraftId: baseItem.minecraftId.createVariant index
|
|
gameName: baseItem.gameName
|
|
displayName: displayName
|
|
iconFileName: fileName
|
|
slug: computeSlug displayName
|
|
|
|
addItem item
|
|
else
|
|
console.error "Could not find item for icon file: #{fileName}"
|
|
|
|
item.iconFile = path.join WORLD_DIR, ICON_DIR, fileName
|
|
|
|
scanItemPanelCsv = ->
|
|
fileText = fs.readFileSync path.join(WORLD_DIR, ITEMPANEL_CSV), 'utf-8'
|
|
lines = fileText.split '\n'
|
|
lines.sort()
|
|
|
|
priorLine = null
|
|
for line, lineNumber in lines
|
|
continue if lineNumber is 0
|
|
continue if priorLine is line
|
|
|
|
match = line.match /^([^,]*),([^,]*),([^,]*),([^,]*),([^,]*)$/
|
|
continue unless match
|
|
|
|
item =
|
|
minecraftId: parseMinecraftId "#{match[1]}:#{match[3]}"
|
|
gameName: match[5]
|
|
iconFileName: match[5].replace(/:/g, '_')
|
|
|
|
item.displayName = MOD.normalizeName item
|
|
item.slug = computeSlug item.displayName
|
|
|
|
addItem item
|
|
|
|
priorLine = line
|
|
|
|
scanOreDict = ->
|
|
fileText = fs.readFileSync path.join(WORLD_DIR, MINETWEAKER_LOG), 'utf-8'
|
|
lines = fileText.split '\n'
|
|
|
|
index = 0
|
|
while true
|
|
break if index >= lines.length
|
|
line = lines[index]
|
|
index += 1
|
|
|
|
match = line.match /^Ore entries for <([^>]*)> :$/
|
|
continue unless match?
|
|
|
|
genericIdText = match[1]
|
|
while true
|
|
break if index >= lines.length
|
|
line = lines[index]
|
|
|
|
match = line.match /^ <([^>]*)>$/
|
|
break unless match?
|
|
index += 1
|
|
|
|
MOD.oreDict[genericIdText] ?= []
|
|
MOD.oreDict[genericIdText].push parseMinecraftId match[1]
|
|
|
|
scanRecipes = ->
|
|
fileText = fs.readFileSync path.join(WORLD_DIR, MINETWEAKER_LOG), 'utf-8'
|
|
for line in fileText.split '\n'
|
|
line = MOD.scrubMinetweakerLine line
|
|
match = line.match /recipes.add(Shaped|Shapeless)\(<([^>]*)>( \* ([0-9]+))?, (.*)\);/
|
|
continue unless match?
|
|
|
|
recipeType = match[1]
|
|
|
|
outputId = parseMinecraftId match[2]
|
|
outputItem = findItem minecraftId:outputId
|
|
continue unless outputItem?
|
|
|
|
quantity = parseInt match[4]
|
|
quantity = if _.isNaN(quantity) then 1 else quantity
|
|
|
|
inputGrid = parseRecipeInputs recipeType, match[5]
|
|
if MOD.shouldNormalizeGrid outputItem, inputGrid
|
|
inputGrid = normalizeRecipeGrid inputGrid
|
|
continue if inputGrid is null
|
|
|
|
recipe =
|
|
inputNames: []
|
|
quantity: quantity
|
|
extras: []
|
|
tools: if inputGrid.isLarge then ['Crafting Table'] else []
|
|
|
|
lookUpItems inputGrid
|
|
computePattern recipe, inputGrid
|
|
|
|
recipe = MOD.adjustRecipe recipe
|
|
if recipe?
|
|
outputItem.recipes ?= []
|
|
outputItem.recipes.push recipe
|
|
|
|
sortItems = ->
|
|
MOD.items.sort (a, b)->
|
|
if a.groupName isnt b.groupName
|
|
if not a.groupName? then return -1
|
|
if not b.groupName? then return +1
|
|
return if a.groupName < b.groupName then -1 else +1
|
|
|
|
if a.displayName isnt b.displayName
|
|
return if a.displayName < b.displayName then -1 else +1
|
|
|
|
return a.minecraftId.compareTo b.minecraftId
|
|
|
|
writeModVersionFile = ->
|
|
lines = ['schema: 1', '']
|
|
indent = 0
|
|
|
|
write = (lineText)->
|
|
for i in [0...indent]
|
|
lineText = ' ' + lineText
|
|
lines.push lineText
|
|
|
|
currentGroupName = null
|
|
for item in MOD.items
|
|
if item.groupName isnt currentGroupName
|
|
indent = 0
|
|
write "group: #{item.groupName}"
|
|
write ''
|
|
currentGroupName = item.groupName
|
|
indent++
|
|
|
|
write "item: #{item.displayName}"
|
|
indent++
|
|
|
|
# write "minecraftId: #{item.minecraftId}"
|
|
|
|
if (not item.isGatherable? and not item.recipes?) or item.isGatherable
|
|
write 'gatherable: yes'
|
|
|
|
if item.recipes?
|
|
for recipe in item.recipes
|
|
write 'recipe:'
|
|
indent++
|
|
|
|
write "input: #{recipe.inputNames.join(', ')}"
|
|
|
|
write "pattern: #{recipe.pattern}"
|
|
|
|
if recipe.quantity isnt 1
|
|
write "quantity: #{recipe.quantity}"
|
|
|
|
if recipe.tools.length > 0
|
|
write "tools: #{recipe.tools.join(', ')}"
|
|
|
|
indent--
|
|
|
|
indent--
|
|
write ''
|
|
|
|
versionPath = path.join 'data', MOD.slug, 'versions', MOD.version
|
|
fs.ensureDirSync versionPath
|
|
fs.writeFileSync path.join(versionPath, 'mod-version.cg'), lines.join('\n'), encoding:'utf-8'
|
|
|
|
writeJsonDump = ->
|
|
versionPath = path.join 'data', MOD.slug, 'versions', MOD.version
|
|
fs.ensureDirSync versionPath
|
|
fs.writeFileSync path.join(versionPath, 'mod-version.json'), JSON.stringify(MOD.items, null, 4), encoding:'utf-8'
|
|
|
|
########################################################################################################################
|
|
|
|
loadModHelper()
|
|
|
|
scanItemPanelCsv()
|
|
scanIconFiles()
|
|
scanOreDict()
|
|
scanRecipes()
|
|
scanFurnaceRecipes()
|
|
|
|
discardNonModItems()
|
|
assignGroups()
|
|
sortItems()
|
|
removeDuplicates()
|
|
makeCorrections()
|
|
|
|
writeModVersionFile()
|
|
# writeJsonDump()
|
|
copyImages()
|