Merge branch 'feature/change-supermodel-to-general-resource-loader' of https://github.com/gintau/codecombat into gintau-feature/change-supermodel-to-general-resource-loader

This commit is contained in:
Scott Erickson 2014-04-11 10:48:17 -07:00
commit 94e574cee8
7 changed files with 261 additions and 125 deletions

View file

@ -59,14 +59,16 @@ module.exports = class LevelLoader extends CocoClass
# Unless you specify cache:false, sometimes the browser will use a cached session # Unless you specify cache:false, sometimes the browser will use a cached session
# and players will 'lose' code # and players will 'lose' code
@session.fetch({cache:false})
@listenToOnce(@session, 'sync', @onSessionLoaded) @listenToOnce(@session, 'sync', @onSessionLoaded)
session_res = @supermodel.addModelResource(@session, @session.url, {cache:false})
session_res.load()
if @opponentSessionID if @opponentSessionID
@opponentSession = new LevelSession() @opponentSession = new LevelSession()
@opponentSession.url = "/db/level_session/#{@opponentSessionID}" @opponentSession.url = "/db/level_session/#{@opponentSessionID}"
@opponentSession.fetch()
@listenToOnce(@opponentSession, 'sync', @onSessionLoaded) @listenToOnce(@opponentSession, 'sync', @onSessionLoaded)
opponentSession_res = @supermodel.addModelResource(@opponentSession, @opponentSession.url)
opponentSession_res.load()
sessionsLoaded: -> sessionsLoaded: ->
return true if @headless return true if @headless
@ -217,7 +219,7 @@ module.exports = class LevelLoader extends CocoClass
progress: -> progress: ->
return 0 unless @level.loaded return 0 unless @level.loaded
overallProgress = 0 overallProgress = 0
supermodelProgress = @supermodel.progress() supermodelProgress = @supermodel.getProgress()
overallProgress += supermodelProgress * 0.7 overallProgress += supermodelProgress * 0.7
overallProgress += 0.1 if @sessionsLoaded() overallProgress += 0.1 if @sessionsLoaded()
if @headless if @headless
@ -226,6 +228,7 @@ module.exports = class LevelLoader extends CocoClass
spriteMapProgress = if supermodelProgress is 1 then 0.2 else 0 spriteMapProgress = if supermodelProgress is 1 then 0.2 else 0
spriteMapProgress *= @spriteSheetsBuilt / @spriteSheetsToBuild if @spriteSheetsToBuild spriteMapProgress *= @spriteSheetsBuilt / @spriteSheetsToBuild if @spriteSheetsToBuild
overallProgress += spriteMapProgress overallProgress += spriteMapProgress
return overallProgress return overallProgress
notifyProgress: -> notifyProgress: ->

View file

