Add script to parse Minetweaker output

This commit is contained in:
Andrew Miner
2016-08-21 14:41:23 -07:00
parent aed8b15738
commit cbb3940360
7 changed files with 1494 additions and 9 deletions
+526
View File
@@ -0,0 +1,526 @@
#!/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 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 ######################################################################################################
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.contains ?= (id)-> return true
MOD.correctItem ?= (item)-> # do nothing
MOD.makeCorrections ?= -> # do nothing
MOD.normalizeName ?= (item, index=1)-> return item.gameName
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'
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'
match = line.match /recipes.add(Shaped|Shapeless)\(<([^>]*)>( \* ([0-9]+))?, (.*)\);/
continue unless match?
recipeType = match[1]
outputId = parseMinecraftId match[2]
outputItem = findItem minecraftId:outputId
if not outputItem?
console.error "Ignoring recipe because there's no matching item: #{line}"
continue
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.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
for item in MOD.items
write "item: #{item.displayName}"
indent++
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()
sortItems()
removeDuplicates()
makeCorrections()
writeModVersionFile()
writeJsonDump()
copyImages()
+26 -9
View File
@@ -1,23 +1,40 @@
#!/bin/bash
read -d '' USAGE <<END
USAGE: convert-nei-dump <source> <mod_slug> <mod_version>
USAGE: convert-dump <source> <mod_slug> <mod_version>
Converts output from NEI dump files into a format which can be used by
Crafting Guide. In particlar, this script looks for the "Item Panel" dump
both in PNG and CSV formats and assumes they contain all the data for the
target mod.
Converts output from various dump files into a format which can be used by
Crafting Guide. BEFORE RUNNING THIS SCRIPT, YOU MUST FIRST CREATE THE
NECESSARY DUMP FILES.
Unfortunate, NEI doesn't generate enough information to fill in all the
recipes for each item: only their names. Once this script has converted the
data, you will need to manually enter the missing content.
First, set up your Minecraft profile with the mod you want to dump as well
as NEI and MineTweaker 3. Then, launch the game into a single player world
and run the following console commands:
/mt oredict
/mt recipes
/mt recipes furnace
Then, open your inventory and click the "NEI Subsets" button at the top.
Right-click the "Mod" menu, and then left-click the one mod you want to
dump. This should change the icons displayed on the right to just those
belonging to that mod.
Now, click the "Options" button in the lower-right corner. Then click
"Tools" followed by "Data Dumps". Next to "Item Panel", click the center
button until it says "PNG" and then the left hand button until it says
"48x48". Finally, click the "Dump" button.
Next, click the center button on the "Item Panel" row until it says "CSV",
and then click "Dump" again.
You have now created all the dump files needed by this script.
<source> : the "dumps" directory where NEI dropped its content
<mod_slug> : the slug of the mod being converted
<mod_version> : the version of the mod being converted
END
SOURCE_DIR="$1"; shift
MOD_SLUG="$1"; shift
MOD_VERSION="$1"; shift
+73
View File
@@ -0,0 +1,73 @@
FACADE_PATTERN = /^Facade: ([^(]*)$/
GATE_PATTERN = /^.*(OR|AND|Basic) Gate$/
HOLLOW_FACADE_PATTERN = /^Facade: ([^(]*) \(item.Facade.state_hollow\)/
exports.contains = (item)->
return false if item.minecraftId.mod.indexOf('BuildCraft') is -1
return false if item.displayName.match /^tile/
return true if item.displayName.match /Stone( Hollow)? Facade/
return false if item.displayName.match /Facade/
return false if item.displayName.match /(Water|Oil) Spring$/
return true
exports.normalizeName = (item, index=1)->
name = item.gameName
if match = name.match FACADE_PATTERN
return "#{match[1]} Facade"
if match = name.match GATE_PATTERN
switch index
when 2 then return "#{name} (Autarchic Pulsar)"
when 3 then return "#{name} (Clock Timer)"
when 4 then return "#{name} (Redstone Fader)"
if match = name.match HOLLOW_FACADE_PATTERN
return "#{match[1]} Hollow Facade"
if name is 'Redstone Board'
switch index
when 1 then return "Redstone Board (Bomber)"
when 2 then return "Redstone Board (Builder)"
when 3 then return "Redstone Board (Butcher)"
when 4 then return "Redstone Board (Carrier)"
when 5 then return "Redstone Board (Crafter)"
when 6 then return "Redstone Board (Delivery)"
when 7 then return "Redstone Board (Farmer)"
when 8 then return "Redstone Board (Knight)"
when 9 then return "Redstone Board (Harvester)"
when 10 then return "Redstone Board (Leaf Cutter)"
when 11 then return "Redstone Board (Lumberjack)"
when 12 then return "Redstone Board (Miner)"
when 13 then return "Redstone Board (Picker)"
when 14 then return "Redstone Board (Planter)"
when 15 then return "Redstone Board (Pump)"
when 16 then return "Redstone Board (Shovelman)"
when 17 then return "Redstone Board (Stripes)"
when 18 then return "Redstone Board (Tank)"
if name is 'Robot'
switch index
when 1 then return "Robot (Bomber)"
when 2 then return "Robot (Builder)"
when 3 then return "Robot (Butcher)"
when 4 then return "Robot (Carrier)"
when 5 then return "Robot (Crafter)"
when 6 then return "Robot (Delivery)"
when 7 then return "Robot (Farmer)"
when 8 then return "Robot (Knight)"
when 9 then return "Robot (Harvester)"
when 10 then return "Robot (Leaf Cutter)"
when 11 then return "Robot (Lumberjack)"
when 12 then return "Robot (Miner)"
when 13 then return "Robot (Picker)"
when 14 then return "Robot (Planter)"
when 15 then return "Robot (Pump)"
when 16 then return "Robot (Shovelman)"
when 17 then return "Robot (Stripes)"
when 18 then return "Robot (Tank)"
return name
+53
View File
@@ -0,0 +1,53 @@
WOOD_TYPES = [ 'Oak Wood', 'Birch', 'Spruce', 'Jungle', 'Acacia Wood', 'Dark Oak' ]
exports.slug = 'minecraft'
exports.version = '1.7.10'
exports.contains = (item)->
return false if item.minecraftId.mod.indexOf('minecraft') is -1
return false if item.minecraftId.name in [ 'end_portal', 'portal', 'mob_spawner', 'monster_egg', 'spawn_egg' ]
return true
exports.correctItem = (item)->
item.isGatherable = true if item.gameName in [
'Coal', 'Diamond', 'Emerald', 'Lapis Lazuli', 'Nether Quartz', 'Redstone', 'Stone', 'Wheat', 'Wool'
]
item.isGatherable = true if item.minecraftId.name in [
'melon_block'
]
item.isGatherable = false if item.gameName in [
'Bottle o\' Enchanting', 'Command Block', 'Farmland', 'Lava', 'Water', 'Written Book'
]
exports.normalizeName = (item)->
name = item.gameName
switch item.minecraftId.name
when 'clay' then name = 'Clay (block)'
when 'clay_ball' then name = 'Clay (item)'
when 'filled_map' then name = 'Map (filled)'
when 'melon_block' then name = 'Melon (block)'
when 'netherbrick' then name = 'Nether Brick (item)'
when 'nether_brick' then name = 'Nether Brick (block)'
when 'planks'
index = item.minecraftId.meta || 0
name = "#{WOOD_TYPES[index]} Planks"
when 'stone_button' then name = 'Button (stone)'
when 'stone_pressure_plate' then name = 'Pressure Plate (stone)'
when 'wooden_button' then name = 'Button (wooden)'
when 'wooden_pressure_plate' then name = 'Pressure Plate (wooden)'
when 'wooden_slab'
index = item.minecraftId.meta || 0
name = "#{WOOD_TYPES[index]} Slab"
return name
exports.shouldNormalizeGrid = (item, grid)->
return true if item.gameName in [
'Bed', 'Bowl', 'Boat', 'Brewing Stand', 'Bucket', 'Cobblestone Slab', 'Diamond Boots', 'Diamond Helmet',
'Minecart', 'Oak Wood Slab', 'Stone Slab', 'Trapdoor', 'Weighted Pressure Plate (Heavy)',
'Weighted Pressure Plate (Light)'
]
return false