From 89a8e2b5390e128861901f35f1e9a318e4679544 Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Wed, 14 Jan 2015 15:19:02 -0800 Subject: [PATCH] Add V2 file format --- scripts/reformat | 20 +- src/scripts/models/item.coffee | 8 +- src/scripts/models/mod_pack.coffee | 2 +- src/scripts/models/mod_version.coffee | 8 +- src/scripts/models/mod_version_parser.coffee | 23 +- .../models/mod_version_parsers/v1.coffee | 11 +- .../models/mod_version_parsers/v2.coffee | 320 ++++++++++++++++++ src/scripts/models/recipe.coffee | 12 +- src/scripts/models/string_builder.coffee | 122 +++++++ test/crafting_plan.test.coffee | 24 +- test/mod_pack.test.coffee | 12 +- test/mod_version.test.coffee | 24 +- test/mod_version_parsers/v1.test.coffee | 4 +- test/mod_version_parsers/v2.test.coffee | 262 ++++++++++++++ test/string_builder.test.coffee | 84 +++++ test/test.coffee | 3 + 16 files changed, 869 insertions(+), 70 deletions(-) create mode 100644 src/scripts/models/mod_version_parsers/v2.coffee create mode 100644 src/scripts/models/string_builder.coffee create mode 100644 test/mod_version_parsers/v2.test.coffee create mode 100644 test/string_builder.test.coffee diff --git a/scripts/reformat b/scripts/reformat index 59eb7f46a..7ea185f92 100755 --- a/scripts/reformat +++ b/scripts/reformat @@ -36,13 +36,15 @@ else global.logger = new Logger level:Logger.WARNING -text = fs.readFileSync sourceFileName, 'UTF-8' -data = JSON.parse text -parser = new ModVersionParser -modVersion = parser.parse data -text = parser.unparse modVersion +try + text = fs.readFileSync sourceFileName, 'UTF-8' + parser = new ModVersionParser + modVersion = parser.parse text + text = parser.unparse modVersion -if targetFileName is '-' - console.log text -else - fs.writeFileSync targetFileName, text, encoding:'UTF-8' + if targetFileName is '-' + console.log text + else + fs.writeFileSync targetFileName, text, encoding:'UTF-8' +catch e + console.error e.stack diff --git a/src/scripts/models/item.coffee b/src/scripts/models/item.coffee index 3d841c243..e504757b5 100644 --- a/src/scripts/models/item.coffee +++ b/src/scripts/models/item.coffee @@ -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)-> diff --git a/src/scripts/models/mod_pack.coffee b/src/scripts/models/mod_pack.coffee index cde3b3805..787ab62ae 100644 --- a/src/scripts/models/mod_pack.coffee +++ b/src/scripts/models/mod_pack.coffee @@ -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)=> diff --git a/src/scripts/models/mod_version.coffee b/src/scripts/models/mod_version.coffee index 5e6d2eeba..94b898f21 100644 --- a/src/scripts/models/mod_version.coffee +++ b/src/scripts/models/mod_version.coffee @@ -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)-> diff --git a/src/scripts/models/mod_version_parser.coffee b/src/scripts/models/mod_version_parser.coffee index 7f4755edd..724addd86 100644 --- a/src/scripts/models/mod_version_parser.coffee +++ b/src/scripts/models/mod_version_parser.coffee @@ -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 diff --git a/src/scripts/models/mod_version_parsers/v1.coffee b/src/scripts/models/mod_version_parsers/v1.coffee index dc68d1562..55b9e4684 100644 --- a/src/scripts/models/mod_version_parsers/v1.coffee +++ b/src/scripts/models/mod_version_parsers/v1.coffee @@ -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' diff --git a/src/scripts/models/mod_version_parsers/v2.coffee b/src/scripts/models/mod_version_parsers/v2.coffee new file mode 100644 index 000000000..989310c1b --- /dev/null +++ b/src/scripts/models/mod_version_parsers/v2.coffee @@ -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 : , 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 diff --git a/src/scripts/models/recipe.coffee b/src/scripts/models/recipe.coffee index 928f1c32f..6532af3d7 100644 --- a/src/scripts/models/recipe.coffee +++ b/src/scripts/models/recipe.coffee @@ -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 ############################################################################### diff --git a/src/scripts/models/string_builder.coffee b/src/scripts/models/string_builder.coffee new file mode 100644 index 000000000..c8152185d --- /dev/null +++ b/src/scripts/models/string_builder.coffee @@ -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] diff --git a/test/crafting_plan.test.coffee b/test/crafting_plan.test.coffee index d25bf773e..6d3982995 100644 --- a/test/crafting_plan.test.coffee +++ b/test/crafting_plan.test.coffee @@ -18,20 +18,16 @@ describe 'CraftingPlan', -> beforeEach -> modPack = new ModPack - modPack.loadModVersionData { - dataVersion: 1 - name: 'Minecraft' - version: '1.7.10' - recipes: [ - { input:'Oak Log', output:[[4, 'Oak Plank']] } - { input:[[2, 'Oak Plank']], output:[[4, 'Stick']] } - { input:[[4, 'Oak Plank']], output:'Crafting Table' } - { input:[[8, 'Cobblestone']], tools:'Crafting Table', output:'Furnace' } - { input:['Iron Ore', 'furnace fuel'], tools:'Furnace', output:'Iron Ingot' } - { input:[[2, 'Iron Ingot'], 'Stick'], tools:'Crafting Table', output:'Iron Sword' } - ] - } + modPack.loadModVersionData """ + schema:2; name:Minecraft; version:1.7.10 + item:Oak Plank; recipe:; input:Oak Log; pattern:... .0. ...; quantity:4 + item:Stick; recipe:; input:Oak Plank; pattern:... .0. .0.; quantity:4 + item:Crafting Table; recipe:; input:Oak Plank; pattern:00. 00. ... + item:Furnace; recipe:; input:Cobblestone; pattern:000 0.0 000; tools:Crafting Table + item:Iron Ingot; recipe:; input:Iron Ore, furnace fuel; pattern:.0. ... .1.; tools:Furnace + item:Iron Sword; recipe:; input:Iron Ingot, Stick; pattern:.0. .0. .1.; tools:Crafting Table + """ plan = new CraftingPlan modPack:modPack, includingTools:false describe 'craft', -> @@ -81,5 +77,3 @@ describe 'CraftingPlan', -> plan.result.toList().should.eql [ 'crafting_table', 'furnace', 'iron_sword', [2, 'oak_plank'], [3, 'stick'] ] - - describe 'using existing inventory', -> diff --git a/test/mod_pack.test.coffee b/test/mod_pack.test.coffee index 5eba34253..2a80ba277 100644 --- a/test/mod_pack.test.coffee +++ b/test/mod_pack.test.coffee @@ -19,17 +19,17 @@ describe 'ModPack', -> beforeEach -> minecraft = new ModVersion name:'Minecraft', version:'1.7.10', enabled:true - minecraft.addItem new Item name:'Wool' - minecraft.addItem new Item name:'Bed', recipes:[''] + new Item modVersion:minecraft, name:'Wool' + new Item modVersion:minecraft, name:'Bed', recipes:[''] minecraft.registerSlug 'iron_chestplate', 'Iron Chestplate' buildcraft = new ModVersion name:'Buildcraft', version:'4.0', enabled:false - buildcraft.addItem new Item name:'Stone Gear', recipes:[''] - buildcraft.addItem new Item name:'Bed', recipes:[''] + new Item modVersion:buildcraft, name:'Stone Gear', recipes:[''] + new Item modVersion:buildcraft, name:'Bed', recipes:[''] industrialCraft = new ModVersion name:'Industrial Craft', version:'2.0', enabled:false - industrialCraft.addItem new Item name:'Resin' - industrialCraft.addItem new Item name:'Rubber', recipes:[''] + new Item modVersion:industrialCraft, name:'Resin' + new Item modVersion:industrialCraft, name:'Rubber', recipes:[''] modPack = new ModPack modVersions:[minecraft, buildcraft, industrialCraft] diff --git a/test/mod_version.test.coffee b/test/mod_version.test.coffee index 392fa0853..ba7c28009 100644 --- a/test/mod_version.test.coffee +++ b/test/mod_version.test.coffee @@ -34,11 +34,11 @@ describe 'ModVersion', -> describe 'addItem', -> it 'refuses to add duplicates', -> - modVersion.addItem new Item name:'Wool' - expect(-> modVersion.addItem new Item name:'Wool').to.throw Error, 'duplicate item for wool' + new Item modVersion:modVersion, name:'Wool' + expect(-> new Item modVersion:modVersion, name:'Wool').to.throw Error, 'duplicate item for Wool' it 'adds an item indexes by its slug', -> - modVersion.addItem new Item name:'Wool' + new Item modVersion:modVersion, name:'Wool' modVersion.items.wool.name.should.equal 'Wool' describe 'compareTo', -> @@ -56,26 +56,26 @@ describe 'ModVersion', -> describe 'findItemByName', -> it 'locates items by slugified name', -> - modVersion.addItem new Item name:'Crafting Table' + new Item modVersion:modVersion, name:'Crafting Table' modVersion.findItemByName('Crafting Table').slug.should.equal 'crafting_table' describe 'gatherNames', -> it 'skips names already found', -> - modVersion.addItem new Item name:'Wool' - modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + new Item modVersion:modVersion, name:'Wool' + new Item modVersion:modVersion, name:'Oak Wood Planks', recipes:['foo'] names = modVersion.gatherNames {wool:true} names.wool.should.be.true names.oak_wood_planks.value.should.equal 'Oak Wood Planks' it 'only includes craftable items', -> - modVersion.addItem new Item name:'Wool' - modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + new Item modVersion:modVersion, name:'Wool' + new Item modVersion:modVersion, name:'Oak Wood Planks', recipes:['foo'] names = modVersion.gatherNames() _.keys(names).should.eql ['oak_wood_planks'] it 'computes the proper value and label', -> - modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + new Item modVersion:modVersion, name:'Oak Wood Planks', recipes:['foo'] names = modVersion.gatherNames() names.oak_wood_planks.value.should.equal 'Oak Wood Planks' names.oak_wood_planks.label.should.equal 'Oak Wood Planks (from Test 0.0)' @@ -83,13 +83,13 @@ describe 'ModVersion', -> describe 'hasRecipe', -> it 'returns false for an unknown item', -> - modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + new Item modVersion:modVersion, name:'Oak Wood Planks', recipes:['foo'] modVersion.hasRecipe('Pineapple Upside-Down Cake').should.be.false it 'returns false for a un-craftable item', -> - modVersion.addItem new Item name:'Wool' + new Item modVersion:modVersion, name:'Wool' modVersion.hasRecipe('Wool').should.be.false it 'returns true for a craftable item', -> - modVersion.addItem new Item name:'Oak Wood Planks', recipes:['foo'] + new Item modVersion:modVersion, name:'Oak Wood Planks', recipes:['foo'] modVersion.hasRecipe('Oak Wood Planks').should.be.true diff --git a/test/mod_version_parsers/v1.test.coffee b/test/mod_version_parsers/v1.test.coffee index 5b69fd552..584251903 100644 --- a/test/mod_version_parsers/v1.test.coffee +++ b/test/mod_version_parsers/v1.test.coffee @@ -15,7 +15,7 @@ modVersion = parser = null ######################################################################################################################## -describe "V1", -> +describe "ModParserVersion.V1", -> beforeEach -> parser = new V1 @@ -69,7 +69,7 @@ describe "V1", -> modVersion.items['wool'].isGatherable.should.be.true it 'marks an existing item as gatherable', -> - modVersion.addItem new Item name:'Wool' + item = new Item modVersion:modVersion, name:'Wool' modVersion.items['wool'].isGatherable.should.be.false parser._parseRawMaterials ['Wool'] modVersion.items['wool'].isGatherable.should.be.true diff --git a/test/mod_version_parsers/v2.test.coffee b/test/mod_version_parsers/v2.test.coffee new file mode 100644 index 000000000..29cd53099 --- /dev/null +++ b/test/mod_version_parsers/v2.test.coffee @@ -0,0 +1,262 @@ +### +Crafting Guide - mod_version_parsers/v2.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +V2 = require '../../src/scripts/models/mod_version_parsers/v2' + +######################################################################################################################## + +baseText = parser = null + +######################################################################################################################## + +describe 'ModVersionParser.V2', -> + + beforeEach -> parser = new V2 + + describe 'Item', -> + + beforeEach -> baseText = 'name:Alpha Bravo; version:1; ' + + it 'allows multiple recipes', -> + recipes = "item: Charlie; + recipe:; input:Alpha; pattern:... .0. ...; + recipe:; input:Bravo; pattern:... 0.0 ...;" + modVersion = parser.parse baseText + recipes + recipes = modVersion.items.charlie.recipes + recipes[0].input[0].itemSlug.should.equal 'alpha' + recipes[1].input[0].itemSlug.should.equal 'bravo' + + describe 'name', -> + + it 'adds the name when present', -> + modVersion = parser.parse baseText + 'item: Charlie' + modVersion.items.charlie.name.should.equal 'Charlie' + + it 'requires a non-empty name', -> + func = -> parser.parse baseText + 'item: \n' + expect(func).to.throw Error, 'cannot be empty' + + describe 'gatherable', -> + + it 'adds "gatherable" when present', -> + modVersion = parser.parse baseText + 'item: Alpha Bravo; gatherable: yes' + modVersion.items.alpha_bravo.isGatherable.should.be.true + + it 'does not allow a duplicate "gatherable" declaration', -> + func = -> parser.parse baseText + 'item: Alpha Bravo; gatherable: yes; gatherable: yes' + expect(func).to.throw Error, 'duplicate' + + it 'requires "gatherable" to be "yes" or "no"', -> + func = -> parser.parse baseText + 'item: Alpha Bravo; gatherable: true' + expect(func).to.throw Error, 'gatherable must be' + + it 'does not allow "gatherable" before "item"', -> + func = -> parser.parse baseText + 'gatherable: yes; item: Alpha Bravo; gatherable: yes' + expect(func).to.throw Error, '"gatherable" before "item"' + + describe 'ModVersion', -> + + it 'allows declarations in any order', -> + modVersion = parser.parse 'item: Alpha; version:1; name:Bravo Charlie' + modVersion.name.should.equal 'Bravo Charlie' + modVersion.version.should.equal '1' + modVersion.items.alpha.name.should.equal 'Alpha' + + it 'does not allow duplicate item declarations', -> + func = -> parser.parse 'version:1; name:Alpha Bravo; item:Charlie; item:Charlie' + expect(func).to.throw Error, 'duplicate item for Charlie' + + it 'allows multiple items', -> + modVersion = parser.parse 'name:Alpha; version:1; item:Bravo; item:Charlie' + _.keys(modVersion.items).sort().should.eql ['bravo', 'charlie'] + + describe 'name', -> + + it 'adds "name" when present', -> + modVersion = parser.parse 'name:Alpha Bravo; version:1' + modVersion.name.should.equal 'Alpha Bravo' + + it 'does not allow a duplicate "name" declaration', -> + func = -> parser.parse 'name:Alpha Bravo; version:1; name:Charlie' + expect(func).to.throw Error, 'duplicate declaration of "name"' + + it 'requires a "name" declaration', -> + func = -> parser.parse 'version:1; item:Alpha' + expect(func).to.throw Error, 'the "name" declaration is required' + + describe 'version', -> + + it 'adds "version" when present', -> + modVersion = parser.parse 'name:Alpha Bravo; version:1' + modVersion.version.should.equal '1' + + it 'does not allow a duplicate "version" declaration', -> + func = -> parser.parse 'name:Alpha Bravo; version:1; item:Charlie; version:2' + expect(func).to.throw Error, 'duplicate declaration of "version"' + + it 'requires a "version" declaration', -> + func = -> parser.parse 'name:Alpha Bravo; item:Charlie' + expect(func).to.throw Error, 'the "version" declaration is required' + + describe 'description', -> + + it 'adds "description" when present', -> + modVersion = parser.parse 'name:Alpha; version:1; description:Charlie Delta' + modVersion.description.should.equal 'Charlie Delta' + + it 'does not allow a duplicate "description" declaration', -> + func = -> parser.parse 'name:Alpha; version:1; description:Bravo; description:Charlie' + expect(func).to.throw Error, 'duplicate declaration of "description"' + + describe 'Recipe', -> + + beforeEach -> baseText = 'name:Alpha Bravo; version:1; item: Charlie; ' + + describe 'input', -> + + it 'adds "input" when present', -> + modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern: ... 012 ...' + slugs = (s.itemSlug for s in modVersion.items.charlie.recipes[0].input) + slugs.should.eql ['alpha', 'bravo', 'charlie'] + + it 'requires an "input" declaration', -> + func = -> parser.parse baseText + 'recipe:; pattern: ... .0. ...' + expect(func).to.throw Error, 'the "input" declaration is required' + + it 'does not allow a duplicate "input" declaration', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:....0....; input:Bravo' + expect(func).to.throw Error, 'duplicate declaration of "input"' + + it 'does not allow "input" before "recipe"', -> + func = -> parser.parse baseText + 'input:Alpha, Bravo; recipe:; pattern:....0....' + expect(func).to.throw Error, 'cannot declare "input" before "recipe"' + + it 'registers slugs for each input name', -> + modVersion = parser.parse baseText + 'recipe:; input:Delta, Echo, Foxtrot; pattern:...012...' + _.keys(modVersion.names).sort().should.eql ['charlie', 'delta', 'echo', 'foxtrot'] + + describe 'pattern', -> + + it 'adds "pattern" when present', -> + modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:... .0. .1.' + modVersion.items.charlie.recipes[0].pattern.should.equal '... .0. .1.' + + it 'requires a "pattern" declaration', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo' + expect(func).to.throw Error, 'the "pattern" declaration is required' + + it 'does not allow a duplicate "pattern" declaration', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern:....0..1.; pattern:01.......' + expect(func).to.throw Error, 'duplicate declaration of "pattern"' + + it 'requires pattern to be the right length', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:000' + expect(func).to.throw Error, 'a pattern must have' + + it 'requires pattern to only use proper characters', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:abc def ghi' + expect(func).to.throw Error, 'a pattern must have' + + it 'requires pattern to only refer to existing items', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha; pattern:... 010 ...' + expect(func).to.throw Error, 'there is no input 1 in this recipe' + + it 'requires all items to appear in the pattern', -> + func = -> parser.parse baseText + 'recipe:; input:Alpha, Bravo; pattern: 000 0.0 000' + expect(func).to.throw Error, 'Bravo is an input' + + it 'computes the input stack sizes from the pattern', -> + modVersion = parser.parse baseText + 'recipe:; input:Alpha, Bravo, Charlie; pattern:111 .0. 2.2' + recipe = modVersion.items.charlie.recipes[0] + recipe.input[0].quantity.should.equal 1 + recipe.input[1].quantity.should.equal 3 + recipe.input[2].quantity.should.equal 2 + + it 'does not allow "pattern" before "recipe"', -> + func = -> parser.parse baseText + 'pattern:... .0. ...; recipe:; inputs:Alpha' + expect(func).to.throw Error, 'cannot declare "pattern" before "recipe"' + + describe 'quantity', -> + + beforeEach -> + baseText = 'name:Alpha Bravo; version:1; item: Charlie; recipe:; input:Alpha; pattern:...0.0...; ' + + it 'adds "quantity" when present', -> + modVersion = parser.parse baseText + 'quantity: 2' + modVersion.items.charlie.recipes[0].output[0].quantity.should.equal 2 + + it 'does not allow a duplicate "quantity" declaration', -> + func = -> parser.parse baseText + 'quantity:1; quantity:2' + expect(func).to.throw Error, 'duplicate declaration of "quantity"' + + it 'requires quantity to be an integer', -> + func = -> parser.parse baseText + 'quantity:ten' + expect(func).to.throw Error, 'quantity must be an integer' + + it 'assumes a quantity of 1 by default', -> + modVersion = parser.parse baseText + modVersion.items.charlie.recipes[0].output[0].quantity.should.equal 1 + + it 'does not allow "quantity" before recipe', -> + func = -> parser.parse 'name:Alpha; version:1; item:Bravo; quantity:12; recipe:;' + expect(func).to.throw Error, 'cannot declare "quantity" before "recipe"' + + describe 'output', -> + + beforeEach -> + baseText = 'name:Alpha; version:1; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; ' + + it 'adds a single item as the default output', -> + modVersion = parser.parse baseText + stack = modVersion.items.bravo.recipes[0].output[0] + stack.itemSlug.should.equal 'bravo' + stack.quantity.should.equal 1 + + it 'can add multiple extras with quantities', -> + modVersion = parser.parse baseText + 'extras:2 Delta, 4 Echo' + output = modVersion.items.bravo.recipes[0].output + output[0].itemSlug.should.equal 'bravo' + output[0].quantity.should.equal 1 + output[1].itemSlug.should.equal 'delta' + output[1].quantity.should.equal 2 + output[2].itemSlug.should.equal 'echo' + output[2].quantity.should.equal 4 + + it 'does not allow "extras" before "recipe"', -> + func = -> parser.parse 'name:Alpha; version:1; item:Bravo; extras:Charlie' + expect(func).to.throw Error, 'cannot declare "extras" before "recipe"' + + it 'registers slugs for each output name', -> + modVersion = parser.parse baseText + 'extras:Delta, Echo' + _.keys(modVersion.names).sort().should.eql ['bravo', 'charlie', 'delta', 'echo'] + + it 'does not allow a duplicate "extras" declaration', -> + func = -> parser.parse baseText + 'extras:Echo; extras:Delta' + expect(func).to.throw Error, 'duplicate declaration of "extras"' + + describe 'tools', -> + + beforeEach -> + baseText = 'name:Alpha; version:1; item:Bravo; recipe:; input:Charlie; pattern:... .0. ...; ' + + it 'can add a single tool', -> + modVersion = parser.parse baseText + 'tools: Furnace' + modVersion.items.bravo.recipes[0].tools[0].itemSlug.should.equal 'furnace' + + it 'can add multiple tools', -> + modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace' + tools = modVersion.items.bravo.recipes[0].tools + tools[0].itemSlug.should.equal 'crafting_table' + tools[1].itemSlug.should.equal 'furnace' + + it 'registers slugs for each tool name', -> + modVersion = parser.parse baseText + 'tools: Crafting Table, Furnace' + _.keys(modVersion.names).sort().should.eql ['bravo', 'charlie', 'crafting_table', 'furnace'] + + it 'does not allow a duplicate "tools" declaration', -> + func = -> parser.parse baseText + 'tools:Crafting Table; tools:Furnace' + expect(func).to.throw Error, 'duplicate declaration of "tools"' diff --git a/test/string_builder.test.coffee b/test/string_builder.test.coffee new file mode 100644 index 000000000..486ebe5e7 --- /dev/null +++ b/test/string_builder.test.coffee @@ -0,0 +1,84 @@ +### +Crafting Guide - string_builder.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +StringBuilder = require '../src/scripts/models/string_builder' + +######################################################################################################################## + +builder = null + +######################################################################################################################## + +describe 'StringBuilder', -> + + beforeEach -> builder = new StringBuilder + + describe 'call', -> + + it 'works with no arguments', -> + builder.call (b)-> b.push 'foo' + builder.toString().should.equal 'foo' + + it 'works with multiple arguments', -> + builder.call 'foo', 'bar', 'baz', (builder, a, b, c)-> builder.loop [a, b, c] + builder.toString().should.equal 'foo, bar, baz' + + describe 'loop', -> + + it 'can make an empty list', -> + builder.loop [], start:'[', end:']' + builder.toString().should.equal '[]' + + it 'can make a list with a single element', -> + builder.loop ['foo'], start:'[', end:']' + builder.toString().should.equal '[foo]' + + it 'can make a list with many elements', -> + builder.loop ['foo', 'bar', 'baz'], start:'[', end:']' + builder.toString().should.equal '[foo, bar, baz]' + + it 'can make a list with a custom callback', -> + builder.loop ['foo', 'bar', 'baz'], start:'[', end:']', onEach:(b, i)-> b.push "\"#{i}\"" + builder.toString().should.equal '["foo", "bar", "baz"]' + + it 'can use a custom delimiter', -> + builder.loop ['foo', 'bar', 'baz'], delimiter:'|' + builder.toString().should.equal 'foo|bar|baz' + + it 'can indent content', -> + builder.loop ['foo', 'bar', 'baz'], start:'[\n', end:'\n]', delimiter:',\n', indent:true + builder.toString().should.equal '[\n foo,\n bar,\n baz\n]' + + describe 'onlyIf', -> + + it 'calls the callback on true', -> + builder.onlyIf true, (b)-> b.push 'foo' + builder.toString().should.equal 'foo' + + describe 'push', -> + + it 'can build a simple string', -> + builder.push('foo').push(' bar').push(' baz') + builder.toString().should.equal 'foo bar baz' + + it 'can build a multi-line string', -> + builder.push('foo').push('\nbar\n').push('baz') + builder.toString().should.equal 'foo\nbar\nbaz' + + it 'can build an indented multi-line string', -> + builder + .push 'foo\n' + .indent() + .push 'bar\n' + .outdent() + .push 'baz' + + builder.toString().should.equal 'foo\n bar\nbaz' + + it 'can pick apart multiple newlines in a single chunk', -> + builder.indent().push('foo\nbar\nbaz').outdent().push('\nbif') + builder.toString().should.equal 'foo\n bar\n baz\nbif' \ No newline at end of file diff --git a/test/test.coffee b/test/test.coffee index c160ca3a2..009b2a309 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -16,6 +16,7 @@ if typeof(global) is 'undefined' global.assert = chai.assert global.expect = chai.expect global.should = chai.should() +global.util = require 'util' Logger = require '../src/scripts/logger' global.logger = new Logger level:Logger.TRACE @@ -32,6 +33,8 @@ require './inventory_parser.test' require './mod_pack.test' require './mod_version.test' require './mod_version_parsers/v1.test' +require './mod_version_parsers/v2.test' +require './string_builder.test' mocha.checkLeaks() mocha.globals ['LiveReload']