@ -1,20 +1,27 @@
class SuperModel module.exports = class SuperModel extends Backbone.Model
constructor: -> constructor: ->
@models = {} @models = {}
@collections = {} @collections = {}
@schemas = {} @schemas = {}
_.extend(@, Backbone.Events)
populateModel: (model) -> populateModel: (model) ->
@mustPopulate = model @mustPopulate = model
model.saveBackups = @shouldSaveBackups(model) model.saveBackups = @shouldSaveBackups(model)
model.fetch() unless model.loaded or model.loading # model.fetch() unless model.loaded or model.loading
@listenToOnce(model, 'sync', @modelLoaded) unless model.loaded # @listenToOnce(model, 'sync', @modelLoaded) unless model.loaded
@listenToOnce(model, 'error', @modelErrored) unless model.loaded # @listenToOnce(model, 'error', @modelErrored) unless model.loaded
url = model.url() url = model.url()
@models[url] = model unless @models[url]? @models[url] = model unless @models[url]?
@modelLoaded(model) if model.loaded @modelLoaded(model) if model.loaded
modelRes = @addModelResource(model, url)
schema = model.schema()
schemaRes = @addModelResource(schema, schema.urlRoot)
@schemas[schema.urlRoot] = schema
modelRes.addDependency(schemaRes.name)
modelRes.load()
# replace or overwrite # replace or overwrite
shouldLoadReference: (model) -> true shouldLoadReference: (model) -> true
shouldLoadProjection: (model) -> false shouldLoadProjection: (model) -> false
@ -25,25 +32,8 @@ class SuperModel
@trigger 'error' @trigger 'error'
@removeEventsFromModel(model) @removeEventsFromModel(model)
modelLoaded: (model) -> modelLoaded: (model) ->
model.loadSchema()
schema = model.schema()
unless schema.loaded
@schemas[schema.urlRoot] = schema
return schema.once('sync', => @modelLoaded(model))
refs = model.getReferencedModels(model.attributes, schema.attributes, '/', @shouldLoadProjection)
refs = [] unless @mustPopulate is model or @shouldPopulate(model)
# console.log 'Loaded', model.get('name')
for ref, i in refs when @shouldLoadReference ref
ref.saveBackups = @shouldSaveBackups(ref)
refURL = ref.url()
continue if @models[refURL]
@models[refURL] = ref
ref.fetch()
@listenToOnce(ref, 'sync', @modelLoaded)
@trigger 'loaded-one', model: model @trigger 'loaded-one', model: model
@trigger 'loaded-all' if @finished()
@removeEventsFromModel(model) @removeEventsFromModel(model)
removeEventsFromModel: (model) -> removeEventsFromModel: (model) ->
@ -96,21 +86,218 @@ class SuperModel
@addModel(model) @addModel(model)
collection collection
progress: ->
total = 0
loaded = 0
for model in _.values @models
total += 1
loaded += 1 if model.loaded
for schema in _.values @schemas
total += 1
loaded += 1 if schema.loaded
return 1.0 unless total
return loaded / total
finished: -> finished: ->
return @progress() == 1.0 return ResourceManager.progress == 1.0 or Object.keys(ResourceManager.resources).length == 0
module.exports = SuperModel
addModelResource: (modelOrCollection, name, fetchOptions, value=1)->
@checkName(name)
res = new ModelResource(modelOrCollection, name, fetchOptions, value)
@storeResource(name, res, value)
return res
addRequestResource: (name, jqxhrOptions, value=1)->
@checkName(name)
res = new RequestResource(name, jqxhrOptions, value)
@storeResource(name, res, value)
return res
addSomethingResource: (name, value=1)->
@checkName(name)
res = new SomethingResource(name, value)
@storeResource(name, res, value)
return res
checkName: (name)->
if not name
throw new Error('Resource name should not be empty.')
if name in ResourceManager.resources
throw new Error('Resource name has been used.')
storeResource: (name, resource, value)->
ResourceManager.resources[name] = resource
@listenToOnce(resource, 'resource:loaded', @onResourceLoaded)
@listenToOnce(resource, 'resource:failed', @onResourceFailed)
ResourceManager.denom += value
loadResources: ()->
for name, res of ResourceManager.resources
res.load()
onResourceLoaded: (r)=>
@modelLoaded(r.model)
# Check if the model has references
if r.constructor.name == 'ModelResource'
model = r.model
@addModelRefencesToLoad(model)
@updateProgress(r)
else
@updateProgress(r)
onResourceFailed: (r)=>
@modelErrored(r.model)
addModelRefencesToLoad: (model)->
schema = model.schema?()
return unless schema
refs = model.getReferencedModels(model.attributes, schema.attributes, '/', @shouldLoadProjection)
refs = [] unless @mustPopulate is model or @shouldPopulate(model)
for ref, i in refs when @shouldLoadReference ref
ref.saveBackups = @shouldSaveBackups(ref)
refURL = ref.url()
continue if @models[refURL]
@models[refURL] = ref
res = @addModelResource(ref, refURL)
res.load()
updateProgress: (r)=>
ResourceManager.num += r.value
ResourceManager.progress = ResourceManager.num / ResourceManager.denom
@trigger('superModel:updateProgress', ResourceManager.progress)
@trigger 'loaded-all' if @finished()
getResource: (name)->
return ResourceManager.resources[name]
getProgress: ()-> return ResourceManager.progress
# Both SuperModel and Resource access this class.
# Set resources as static so no need to load resources multiple times when more than one view is used.
class ResourceManager
@num = 0
@denom = 0
@showing = false
@progress = 0
@resources: {}
class Resource extends Backbone.Model
constructor: (name, value=1)->
@name = name
@value = value
@dependencies = []
@isLoading = false
@isLoaded = false
@model = null
@loadDeferred = null
@value = 1
addDependency: (name)->
depRes = ResourceManager.resources[name]
throw new Error('Resource not found') unless depRes
return if (depRes.isLoaded or name == @name)
@dependencies.push(name)
markLoaded: ()->
@trigger('resource:loaded', @) if not @isLoaded
@isLoaded = true
@isLoading = false
markFailed: ()->
@trigger('resource:failed', @) if not @isLoaded
@isLoaded = false
@isLoading = false
load: ()->
isReadyForLoad: ()-> return not (@isloaded and @isLoading)
getModel: ()-> @model
class ModelResource extends Resource
constructor: (modelOrCollection, name, fetchOptions, value)->
super(name, value)
@model = modelOrCollection
@fetchOptions = fetchOptions
load: ()->
return @loadDeferred.promise() if @isLoading or @isLoaded
@isLoading = true
@loadDeferred = $.Deferred()
$.when.apply($, @loadDependencies())
.then(@onLoadDependenciesSuccess, @onLoadDependenciesFailed)
.always(()=> @isLoading = false)
return @loadDeferred.promise()
loadDependencies: ()->
promises = []
for resName in @dependencies
dep = ResourceManager.resources[resName]
continue if not dep.isReadyForLoad()
promises.push(dep.load())
return promises
onLoadDependenciesSuccess: ()=>
@model.fetch(@fetchOptions)
@listenToOnce(@model, 'sync', ()=>
@markLoaded()
@loadDeferred.resolve(@)
)
@listenToOnce(@model, 'error', ()=>
@markFailed()
@loadDeferred.reject(@)
)
onLoadDependenciesFailed: ()=>
@markFailed()
@loadDeferred.reject(@)
class RequestResource extends Resource
constructor: (name, jqxhrOptions, value)->
super(name, value)
@model = $.ajax(jqxhrOptions)
@jqxhrOptions = jqxhrOptions
@loadDeferred = @model
load: ()->
return @loadDeferred.promise() if @isLoading or @isLoaded
@isLoading = true
$.when.apply($, @loadDependencies())
.then(@onLoadDependenciesSuccess, @onLoadDependenciesFailed)
.always(()=> @isLoading = false)
return @loadDeferred.promise()
loadDependencies: ()->
promises = []
for depName in @dependecies
dep = ResourceManager.resources[depName]
continue if not dep.isReadyForLoad()
promises.push(dep.load())
return promises
onLoadDependenciesSuccess: ()->
@model = $.ajax(@jqxhrOptions)
@model.done(()=> @markLoaded()).failed(()=> @markFailed())
onLoadDependenciesFailed: ()->
@markFailed()
class SomethingResource extends Resource
constructor: (name, value)->
super(value)
@name = name
@loadDeferred = $.Deferred()
load: ()->
return @loadDeferred.promise()
markLoaded: ()->
@loadDeferred.resolve()
super()
markFailed: ()->
@loadDeferred.reject()
super()

