Massive refactoring to clean up models

* Tweak style of quantity label in output box to be more visible
* Move data files for all mods under a directory for the specific
  version of the mod
* Add a `silent` property to the base model to prevent models which
  shouldn't be observed from emitting events
* Change the term "itemSlug" to just "slug" across the board
* Change all models so that they don't require their parent in their
  constructors and so that they don't automatically add themselves to
  their parents lists
* Remove a bunch of unnecessary event triggering
* Tighten up access to various lists of children across all models
* Refactor the guts of the V2 parser into an abstract base class so
  it can be used for other parsers in the future
* Remove a number of unused methods
This commit is contained in:
Andrew Miner
2015-01-17 11:49:07 -08:00
parent bb959264ee
commit 83868e2760
34 changed files with 530 additions and 519 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ ModelState.loaded = 'loaded'
ModelState.failed = 'failed'
exports.Url = Url = {}
Url.itemIcon = _.template "/data/<%= modSlug %>/<%= modVersion %>/images/<%= itemSlug %>.png"
Url.itemIcon = _.template "/data/<%= modSlug %>/<%= modVersion %>/images/<%= slug %>.png"
Url.item = _.template "/item/<%= encodeURIComponent(itemName) %>"
Url.mod = _.template "/data/<%= modSlug %>/mod.cg"
Url.modVersion = _.template "/data/<%= modSlug %>/<%= modVersion %>/mod-version.cg"
@@ -74,6 +74,7 @@ module.exports = class BaseController extends Backbone.View
return unless @onWillChangeModel @_model, newModel
@_model = newModel
@_model.silent = false
@_tryRefresh()
# Backbone.View Overrides ######################################################################
@@ -25,6 +25,8 @@ module.exports = class CraftingTableController extends BaseController
@imageLoader = options.imageLoader
@modPack = options.modPack
@model.plan.silent = false
# Event Methods ################################################################################
onNextClicked: ->
@@ -79,7 +81,7 @@ module.exports = class CraftingTableController extends BaseController
outputStack = @model.output
if outputStack?
display = @modPack.findItemDisplay outputStack.itemSlug
display = @modPack.findItemDisplay outputStack.slug
@$outputLink.attr 'href', display.itemUrl
@$outputLink.attr 'title', display.itemName
@$outputImg.attr 'alt', display.itemName
@@ -32,8 +32,9 @@ module.exports = class ModPackController extends BaseController
onWillRender: ->
for attributes in DefaultModVersions
modVersion = new ModVersion _.extend attributes, modPack:@model
modVersion = new ModVersion attributes
modVersion.fetch()
@model.addModVersion modVersion
onDidRender: ->
@$table = @$('table')
@@ -47,13 +48,14 @@ module.exports = class ModPackController extends BaseController
return
index = 0
while index < Math.min @_controllers.length, @model.modVersions.length
modVersions = @model.getModVersions()
while index < Math.min @_controllers.length, modVersions.length
controller = @_controllers[index]
controller.model = @model.modVersions[index]
controller.model = modVersions[index]
index++
while @_controllers.length < @model.modVersions.length
controller = new ModVersionController model:@model.modVersions[index]
while @_controllers.length < modVersions.length
controller = new ModVersionController model:modVersions[index]
controller.render()
@_controllers.push controller
controller.$el.hide duration:0
@@ -61,7 +63,7 @@ module.exports = class ModPackController extends BaseController
controller.$el.slideDown duration:Duration.normal
index++
while @_controllers.length > @model.modVersions.length
while @_controllers.length > modVersions.length
controller = @_controllers.pop()
controller.$el.slideUp duration:Duration.normal, complete:-> controller.$el.remove()
@@ -31,7 +31,7 @@ module.exports = class StackController extends BaseController
super
refresh: ->
display = @modPack.findItemDisplay @model.itemSlug
display = @modPack.findItemDisplay @model.slug
@$nameField.html display.itemName
@$quantityField.html @model.quantity
+1
View File
@@ -6,6 +6,7 @@ All rights reserved.
###
require './underscore_mixins'
require './polyfill'
views = require './views'
FeedbackController = require './controllers/feedback_controller'
+10
View File
@@ -0,0 +1,10 @@
###
Crafting Guide - base_collection.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class BaseCollection extends Backbone.Collection
+5 -3
View File
@@ -13,7 +13,7 @@ All rights reserved.
module.exports = class BaseModel extends Backbone.Model
constructor: (attributes={}, options={})->
options.allowedSyncMethods = []
options.silent ?= true
super attributes, options
makeGetter = (name)-> return -> @get name
@@ -22,9 +22,9 @@ module.exports = class BaseModel extends Backbone.Model
continue if name is 'id'
Object.defineProperty this, name, get:makeGetter(name), set:makeSetter(name)
@allowedSyncMethods = options.allowedSyncMethods
@silent = options.silent
@state = ModelState.unloaded
@state = ModelState.unloaded
@on 'request', => @state = ModelState.loading
@on 'sync', => @state = ModelState.loaded
@on 'error', => @state = ModelState.error
@@ -61,6 +61,7 @@ module.exports = class BaseModel extends Backbone.Model
success: (text, status, xhr)=> resolve @onLoadSucceeded text, status, xhr
error: (xhr, status, error)=> reject @onLoadFailed error, status, xhr
@loading.catch -> # do nothing. prevents unhandled promise warnings
return @loading
parse: (text)->
@@ -70,6 +71,7 @@ module.exports = class BaseModel extends Backbone.Model
throw new Error "#{@constructor.name} (#{@cid}) is not permitted to #{method}"
trigger: (name)->
return if @silent
logger.trace "#{@constructor.name}.#{@cid} triggered a \"#{name}\" event"
super
@@ -0,0 +1,89 @@
###
Crafting Guide - command_parser_base.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
module.exports = class CommandParserBase
constructor: (options={})->
if not options.model? then throw new Error 'options.model is required'
options.showAllErrors ?= false
@_model = options.model
@_showAllErrors = options.showAllErrors
# Class Methods ################################################################################
@COMMAND = /\ *([^:]*):?(.*)/
@COMMENT = /([^\\]?)#.*/
# Public Methods ###############################################################################
parse: (text)->
@_rawData = {}
@_lineNumber = 1
lines = text.split '\n'
for i in [0...lines.length]
@_lineNumber = i + 1
commands = @_parseLine lines[i]
for command in commands
@_handleErrors @_execute, command
@_handleErrors @_buildModel, @_rawData, @_model
return @_model
unparse: ->
builder = new StringBuilder context:@model
@_unparseModel builder, model
return builder.toString()
# Subclass Methods #############################################################################
_buildModel: (rawData, model)->
throw new Error 'Subclasses must override this method'
_unparseModel: (builder, model)->
throw new Error 'Subclasses must override this method'
# Private Methods ##############################################################################
_execute: (command)->
method = this["_command_#{command.name}"]
if not method? then throw new Error "Unknown command: #{command.name}"
@_handleErrors method, command.args
_parseLine: (line)->
line = line.replace CommandParserBase.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 = CommandParserBase.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
_handleErrors: (callback, args...)->
if args.length is 1 and _.isArray(args[0]) then args = args[0]
try
callback.apply this, args
catch e
e.message = "line #{@_lineNumber}: #{e.message}"
if not @_showAllErrors then throw e
logger.error e.message
+7 -6
View File
@@ -18,16 +18,17 @@ module.exports = class CraftingGrid extends BaseModel
attributes.recipe ?= null
super attributes, options
Object.defineProperty @prototype, 'slotCount', get:-> CraftingGrid.SLOT_COUNT
Object.defineProperties this,
'slotCount': { get:-> CraftingGrid.SLOT_COUNT }
# Public Methods ###############################################################################
getItemDisplayAt: (index)->
if index >= @slotCount then throw new Error "index (#{index}) must be less than #{@slotCount}"
getItemDisplayAt: (slot)->
if slot >= @slotCount then throw new Error "slot (#{slot}) must be less than #{@slotCount}"
return null unless @recipe?
itemSlug = @recipe.getItemSlugAt index
return null unless itemSlug?
slug = @recipe.getItemSlugAt slot
return null unless slug?
itemDisplay = @modPack.findItemDisplay itemSlug
itemDisplay = @modPack.findItemDisplay slug
return itemDisplay
+39 -32
View File
@@ -6,6 +6,7 @@ All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
Inventory = require './inventory'
########################################################################################################################
@@ -17,44 +18,50 @@ module.exports = class CraftingPlan extends BaseModel
attributes.includingTools ?= false
super attributes, options
@have = new Inventory
@want = new Inventory
@need = new Inventory
@result = new Inventory
@storage = options.storage
@have = new Inventory
@want = new Inventory
@need = new Inventory
@result = new Inventory
@have.on 'change', => @craft()
@want.on 'change', => @craft()
@modPack.on 'change', => @craft()
@clear silent:true
@on 'change:includingTools', => @craft()
@have.on Event.change, => @craft()
@want.on Event.change, => @craft()
@modPack.on Event.add, => @craft()
@clear()
@on Event.change + ':includingTools', => @craft()
# Public Methods ###############################################################################
clear: ->
clear: (options={})->
options.silent ?= false
@steps = []
@need.clear()
@result.clear()
@trigger 'change', this
@trigger 'change', this unless options.silent
return this
craft: ->
@clear()
@clear silent:true
@need.silent = @result.silent = true
@result.addInventory @have
@steps = {}
@want.each (stack)=>
@_findSteps stack.itemSlug
@need.add stack.itemSlug, stack.quantity
@_findSteps stack.slug
@need.add stack.slug, stack.quantity
@steps = _.values @steps
@_resolveNeeds()
@_removeExtraSteps()
@result.addInventory @want
@need.silent = @result.silent = false
@need.trigger 'change', @need
@result.trigger 'change', @result
@trigger 'change', this
# Event Methods ################################################################################
@@ -82,10 +89,10 @@ module.exports = class CraftingPlan extends BaseModel
@steps[recipe.slug] = recipe:recipe
_chooseRecipe: (item)->
return item.recipes[0]
return item.getPrimaryRecipe()
_findSteps: (itemSlug)->
item = @modPack.findItem itemSlug
_findSteps: (slug)->
item = @modPack.findItem slug
return unless item?
return unless item.isCraftable
return if item.isGatherable
@@ -94,16 +101,16 @@ module.exports = class CraftingPlan extends BaseModel
if @includingTools
for toolStack in recipe.tools
if not @_hasStep toolStack.itemSlug
@_findSteps toolStack.itemSlug
if not @_hasStep toolStack.slug
@_findSteps toolStack.slug
for inputStack in recipe.input
@_findSteps inputStack.itemSlug
@_findSteps inputStack.slug
@_addStep recipe
_hasStep: (itemSlug)->
return @steps[itemSlug]?
_hasStep: (slug)->
return @steps[slug]?
_removeExtraSteps: ->
result = (step for step in @steps when step.multiplier > 0)
@@ -118,25 +125,25 @@ module.exports = class CraftingPlan extends BaseModel
if @includingTools
for stack in recipe.tools
slug = stack.itemSlug
slug = stack.slug
available = @result.quantityOf(slug) + @need.quantityOf(slug)
needed = Math.max 0, stack.quantity - available
@need.add stack.itemSlug, needed
@result.add stack.itemSlug, needed
@need.add stack.slug, needed
@result.add stack.slug, needed
for stack in recipe.input
needed = step.multiplier * stack.quantity
consumed = Math.min needed, @result.quantityOf(stack.itemSlug)
consumed = Math.min needed, @result.quantityOf(stack.slug)
remaining = needed - consumed
@result.remove stack.itemSlug, consumed
@need.add stack.itemSlug, remaining
@result.remove stack.slug, consumed
@need.add stack.slug, remaining
for stack in recipe.output
created = stack.quantity * step.multiplier
consumed = Math.min created, @need.quantityOf stack.itemSlug
consumed = Math.min created, @need.quantityOf stack.slug
remaining = created - consumed
@result.add stack.itemSlug, remaining
@need.remove stack.itemSlug, consumed
@result.add stack.slug, remaining
@need.remove stack.slug, consumed
+1 -1
View File
@@ -81,7 +81,7 @@ module.exports = class CraftingTable extends BaseModel
getToolNames: ->
recipe = @plan.steps[@_step]?.recipe
return '' unless recipe?
toolSlugs = (stack.itemSlug for stack in recipe.tools)
toolSlugs = (stack.slug for stack in recipe.tools)
toolNames = (@modPack.findName(slug) for slug in toolSlugs).join ', '
return toolNames
+42 -39
View File
@@ -15,78 +15,80 @@ module.exports = class Inventory extends BaseModel
constructor: (attributes={}, options={})->
super attributes, options
@clear()
@clear silent:true
Object.defineProperty @prototype, 'isEmpty', get:-> @_slugs.length is 0
Object.defineProperties this,
isEmpty: { get:-> @_slugs.length is 0 }
# Public Methods ###############################################################################
add: (itemSlug, quantity=1)->
@_add itemSlug, quantity
@trigger Event.add, this, itemSlug, quantity
add: (slug, quantity=1)->
@_add slug, quantity
@trigger Event.add, this, slug, quantity
@trigger Event.change, this
return this
addInventory: (inventory)->
inventory.each (stack)=> @_add stack.itemSlug, stack.quantity
@silent = true
inventory.each (stack)=> @_add stack.slug, stack.quantity
@silent = false
@trigger Event.change, this
return this
clear: ->
clear: (options={})->
options.silent ?= false
@_stacks = {}
@_slugs = []
@trigger 'change', self
@trigger Event.change, this unless options.silent
clone: ->
inventory = new Inventory
@each (stack)-> inventory.add stack.itemSlug, stack.quantity
inventory.addInventory this
return inventory
each: (onStack)->
for itemSlug in @_slugs
stack = @_stacks[itemSlug]
onStack stack
each: (callback)->
for slug in @_slugs
callback @_stacks[slug]
getStack: (itemSlug)->
return @_stacks[itemSlug]
hasAtLeast: (itemSlug, quantity=1)->
hasAtLeast: (slug, quantity=1)->
if quantity is 0 then return true
stack = @_stacks[itemSlug]
stack = @_stacks[slug]
return false unless stack?
return stack.quantity >= quantity
pop: ->
itemSlug = @_slugs.pop()
return null unless itemSlug?
slug = @_slugs.pop()
return null unless slug?
stack = @_stacks[itemSlug]
delete @_stacks[itemSlug]
stack = @_stacks[slug]
delete @_stacks[slug]
@trigger Event.remove, this, stack.itemSlug, stack.quantity
@trigger Event.remove, this, stack.slug, stack.quantity
@trigger Event.change, this
return stack
quantityOf: (itemSlug)->
stack = @_stacks[itemSlug]
quantityOf: (slug)->
stack = @_stacks[slug]
return 0 unless stack?
return stack.quantity
remove: (itemSlug, quantity=1)->
remove: (slug, quantity=1)->
return if quantity is 0
stack = @_stacks[itemSlug]
if not stack? then throw new Error "cannot remove #{itemSlug} since it is not in this inventory"
stack = @_stacks[slug]
if not stack? then throw new Error "cannot remove #{slug} since it is not in this inventory"
if stack.quantity < quantity
throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{itemSlug} in this inventory"
throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{slug} in this inventory"
stack.quantity -= quantity
if stack.quantity is 0
delete @_stacks[itemSlug]
@_slugs = _(@_slugs).without itemSlug
delete @_stacks[slug]
@_slugs = _(@_slugs).without slug
@trigger Event.remove, this, itemSlug, quantity
@trigger Event.remove, this, slug, quantity
@trigger Event.change, this
return this
@@ -94,9 +96,9 @@ module.exports = class Inventory extends BaseModel
result = []
@each (stack)->
if stack.quantity > 1
result.push [stack.quantity, stack.itemSlug]
result.push [stack.quantity, stack.slug]
else
result.push stack.itemSlug
result.push stack.slug
return result
# Object Overrides #############################################################################
@@ -116,14 +118,15 @@ module.exports = class Inventory extends BaseModel
# Private Methods ##############################################################################
_add: (itemSlug, quantity=1)->
_add: (slug, quantity=1)->
return unless slug?
return if quantity is 0
stack = @_stacks[itemSlug]
stack = @_stacks[slug]
if not stack?
stack = new Stack itemSlug:itemSlug, quantity:quantity
@_stacks[itemSlug] = stack
@_slugs.push itemSlug
stack = new Stack slug:slug, quantity:quantity
@_stacks[slug] = stack
@_slugs.push slug
@_slugs.sort()
else
stack.quantity += quantity
+19 -17
View File
@@ -5,33 +5,38 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
BaseCollection = require './base_collection'
BaseModel = require './base_model'
{Event} = require '../constants'
Recipe = require './recipe'
########################################################################################################################
module.exports = class Item extends BaseModel
@DEFAULT_STACK_SIZE = 64
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.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
@_recipes = []
Object.defineProperties this,
'isCraftable': { get:-> @_recipes.length > 0 }
# Public Methods ###############################################################################
addRecipe: (recipe)->
if recipe.item isnt this then throw new Error "cannot add a recipe which isn't associated with this item"
@recipes.push recipe
if recipe.slug isnt @slug then throw new Error "cannot add a recipe for #{recipe.slug} to #{@slug}"
@_recipes.push recipe
eachRecipe: (callback)->
for recipe in @_recipes
callback recipe
getPrimaryRecipe: ->
return @_recipes[0]
compareTo: (that)->
if this.slug isnt that.slug
@@ -52,13 +57,10 @@ module.exports = class Item extends BaseModel
if _.slugify(@name) isnt @slug
result.push ', slug:'; result.push @slug
if @stackSize isnt Item.DEFAULT_STACK_SIZE
result.push ', stackSize:'; result.push @stackSize
if @recipes.length > 0
result.push ', recipes:'
result.push @recipes.length
result.push ' items'
result.push ', recipes:«'
result.push @_recipes.length
result.push ' items»'
result.push '}'
return result.join ''
+2 -2
View File
@@ -41,7 +41,7 @@ module.exports = class ItemPage extends BaseModel
_updateLocation: ->
list = @plan.want.toList()
if list.length is 1
itemSlug = if _.isArray(list[0]) then list[0][1] else list[0]
router.navigate "/item/#{itemSlug}"
slug = if _.isArray(list[0]) then list[0][1] else list[0]
router.navigate "/item/#{slug}"
else
router.navigate "/"
+2 -5
View File
@@ -6,6 +6,7 @@ All rights reserved.
###
BaseModel = require './base_model'
{Url} = require '../constants'
########################################################################################################################
@@ -20,9 +21,5 @@ module.exports = class Mod extends BaseModel
parse: (response)->
sync: (method, model, options={})->
if method isnt 'read' then throw new Error "Mod data can only be read, not #{method}d"
super method, model, options
url: ->
return "/data/#{@slug}/mod.cg"
return Url.mod modSlug:@slug
+33 -39
View File
@@ -16,35 +16,18 @@ ModVersionParser = require './mod_version_parser'
module.exports = class ModPack extends BaseModel
constructor: (attributes={}, options={})->
attributes.modVersions ?= []
super attributes, options
@_modVersions = []
# Public Methods ###############################################################################
addModVersion: (modVersion)->
if modVersion.modPack isnt this then throw new Error "the mod version must be associated with this mod pack"
return if @modVersions.indexOf(modVersion) isnt -1
@modVersions.push modVersion
@trigger Event.add, modVersion, this
@modVersions.sort (a, b)-> a.compareTo b
@trigger Event.sort, this
@trigger Event.change, this
return this
enableModsForItem: (name)->
for modVersion in @modVersions
continue if modVersion.enabled
if modVersion.hasRecipe name
modVersion.enabled = true
findItem: (itemSlug, options={})->
findItem: (slug, options={})->
options.includeDisabled ?= false
for modVersion in @modVersions
for modVersion in @_modVersions
continue unless modVersion.enabled or options.includeDisabled
item = modVersion.items[itemSlug]
item = modVersion.findItem slug
return item if item?
return null
@@ -53,9 +36,9 @@ module.exports = class ModPack extends BaseModel
options.includeDisabled ?= false
slug = _.slugify name
for modVersion in @modVersions
for modVersion in @_modVersions
continue unless modVersion.enabled or options.includeDisabled
item = modVersion.items[slug]
item = modVersion.findItem slug
return item if item?
return null
@@ -63,12 +46,12 @@ module.exports = class ModPack extends BaseModel
findName: (slug, options={})->
options.includeDisabled ?= false
for modVersion in @modVersions
for modVersion in @_modVersions
continue unless modVersion.enabled or options.includeDisabled
name = modVersion.findName slug
return name if name
return slug
return null
findItemDisplay: (slug)->
result = {}
@@ -76,39 +59,50 @@ module.exports = class ModPack extends BaseModel
if item?
result.modSlug = item.modVersion.slug
result.modVersion = item.modVersion.version
result.itemSlug = item.slug
result.slug = item.slug
result.itemName = item.name
else
result.modSlug = _.slugify DefaultModVersions[0].name
result.modVersion = DefaultModVersions[0].version
result.itemSlug = slug
result.slug = slug
result.itemName = @findName slug, includeDisabled:true
result.iconUrl = Url.itemIcon result
result.itemUrl = Url.item result
return result
hasRecipe: (name, options={})->
options.includeDisabled ?= false
for modVersion in @modVersions
continue unless modVersion.enabled or options.includeDisabled
return true if modVersion.hasRecipe name
return false
isValidName: (name, options={})->
options.includeDisabled ?= false
slug = _.slugify name
for modVersion in @modVersions
for modVersion in @_modVersions
continue unless modVersion.enabled or options.includeDisabled
name = modVersion.findName slug
return true if name
return false
# Property Methods #############################################################################
addModVersion: (modVersion)->
return if @_modVersions.indexOf(modVersion) isnt -1
@_modVersions.push modVersion
@trigger Event.add, modVersion, this
@_modVersions.sort (a, b)-> a.compareTo b
@trigger Event.change, this
return this
eachModVersion: (callback)->
for modVersion in @_modVersions
callback modVersion
getModVersions: ->
return @_modVersions[..]
# Object Overrides #############################################################################
toString: ->
return "ModPack (#{@cid}) {modVersions:#{@modVersions.length} items}"
return "ModPack (#{@cid}) {modVersions:#{@_modVersions.length} items}"
+50 -28
View File
@@ -5,40 +5,38 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
{RequiredMods} = require '../constants'
{Url} = require '../constants'
BaseCollection = require './base_collection'
BaseModel = require './base_model'
{Event} = require '../constants'
Item = require './item'
{RequiredMods} = require '../constants'
{Url} = require '../constants'
########################################################################################################################
module.exports = class ModVersion extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.modPack? then throw new Error 'attributes.modPack is required'
if _.isEmpty(attributes.name) then throw new Error 'attributes.name cannot be empty'
if _.isEmpty(attributes.version) then throw new Error 'attributes.version cannot be empty'
if not attributes.name? then throw new Error 'attributes.name is required'
if not attributes.version? then throw new Error 'attributes.version is required'
attributes.description ?= ''
attributes.enabled ?= true
attributes.items ?= {}
attributes.names ?= {}
attributes.slug ?= _.slugify attributes.name
super attributes, options
@modPack.addModVersion this
@_items = {}
@_names = {}
@_slugs = []
# Public Methods ###############################################################################
addItem: (item)->
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
@names[item.slug] = item.name
@trigger Event.add, item, this
@trigger Event.change, this
if @_items[item.slug]? then throw new Error "duplicate item for #{item.name}"
@_items[item.slug] = item
item.modVersion = this
@registerSlug item.slug, item.name
return this
compareTo: (that)->
@@ -56,26 +54,49 @@ module.exports = class ModVersion extends BaseModel
else
return if this.name < that.name then -1 else +1
findName: (slug)->
return @names[slug]
eachItem: (callback)->
for slug in @_slugs
callback @_items[slug], slug
return this
hasRecipe: (name)->
item = @findItemByName name
return false unless item?
return item.recipes.length > 0
eachName: (callback)->
for slug in @_slugs
callback @_names[slug], slug
return this
findItem: (slug)->
return @_items[slug]
findItemByName: (name)->
return @findItem _.slugify name
findName: (slug)->
return @_names[slug]
registerSlug: (slug, name)->
@names[slug] = name
@trigger Event.change + ':names', this, @names
@trigger Event.change, this
hasSlug = @_names[slug]?
@_names[slug] = name
if not hasSlug
@_slugs.push slug
@_slugs.sort()
@_slugs = _.uniq @_slugs, true
return this
# Backbone.Model Overrides #####################################################################
parse: (text)->
currentSilent = @silent
@silent = true
ModVersionParser = require './mod_version_parser' # to avoid require cycles
@_parser ?= new ModVersionParser modVersion:this
@_parser ?= new ModVersionParser model:this
@_parser.parse text
@silent = currentSilent
@trigger Event.change, this
return null # prevent calling `set`
url: ->
@@ -88,4 +109,5 @@ module.exports = class ModVersion extends BaseModel
enabled:#{@enabled},
name:#{@name},
version:#{@version},
items:#{_.keys(@items).length} items}"
items:#{_.keys(@_items).length} items
}"
+4 -8
View File
@@ -16,10 +16,10 @@ module.exports = class ModVersionParser
@CURRENT_VERSION = '2'
constructor: (options={})->
if not options.modVersion? then throw new Error 'options.modVersion is required'
if not options.model? then throw new Error 'options.model is required'
options.showAllErrors ?= false
@_modVersion = options.modVersion
@_model = options.model
@_parsers =
'1': new ModVersionParserV1 options
'2': new ModVersionParserV2 options
@@ -34,13 +34,9 @@ module.exports = class ModVersionParser
parser = @_parsers['2']
if not parser? then throw new Error "cannot parse version #{data.dataVersion} mod descriptions"
parser.parse data
oldLevel = logger.level
logger.level = Logger.WARNING
result = parser.parse data
logger.level = oldLevel
return @_modVersion
return @_model
unparse: (dataVersion=ModVersionParser.CURRENT_VERSION)->
if not modVersion? then throw new Error 'modVersion is required'
+7 -7
View File
@@ -59,19 +59,19 @@ module.exports = class NameFinder
names = []
nameMap = {}
for modVersion in @modPack.modVersions
continue unless modVersion.enabled or @includeDisabledMods
@modPack.eachModVersion (modVersion)=>
return unless modVersion.enabled or @includeDisabledMods
for slug, name of modVersion.names
continue if nameMap[name]
modVersion.eachName (name, slug)=>
return if nameMap[name]
item = modVersion.items[slug]
item = modVersion.findItem slug
if not @includeGatherable
continue unless item? and (not item.isGatherable)
return unless item? and (not item.isGatherable)
scanName = "#{modVersion.name} : #{name}"
if nameHint?
continue unless @_isMatch scanName.toLowerCase(), nameHint
return unless @_isMatch scanName.toLowerCase(), nameHint
nameMap[name] = name
names.push value:name, label:scanName, modVersion:modVersion
@@ -15,8 +15,8 @@ Stack = require '../stack'
module.exports = class ModVersionParserV1
constructor: (options={})->
if not options.modVersion? then throw new Error 'options.modVersion is required'
@_modVersion = options.modVersion
if not options.model? then throw new Error 'options.model is required'
@_model = options.model
@_errorLocation = 'the header information'
parse: (data)->
@@ -27,25 +27,48 @@ module.exports = class ModVersionParserV1
# Private Methods ##############################################################################
_computeDefaultPattern: (input)->
itemCount = input.length
slotCount = _.reduce input, ((total, stack)-> total + stack.quantity), 0
return '... .0. ...' if itemCount is 1 and slotCount is 1
return '00. 00. ...' if itemCount is 1 and slotCount is 4
return '000 000 000' if itemCount is 1 and slotCount is 9
result = ['.', '.', '.', '.', '.', '.', '.', '.', '.']
indexes = [4, 7, 1, 3, 5, 6, 8, 0, 2]
for i in [0...input.length]
stack = input[i]
for j in [0...stack.quantity]
index = indexes.shift()
result[index] = "#{i}"
pattern = result.join ''
pattern = pattern.replace /(...)(...)(...)/, '$1 $2 $3'
return pattern
_findOrCreateItem: (name)->
item = @_modVersion.findItemByName name
item = @_model.findItemByName name
if not item?
item = new Item modVersion:@_modVersion, name:name
@_modVersion.registerSlug item.slug, item.name
item = new Item name:name
@_model.addItem item
return item
# Parsing Methods ##############################################################################
_parseModVersion: (data)->
if not data? then throw new Error 'mod description data is missing'
if not data.name? then throw new Error 'name is required'
if not data.version? then throw new Error 'version is required'
if not _.isArray(data.recipes) then throw new Error 'recipes must be an array'
if data.name isnt @_modVersion.name
throw new Error "the data is for #{data.name}, not #{@_modVersion.name} as expected"
if data.version isnt @_modVersion.version
throw new Error "the data is for version #{data.version}, not #{@_modVersion.version} as expected"
if data.name isnt @_model.name
throw new Error "the data is for #{data.name}, not #{@_model.name} as expected"
if data.version isnt @_model.version
throw new Error "the data is for version #{data.version}, not #{@_model.version} as expected"
@_modVersion.description = data.description or ''
@_model.description = data.description or ''
@_parseRawMaterials data.raw_materials
for index in [0...data.recipes.length]
@@ -54,7 +77,7 @@ module.exports = class ModVersionParserV1
recipe = @_parseRecipe recipeData
recipe._originalIndex = index
return @_modVersion
return @_model
_parseRawMaterials: (data)->
return unless data? and data.length > 0
@@ -84,7 +107,7 @@ module.exports = class ModVersionParserV1
output: @_parseStackList(data.output, field:'output', canBeEmpty:false)
input: @_parseStackList(data.input, field:'input', canBeEmpty:true)
tools: @_parseStackList(data.tools, field:'tools', canBeEmpty:true)
attributes.pattern = data.pattern if data.pattern?
attributes.pattern = data.pattern or @_computeDefaultPattern attributes.input
recipe = new Recipe attributes
return recipe
@@ -102,9 +125,9 @@ module.exports = class ModVersionParserV1
name = data[1]
slug = _.slugify name
@_modVersion.registerSlug slug, name
@_model.registerSlug slug, name
return new Stack itemSlug:slug, quantity:data[0]
return new Stack slug:slug, quantity:data[0]
_parseStackList: (data, options={})->
if not data? then throw new Error "#{@_errorLocation} must have an #{options.field} field"
@@ -191,9 +214,9 @@ module.exports = class ModVersionParserV1
else if stackList.length is 1
stack = stackList[0]
if stack.quantity is 1
result.push '"' + @_modVersion.findName(stack.itemSlug) + '"'
result.push '"' + @_model.findName(stack.slug) + '"'
else
result.push '[[' + stack.quantity + ', "' + @_modVersion.findName(stack.itemSlug) + '"]]'
result.push '[[' + stack.quantity + ', "' + @_model.findName(stack.slug) + '"]]'
else
result.push '['
@@ -202,17 +225,17 @@ module.exports = class ModVersionParserV1
stacks.sort (a, b)->
if a.quantity isnt b.quantity
return if a.quantity > b.quantity then -1 else +1
if a.itemSlug isnt b.itemSlug
return if a.itemSlug < b.itemSlug then -1 else +1
if a.slug isnt b.slug
return if a.slug < b.slug then -1 else +1
return 0
firstItem = true
for stack in stacks
result.push ', ' if not firstItem
if stack.quantity is 1
result.push '"' + @_modVersion.findName(stack.itemSlug) + '"'
result.push '"' + @_model.findName(stack.slug) + '"'
else
result.push '[' + stack.quantity + ', "' + @_modVersion.findName(stack.itemSlug) + '"]'
result.push '[' + stack.quantity + ', "' + @_model.findName(stack.slug) + '"]'
firstItem = false
result.push ']'
@@ -5,97 +5,38 @@ 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'
CommandParserBase = require '../command_parser_base'
Item = require '../item'
ModVersion = require '../mod_version'
Recipe = require '../recipe'
Stack = require '../stack'
StringBuilder = require '../string_builder'
########################################################################################################################
module.exports = class ModVersionParserV2
constructor: (options={})->
if not options.modVersion? then throw new Error 'options.modVersion is required'
options.showAllErrors ?= false
@_modVersion = options.modVersion
@_showAllErrors = options.showAllErrors
module.exports = class ModVersionParserV2 extends CommandParserBase
# Class Methods ################################################################################
@COMMAND = /\ *([^:]*):?(.*)/
@COMMENT = /([^\\]?)#.*/
@INTEGER = /[0-9]+/
@PATTERN = /^[0-9.]{3} ?[0-9.]{3} ?[0-9.]{3}$/
@STACK = /^([0-9]+) +(.*)$/
# Public Methods ###############################################################################
# CommandParserBase Overrides ##################################################################
parse: (text)->
@_modVersionData = {}
@_lineNumber = 1
_buildModel: (rawData, model)->
@_buildModVersion rawData, model
lines = text.split '\n'
for i in [0...lines.length]
@_lineNumber = i + 1
commands = @_parseLine lines[i]
for command in commands
@_handleErrors @_execute, command
return @_handleErrors @_buildModVersion, @_modVersionData, @_modVersion
unparse: ->
builder = new StringBuilder context:@_modVersion
@_unparseModVersion builder
return builder.toString()
# Private Methods ##############################################################################
_execute: (command)->
method = this["_command_#{command.name}"]
if not method? then throw new Error "Unknown command: #{command.name}"
@_handleErrors method, command.args
_parseLine: (line)->
line = line.replace ModVersionParserV2.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 = ModVersionParserV2.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
_handleErrors: (callback, args...)->
if args.length is 1 and _.isArray(args[0]) then args = args[0]
try
callback.apply this, args
catch e
e.message = "line #{@_lineNumber}: #{e.message}"
if not @_showAllErrors then throw e
logger.error e.message
_unparseModel: (builder, model)->
@_unparseModVersion builder, model
# Command Methods ##############################################################################
_command_description: (descriptionParts...)->
if @_modVersionData.description? then throw new Error 'duplicate declaration of "description"'
@_modVersionData.description = descriptionParts.join ', '
if @_rawData.description? then throw new Error 'duplicate declaration of "description"'
@_rawData.description = descriptionParts.join ', '
_command_extras: (extraTerms...)->
if not @_recipeData? then throw new Error 'cannot declare "extras" before "recipe"'
@@ -120,16 +61,16 @@ module.exports = class ModVersionParserV2
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
@_rawData.items ?= []
@_rawData.items.push @_itemData
@_recipeData = null
_command_name: (name='')->
if @_modVersionData.name? then throw new Error 'duplicate declaration of "name"'
if @_rawData.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
@_rawData.name = name
_command_input: (inputNames...)->
if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"'
@@ -175,10 +116,10 @@ module.exports = class ModVersionParserV2
@_recipeData.tools.push name
_command_version: (version='')->
if @_modVersionData.version? then throw new Error 'duplicate declaration of "version"'
if @_rawData.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
@_rawData.version = version
# Object Creation Methods ######################################################################
@@ -208,7 +149,8 @@ module.exports = class ModVersionParserV2
itemData.gatherable ?= false
itemData.recipes ?= []
item = new Item modVersion:modVersion, name:itemData.name, isGatherable:itemData.gatherable
item = new Item name:itemData.name, isGatherable:itemData.gatherable
modVersion.addItem item
for recipeData in itemData.recipes
@_handleErrors @_buildRecipe, modVersion, item, recipeData
@@ -228,7 +170,7 @@ module.exports = class ModVersionParserV2
for name in recipeData.input
slug = _.slugify name
modVersion.registerSlug slug, name
inputStacks.push new Stack itemSlug:slug, quantity:0
inputStacks.push new Stack slug:slug, quantity:0
for c in recipeData.pattern
continue if c is '.'
@@ -240,42 +182,43 @@ module.exports = class ModVersionParserV2
for i in [0...inputStacks.length]
stack = inputStacks[i]
if stack.quantity is 0
name = modVersion.findName stack.itemSlug
name = modVersion.findName stack.slug
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 ]
outputStacks = [ new Stack slug: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
outputStacks.push new Stack slug: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
toolStacks.push new Stack slug:slug, quantity:1
attributes =
input: inputStacks
item: item
pattern: recipeData.pattern
output: outputStacks
tools: toolStacks
input: inputStacks
name: item.name
pattern: recipeData.pattern
output: outputStacks
tools: toolStacks
recipe = new Recipe attributes
item.addRecipe recipe
return recipe
# Un-parsing Methods ###########################################################################
_unparseModVersion: (builder)->
itemList = _.values @modVersion.items
_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 '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)
@@ -291,13 +234,13 @@ module.exports = class ModVersionParserV2
.outdent()
_unparseRecipe: (builder, recipe)->
inputNames = (builder.context.findName(stack.itemSlug) for stack in recipe.input)
inputNames = (builder.context.findName(stack.slug) 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)}"
patternMap["#{i}"] = "#{inputNames.indexOf builder.context.findName(stack.slug)}"
pattern = recipe.pattern or recipe.defaultPattern
newPattern = []
@@ -333,9 +276,9 @@ module.exports = class ModVersionParserV2
_unparseStackList: (builder, stackList)->
if stackList.length is 1 and stackList[0].quantity is 1
builder.push builder.context.findName(stackList[0].itemSlug)
builder.push builder.context.findName(stackList[0].slug)
else
builder.loop stackList, onEach:(b, stack)=>
builder
.onlyIf stack.quantity > 1, => builder.push stack.quantity, ' '
.push builder.context.findName stack.itemSlug
.push builder.context.findName stack.slug
+14 -49
View File
@@ -13,50 +13,36 @@ Stack = require './stack'
module.exports = class Recipe extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.item? then throw new Error 'attributes.item is required'
if attributes.item?
attributes.name = attributes.item.name
attributes.slug = attributes.item.slug
attributes.output ?= [new Stack slug:item.slug, quantity:1]
attributes.output ?= [new Stack itemSlug:attributes.item.slug]
if not attributes.name? then throw new Error 'attributes.name is required'
if not attributes.input? then throw new Error 'attributes.input is required'
if not attributes.pattern? then throw new Error 'attributes.pattern is required'
attributes.item ?= null
attributes.output ?= [new Stack slug:_.slugify(attributes.name), quantity:1]
attributes.pattern = @_parsePattern attributes.pattern
attributes.slug ?= attributes.output[0].slug
attributes.tools ?= []
super attributes, options
@item.addRecipe this
Object.defineProperties this,
'defaultPattern': { get: -> @_computeDefaultPattern() }
'name': { get: -> @item.name }
'slug': { get: -> @item.slug }
# Public Methods ###############################################################################
getItemSlugAt: (index)->
getItemSlugAt: (patternSlot)->
pattern = if @pattern? then @pattern else @_computeDefaultPattern()
trueIndex = 0:0, 1:1, 2:2, 3:4, 4:5, 5:6, 6:8, 7:9, 8:10
patternDigit = pattern[trueIndex[index]]
patternDigit = pattern[trueIndex[patternSlot]]
return null unless patternDigit?
return null unless patternDigit.match /[0-9]/
stack = @input[parseInt(patternDigit)]
return null unless stack?
return stack.itemSlug
make: (inventory, missing)->
for stack in @input
itemSlug = stack.itemSlug
needed = stack.quantity
while needed > 0
if inventory.hasAtLeast itemSlug
inventory.remove itemSlug
else
missing.add itemSlug
for stack in @output
inventory.add stack.itemSlug, stack.quantity
return this
return stack.slug
# Object Overrides #############################################################################
@@ -109,24 +95,3 @@ module.exports = class Recipe extends BaseModel
pattern = array.join ''
pattern = pattern.replace /(...)(...)(...)/, '$1 $2 $3'
return pattern
_computeDefaultPattern: ->
itemCount = @input.length
slotCount = _.reduce @input, ((total, stack)-> total + stack.quantity), 0
return '... .0. ...' if itemCount is 1 and slotCount is 1
return '00. 00. ...' if itemCount is 1 and slotCount is 4
return '000 000 000' if itemCount is 1 and slotCount is 9
result = ['.', '.', '.', '.', '.', '.', '.', '.', '.']
indexes = [4, 7, 1, 3, 5, 6, 8, 0, 2]
for i in [0...@input.length]
stack = @input[i]
for j in [0...stack.quantity]
index = indexes.shift()
result[index] = "#{i}"
pattern = result.join ''
pattern = pattern.replace /(...)(...)(...)/, '$1 $2 $3'
return pattern
+6 -18
View File
@@ -5,30 +5,18 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
########################################################################################################################
module.exports = class Stack
module.exports = class Stack extends BaseModel
constructor: (attributes={})->
if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required'
if not attributes.slug? then throw new Error 'attributes.slug is required'
attributes.quantity ?= 1
@itemSlug = attributes.itemSlug
@quantity = attributes.quantity
# Public Methods ###############################################################################
canMerge: (stack)->
return @itemSlug is stack.itemSlug
merge: (stack)->
if not @canMerge stack
throw new Error "this stack of #{@itemSlug} cannot merge a stack of #{@stack.itemSlug}"
@quantity += stack.quantity
return this
super attributes
# Object Overrides #############################################################################
toString: ->
return "#{@quantity} #{@itemSlug}"
return "#{@quantity} #{@slug}"
+13
View File
@@ -0,0 +1,13 @@
###
Crafting Table - polyfill.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
########################################################################################################################
if not Array.prototype.clear?
Object.defineProperty Array.prototype, 'clear', value:->
@splice 0, @length
return this