Add V2 file format

This commit is contained in:
Andrew Miner
2015-01-14 15:19:02 -08:00
parent c417a24653
commit 89a8e2b539
16 changed files with 869 additions and 70 deletions
+4 -4
View File
@@ -15,22 +15,22 @@ module.exports = class Item extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.name? then throw new Error 'attributes.name is required'
if not attributes.modVersion? then throw new Error 'attributes.modVersion is required'
attributes.isGatherable ?= false
attributes.modVersion ?= null
attributes.recipes ?= []
attributes.slug ?= _.slugify attributes.name
attributes.stackSize ?= Item.DEFAULT_STACK_SIZE
super attributes, options
@modVersion.addItem this
Object.defineProperty @prototype, 'isCraftable', get:-> @recipes.length > 0
# Public Methods ###############################################################################
addRecipe: (recipe)->
slug = recipe.output[0].itemSlug
if slug isnt @slug then throw new Error "invalid recipe for #{@slug} because it makes a #{slug}"
if recipe.item isnt this then throw new Error "cannot add a recipe which isn't associated with this item"
@recipes.push recipe
compareTo: (that)->
+1 -1
View File
@@ -119,7 +119,7 @@ module.exports = class ModPack extends BaseModel
@trigger Event.load.started, this, url
$.ajax
url: url
dataType: 'json'
dataType: 'text'
success: (data, status, xhr)=>
resolve @onModVersionLoaded(url, data, status, xhr)
error: (xhr, status, error)=>
+4 -4
View File
@@ -13,8 +13,6 @@ BaseModel = require './base_model'
module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})->
options.storage ?= window.localStorage
if _.isEmpty(attributes.name) then throw new Error 'name cannot be empty'
if _.isEmpty(attributes.version) then throw new Error 'version cannot be empty'
@@ -28,9 +26,11 @@ module.exports = class ModVersion extends BaseModel
# Public Methods ###############################################################################
addItem: (item)->
if @items[item.slug]? then throw new Error "duplicate item for #{item.slug}"
if item.modVersion isnt this then throw new Error "cannot add item not associated with this mod version"
if @items[item.slug]? then throw new Error "duplicate item for #{item.name}"
@items[item.slug] = item
item.modVersion = this
@names[item.slug] = item.name
return this
compareTo: (that)->
+20 -3
View File
@@ -7,22 +7,28 @@ All rights reserved.
Logger = require '../logger'
V1 = require './mod_version_parsers/v1'
V2 = require './mod_version_parsers/v2'
########################################################################################################################
module.exports = class ModVersionParser
@CURRENT_VERSION = '1'
@CURRENT_VERSION = '2'
constructor: ->
@_parsers =
'1': new V1
'2': new V2
parse: (data)->
if not data? then throw new Error 'mod description data is missing'
if not data.dataVersion? then throw new Error 'dataVersion is required'
parser = @_parsers["#{data.dataVersion}"]
if @_isJson data
parser = @_parsers['1']
data = JSON.parse data
else
parser = @_parsers['2']
if not parser? then throw new Error "cannot parse version #{data.dataVersion} mod descriptions"
oldLevel = logger.level
@@ -39,3 +45,14 @@ module.exports = class ModVersionParser
if not parser? then throw new Error "version #{dataVersion} is not supported"
return parser.unparse modVersion
# Private Methods ##############################################################################
_isJson: (data)->
i = 0
while i < data.length
continue if data[i] is '\n'
continue if data[i] is '\r'
return true if data[i] is '{'
return false
@@ -31,8 +31,7 @@ module.exports = class V1
_findOrCreateItem: (name)->
item = @modVersion.findItemByName name
if not item?
item = new Item name:name
@modVersion.addItem item
item = new Item modVersion:@modVersion, name:name
@modVersion.registerSlug item.slug, item.name
return item
@@ -87,7 +86,6 @@ module.exports = class V1
attributes.pattern = data.pattern if data.pattern?
recipe = new Recipe attributes
item.addRecipe recipe
return recipe
_parseStack: (data, options={})->
@@ -219,10 +217,3 @@ module.exports = class V1
result.push ']'
return result
########################################################################################################################
module.exports.V2 = class V2 extends V1
constructor: ->
@_errorLocation = 'the header information'
@@ -0,0 +1,320 @@
###
Crafting Guide - mod_version_parsers/v2.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
Item = require '../item'
ModVersion = require '../mod_version'
Recipe = require '../recipe'
Stack = require '../stack'
StringBuilder = require '../string_builder'
########################################################################################################################
module.exports = class V2
@COMMAND = /\ *([^:]*):?(.*)/
@COMMENT = /([^\\]?)#.*/
@INTEGER = /[0-9]+/
@PATTERN = /^[0-9.]{3} ?[0-9.]{3} ?[0-9.]{3}$/
@STACK = /^([0-9]+) +(.*)$/
parse: (text)->
@_modVersionData = {}
@_lineNumber = 1
lines = text.split '\n'
for i in [0...lines.length]
@_lineNumber = i + 1
commands = @_parseLine lines[i]
for command in commands
@_execute command
return @_buildModVersion @_modVersionData
unparse: (modVersion)->
builder = new StringBuilder context:modVersion
@_unparseModVersion builder, modVersion
return builder.toString()
# Private Methods ##############################################################################
_execute: (command)->
method = this["_command_#{command.name}"]
if not method? then throw new Error "Unknown command: #{command.name}"
try
method.apply this, command.args
catch e
e.message = "line #{@_lineNumber}: #{e.message}"
throw e
_parseLine: (line)->
line = line.replace V2.COMMENT, '$1'
line = line.trim()
return [] if line.length is 0
lineParts = (part.trim() for part in line.split(';'))
commands = []
for linePart in lineParts
continue if linePart.length is 0
match = V2.COMMAND.exec linePart
if not match? then throw new Error "Expected <command>: <args>, but found: \"#{linePart}\""
args = []
args = (s.trim() for s in match[2].split(',')) if match[2]?
commands.push name:match[1], args:args
return commands
# Command Methods ##############################################################################
_command_description: (descriptionParts...)->
if @_modVersionData.description? then throw new Error 'duplicate declaration of "description"'
@_modVersionData.description = descriptionParts.join ', '
_command_extras: (extraTerms...)->
if not @_recipeData? then throw new Error 'cannot declare "extras" before "recipe"'
if @_recipeData.extras? then throw new Error 'duplicate declaration of "extras"'
@_recipeData.extras = []
for term in extraTerms
match = V2.STACK.exec term
if match?
@_recipeData.extras.push quantity:parseInt(match[1]), name:match[2]
else
@_recipeData.extras.push quantity:1, name:term
_command_gatherable: (gatherable)->
if not @_itemData? then throw new Error 'cannot declare "gatherable" before "item"'
if @_itemData.gatherable? then throw new Error 'duplicate declaration of "gatherable"'
if not (gatherable in ['yes', 'no']) then throw new Error 'gatherable must be either "yes" or "no"'
@_itemData.gatherable = (gatherable is 'yes')
_command_item: (name='')->
if not name.length > 0 then throw new Error 'the item name cannot be empty'
@_itemData = name:name, line:@_lineNumber
@_modVersionData.items ?= []
@_modVersionData.items.push @_itemData
@_recipeData = null
_command_name: (name='')->
if @_modVersionData.name? then throw new Error 'duplicate declaration of "name"'
if not name.length > 0 then throw new Error 'the mod name cannot be empty'
@_modVersionData.name = name
_command_input: (inputNames...)->
if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"'
if @_recipeData.input? then throw new Error 'duplicate declaration of "input"'
@_recipeData.input = []
for name in inputNames
if name.length is 0 then throw new Error 'input names cannot be empty'
@_recipeData.input.push name
_command_pattern: (pattern='')->
if not @_recipeData? then throw new Error 'cannot declare "pattern" before "recipe"'
if @_recipeData.pattern? then throw new Error 'duplicate declaration of "pattern"'
if not V2.PATTERN.test pattern
throw new Error 'a pattern must have 9 digits using 0-9 for items and "." for an empty spot;
spaces are optional'
@_recipeData.pattern = pattern
_command_quantity: (quantity)->
if not @_recipeData? then throw new Error 'cannot declare "quantity" before "recipe"'
if @_recipeData.quantity? then throw new Error 'duplicate declaration of "quantity"'
if not V2.INTEGER.test(quantity) then throw new Error 'quantity must be an integer'
@_recipeData.quantity = parseInt quantity
_command_recipe: ->
if not @_itemData? then throw new Error 'cannot delcare "recipe" before "item"'
@_recipeData = line:@_lineNumber
@_itemData.recipes ?= []
@_itemData.recipes.push @_recipeData
_command_schema: -> # do nothing
_command_tools: (toolNames...)->
if not @_recipeData? then throw new Error 'cannot declare "tools" before "recipe"'
if @_recipeData.tools? then throw new Error 'duplicate declaration of "tools"'
@_recipeData.tools = []
for name in toolNames
if name.length is 0 then throw new Error 'tool names cannot be empty'
@_recipeData.tools.push name
_command_version: (version='')->
if @_modVersionData.version? then throw new Error 'duplicate declaration of "version"'
if version.length is 0 then throw new Error 'version cannot be empty'
@_modVersionData.version = version
# Object Creation Methods ######################################################################
_buildModVersion: (modVersionData)->
if not modVersionData.name? then throw new Error 'the "name" declaration is required'
if not modVersionData.version? then throw new Error 'the "version" declaration is required'
modVersionData.description ?= ''
modVersionData.items ?= []
attributes =
name: modVersionData.name
version: modVersionData.version
description: modVersionData.description
modVersion = new ModVersion attributes
for itemData in modVersionData.items
@_buildItem modVersion, itemData
return modVersion
_buildItem: (modVersion, itemData)->
@_lineNumber = itemData.line
itemData.gatherable ?= false
itemData.recipes ?= []
item = new Item modVersion:modVersion, name:itemData.name, isGatherable:itemData.gatherable
for recipeData in itemData.recipes
@_buildRecipe modVersion, item, recipeData
return item
_buildRecipe: (modVersion, item, recipeData)->
@_lineNumber = recipeData.line
if not recipeData.input? then throw new Error 'the "input" declaration is required'
if not recipeData.pattern? then throw new Error 'the "pattern" declaration is required'
recipeData.quantity ?= 1
recipeData.extras ?= []
recipeData.tools ?= []
inputStacks = []
for name in recipeData.input
slug = _.slugify name
modVersion.registerSlug slug, name
inputStacks.push new Stack itemSlug:slug, quantity:0
for c in recipeData.pattern
continue if c is '.'
continue if c is ' '
stack = inputStacks[parseInt(c)]
if not stack? then throw new Error "there is no input #{c} in this recipe"
stack.quantity += 1
for i in [0...inputStacks.length]
stack = inputStacks[i]
if stack.quantity is 0
name = modVersion.findName stack.itemSlug
throw new Error "#{name} is an input for this recipe, but it is not in the pattern"
outputStacks = [ new Stack itemSlug:item.slug, quantity:recipeData.quantity ]
for extraData in recipeData.extras
slug = _.slugify extraData.name
modVersion.registerSlug slug, extraData.name
outputStacks.push new Stack itemSlug:slug, quantity:extraData.quantity
toolStacks = []
for name in recipeData.tools
slug = _.slugify name
modVersion.registerSlug slug, name
toolStacks.push new Stack itemSlug:slug, quantity:1
attributes =
input: inputStacks
item: item
pattern: recipeData.pattern
output: outputStacks
tools: toolStacks
recipe = new Recipe attributes
return recipe
# Un-parsing Methods ###########################################################################
_unparseModVersion: (builder, modVersion)->
itemList = _.values modVersion.items
itemList.sort (a, b)-> a.compareTo b
builder
.line 'schema: ', 2
.line 'name: ', modVersion.name
.line 'version: ', modVersion.version
.onlyIf modVersion.description?, => builder.line 'description: ', modVersion.description
.line()
.onlyIf itemList.length > 0, =>
builder.loop itemList, delimiter:'\n', onEach:(b, i)=> @_unparseItem(b, i)
.outdent()
_unparseItem: (builder, item)->
builder
.line 'item: ', item.name
.indent()
.onlyIf item.isGatherable, => builder.line 'gatherable: yes'
.onlyIf item.recipes.length > 0, =>
builder.loop item.recipes, delimiter:'', onEach:(b, r)=> @_unparseRecipe(b, r)
.outdent()
_unparseRecipe: (builder, recipe)->
inputNames = (builder.context.findName(stack.itemSlug) for stack in recipe.input)
inputNames.sort()
patternMap = {'.', '.'}
for i in [0...recipe.input.length]
stack = recipe.input[i]
patternMap["#{i}"] = "#{inputNames.indexOf builder.context.findName(stack.itemSlug)}"
pattern = recipe.pattern or recipe.defaultPattern
newPattern = []
for c in pattern.split('')
newPattern.push patternMap[c]
newPattern = newPattern.join ''
newPattern = newPattern.replace /(...)(...)(...)/, '$1 $2 $3'
quantity = recipe.output[0].quantity
extraOutputs = recipe.output[0...recipe.output.length]
extraOutputs.shift()
builder
.line 'recipe:'
.indent()
.onlyIf extraOutputs.length > 0, =>
builder
.push 'extras: '
.call => @_unparseStackList builder, extraOutputs
.line()
.push 'input: '
.loop inputNames
.line()
.line 'pattern: ', newPattern
.onlyIf quantity > 1, => builder.line 'quantity: ', quantity
.onlyIf recipe.tools.length > 0, =>
builder
.push 'tools: '
.call => @_unparseStackList builder, recipe.tools
.line()
.outdent()
_unparseStackList: (builder, stackList)->
if stackList.length is 1 and stackList[0].quantity is 1
builder.push builder.context.findName(stackList[0].itemSlug)
else
builder.loop stackList, onEach:(b, stack)=>
builder
.onlyIf stack.quantity > 1, => builder.push stack.quantity, ' '
.push builder.context.findName stack.itemSlug
+8 -4
View File
@@ -6,23 +6,27 @@ All rights reserved.
###
BaseModel = require './base_model'
Stack = require './stack'
########################################################################################################################
module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.item? then throw new Error 'attributes.item is required'
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.output? then throw new Error 'attributes.output is required'
if not attributes.item? then throw new Error 'attributes.item is required'
attributes.output ?= [new Stack itemSlug:attributes.item.slug]
attributes.pattern = @_parsePattern attributes.pattern
attributes.tools ?= []
super attributes, options
@item.addRecipe this
Object.defineProperties this,
'name': { get: -> @item.name }
'slug': { get: -> @item.slug }
'defaultPattern': { get: -> @_computeDefaultPattern() }
'name': { get: -> @item.name }
'slug': { get: -> @item.slug }
# Public Methods ###############################################################################
+122
View File
@@ -0,0 +1,122 @@
###
Crafting Guide - string_builder.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class StringBuilder
constructor: (options={})->
options.indent ?= 0
options.indentString ?= ' '
@context = options.context
@_initialIndent = options.indent
@_indent = options.indent
@_indentString = options.indentString
@_pieces = []
# Public Methods ###############################################################################
call: ->
args = (arg for arg in arguments)
callback = args.pop()
args.unshift this
return unless _.isFunction callback
callback.apply null, args
clear: ->
@_indent = @_initialIndent
@_pieces = []
return this
indent: ->
@_indent += 1
return this
line: (args...)->
@push.apply this, args
@push '\n'
loop: (list, options={})->
options.start ?= ''
options.end ?= ''
options.indent ?= false
options.delimiter ?= ', '
options.onEach ?= (builder, item)-> builder.push item
@_pushText options.start
if options.indent then @indent()
isFirst = true
for item in list
if not isFirst then @push options.delimiter
isFirst = false
options.onEach this, item
if options.indent then @outdent()
@_pushText options.end
return this
onlyIf: (condition, callback=null)->
return this unless condition
callback ?= (builder)-> # do nothing
callback this
return this
outdent: ->
@_indent -= 1
return this
push: ->
for i in [0...arguments.length]
arg = arguments[i]
if _.isArray arg
@push.apply this, arg
else if _.isString arg
@_pushText arg
else
@_pushText "#{arg}"
return this
# Object Overrides #############################################################################
toString: ->
return @_pieces.join ''
# Private Methods ##############################################################################
_pushIndent: ->
for i in [0...@_indent]
@_pieces.push @_indentString
_pushText: (text)->
return if text.length is 0
index = 0
if @_pieces.length > 0
lastPiece = @_pieces[@_pieces.length-1]
if lastPiece[lastPiece.length-1] is '\n'
@_pushIndent()
while true
newLineAt = text.indexOf '\n', index
break if newLineAt is -1
@_pieces.push text[index..newLineAt]
index = newLineAt + 1
break if newLineAt is text.length - 1
@_pushIndent()
if text.length > index
@_pieces.push text[index...text.length]