View file

@ -58,7 +58,7 @@ module.exports = class EditorLevelView extends View
onLevelLoaded: -> onLevelLoaded: ->
@files = new DocumentFiles(@level) @files = new DocumentFiles(@level)
@files.fetch() @supermodel.addModelResource(@files, @files.url).load()
onAllLoaded: -> onAllLoaded: ->
@level.unset('nextLevel') if _.isString(@level.get('nextLevel')) @level.unset('nextLevel') if _.isString(@level.get('nextLevel'))

View file

@ -56,17 +56,21 @@ module.exports = class ThangTypeEditView extends View
@insertSubView(new ErrorView()) @insertSubView(new ErrorView())
) )
@thangType.fetch() thang_res = @supermodel.addModelResource(@thangType, 'thang_type')
@thangType.loadSchema() thang_schema_res = @supermodel.addModelResource(@thangType.schema(), 'thang_type_schema')
thang_res.addDependency('thang_type_schema')
thang_res.load()
@listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync) @listenToOnce(@thangType.schema(), 'sync', @onThangTypeSync)
@listenToOnce(@thangType, 'sync', @onThangTypeSync) @listenToOnce(@thangType, 'sync', @onThangTypeSync)
@refreshAnimation = _.debounce @refreshAnimation, 500 @refreshAnimation = _.debounce @refreshAnimation, 500
onThangTypeSync: -> onThangTypeSync: ->
return unless @thangType.loaded and ThangType.hasSchema() return unless @thangType.loaded and ThangType.hasSchema()
@startsLoading = false @startsLoading = false
@files = new DocumentFiles(@thangType) @files = new DocumentFiles(@thangType)
@files.fetch() @supermodel.addModelResource(@files, @files.url).load()
@render() @render()
getRenderData: (context={}) -> getRenderData: (context={}) ->

View file

@ -26,12 +26,6 @@ module.exports = class CocoView extends Backbone.View
# load progress properties # load progress properties
loadProgress: loadProgress:
num: 0
denom: 0
showing: false
resources: [] # models and collections
requests: [] # jqxhr's
somethings: [] # everything else
progress: 0 progress: 0
# Setup, Teardown # Setup, Teardown
@ -48,6 +42,10 @@ module.exports = class CocoView extends Backbone.View
@listenToShortcuts() @listenToShortcuts()
@updateProgressBar = _.debounce @updateProgressBar, 100 @updateProgressBar = _.debounce @updateProgressBar, 100
# Backbone.Mediator handles subscription setup/teardown automatically # Backbone.Mediator handles subscription setup/teardown automatically
@listenToOnce(@supermodel, 'loaded-all', ()=>@onLoaded)
@listenToOnce(@supermodel, 'superModel:updateProgress', @updateProgress)
super options super options
destroy: -> destroy: ->
@ -87,8 +85,13 @@ module.exports = class CocoView extends Backbone.View
super() super()
return @template if _.isString(@template) return @template if _.isString(@template)
@$el.html @template(@getRenderData()) @$el.html @template(@getRenderData())
if not @supermodel.finished()
@showLoading()
else
@hideLoading()
@afterRender() @afterRender()
@showLoading() if @loading()
@$el.i18n() @$el.i18n()
@ @
@ -103,81 +106,18 @@ module.exports = class CocoView extends Backbone.View
context context
afterRender: -> afterRender: ->
@hideLoading()
# Resource and request loading management for any given view
addResourceToLoad: (modelOrCollection, name, value=1) ->
res = {resource:modelOrCollection, value:value, name:name, loaded: modelOrCollection.loaded}
@loadProgress.resources.push res updateProgress: (progress)=>
@loadProgress.denom += value
@listenToOnce modelOrCollection, 'sync', ()=>
# Sprite builder only works after rendering, if callback creates sprite, we need to update progress first.
res.loaded = true
@updateProgress(res)
@listenTo modelOrCollection, 'error', @onResourceLoadFailed
@updateProgress(res)
addRequestToLoad: (jqxhr, name, retryFunc, value=1) ->
res = {request:jqxhr, value:value, name: name, retryFunc: retryFunc, loaded:false}
@loadProgress.requests.push res
@loadProgress.denom += value
jqxhr.done ()=>
res.loaded = true
@updateProgress(res)
jqxhr.fail ()=>
@onRequestLoadFailed(jqxhr)
addSomethingToLoad: (name, value=1) ->
res = {name: name, value: value, loaded: false}
@loadProgress.somethings.push res
@loadProgress.denom += value
@updateProgress(res)
somethingLoaded: (name) ->
r = _.find @loadProgress.somethings, {name: name}
return console.error 'Could not find something called', name if not r
r.loaded = true
@updateProgress(r)
loading: ->
return false if @loaded
for r in @loadProgress.resources
return true if not r.resource.loaded
for r in @loadProgress.requests
return true if not r.request.status
for r in @loadProgress.somethings
return true if not r.loaded
return false
updateProgress: (r)=>
console.debug 'Loaded', r.name, r.loaded
denom = @loadProgress.denom
@loadProgress.num += r.value if r.loaded
num = @loadProgress.num
progress = if denom then num / denom else 0
# sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up
@loadProgress.progress = progress if progress > @loadProgress.progress @loadProgress.progress = progress if progress > @loadProgress.progress
@updateProgressBar() @updateProgressBar(progress)
if num is denom
@onLoaded()
updateProgressBar: => updateProgressBar: (progress) =>
prog = "#{parseInt(@loadProgress.progress*100)}%" prog = "#{parseInt(progress*100)}%"
@$el.find('.loading-screen .progress-bar').css('width', prog) @$el.find('.loading-screen .progress-bar').css('width', prog)
onLoaded: -> onLoaded: ->
@render() #@render()
# Error handling for loading # Error handling for loading

View file

@ -15,6 +15,7 @@ module.exports = class LevelLoadingView extends View
tip = _.sample(tips) tip = _.sample(tips)
$(tip).removeClass('to-remove') $(tip).removeClass('to-remove')
@$el.find('.to-remove').remove() @$el.find('.to-remove').remove()
@hideLoading()
onLevelLoaderProgressChanged: (e) -> onLevelLoaderProgressChanged: (e) ->
@progress = e.progress @progress = e.progress
@ -33,7 +34,7 @@ module.exports = class LevelLoadingView extends View
_.delay @reallyUnveil, 1000 _.delay @reallyUnveil, 1000
reallyUnveil: => reallyUnveil: =>
return if @destroyed return if @destroyed or @progress < 1
loadingDetails = @$el.find('.loading-details') loadingDetails = @$el.find('.loading-details')
duration = parseFloat loadingDetails.css 'transition-duration' duration = parseFloat loadingDetails.css 'transition-duration'
loadingDetails.css 'top', -loadingDetails.outerHeight(true) loadingDetails.css 'top', -loadingDetails.outerHeight(true)

View file

@ -86,6 +86,7 @@ module.exports = class PlayLevelView extends View
@listenToOnce(@supermodel, 'error', @onLevelLoadError) @listenToOnce(@supermodel, 'error', @onLevelLoadError)
@saveScreenshot = _.throttle @saveScreenshot, 30000 @saveScreenshot = _.throttle @saveScreenshot, 30000
@insertSubView @loadingView = new LoadingView {}
if @isEditorPreview if @isEditorPreview
f = => f = =>
@supermodel.shouldSaveBackups = (model) -> @supermodel.shouldSaveBackups = (model) ->
@ -361,11 +362,11 @@ module.exports = class PlayLevelView extends View
.css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)") .css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)")
.css('top', target_top - 50) .css('top', target_top - 50)
.css('left', target_left - 50) .css('left', target_left - 50)
setTimeout((=> setTimeout(()=>
@animatePointer() @animatePointer()
clearInterval(@pointerInterval) clearInterval(@pointerInterval)
@pointerInterval = setInterval(@animatePointer, 1200) @pointerInterval = setInterval(@animatePointer, 1200)
), 1) , 1)
animatePointer: -> animatePointer: ->