diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 0cc871d54..df648c290 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -59,14 +59,16 @@ module.exports = class LevelLoader extends CocoClass # Unless you specify cache:false, sometimes the browser will use a cached session # and players will 'lose' code - @session.fetch({cache:false}) @listenToOnce(@session, 'sync', @onSessionLoaded) + session_res = @supermodel.addModelResource(@session, @session.url, {cache:false}) + session_res.load() if @opponentSessionID @opponentSession = new LevelSession() @opponentSession.url = "/db/level_session/#{@opponentSessionID}" - @opponentSession.fetch() @listenToOnce(@opponentSession, 'sync', @onSessionLoaded) + opponentSession_res = @supermodel.addModelResource(@opponentSession, @opponentSession.url) + opponentSession_res.load() sessionsLoaded: -> return true if @headless @@ -217,7 +219,7 @@ module.exports = class LevelLoader extends CocoClass progress: -> return 0 unless @level.loaded overallProgress = 0 - supermodelProgress = @supermodel.progress() + supermodelProgress = @supermodel.getProgress() overallProgress += supermodelProgress * 0.7 overallProgress += 0.1 if @sessionsLoaded() if @headless @@ -226,6 +228,7 @@ module.exports = class LevelLoader extends CocoClass spriteMapProgress = if supermodelProgress is 1 then 0.2 else 0 spriteMapProgress *= @spriteSheetsBuilt / @spriteSheetsToBuild if @spriteSheetsToBuild overallProgress += spriteMapProgress + return overallProgress notifyProgress: -> diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 9e2bcd347..a626f9d3e 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -1,20 +1,27 @@ -class SuperModel +module.exports = class SuperModel extends Backbone.Model constructor: -> @models = {} @collections = {} @schemas = {} - _.extend(@, Backbone.Events) populateModel: (model) -> @mustPopulate = model model.saveBackups = @shouldSaveBackups(model) - model.fetch() unless model.loaded or model.loading - @listenToOnce(model, 'sync', @modelLoaded) unless model.loaded - @listenToOnce(model, 'error', @modelErrored) unless model.loaded + # model.fetch() unless model.loaded or model.loading + # @listenToOnce(model, 'sync', @modelLoaded) unless model.loaded + # @listenToOnce(model, 'error', @modelErrored) unless model.loaded url = model.url() @models[url] = model unless @models[url]? @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 shouldLoadReference: (model) -> true shouldLoadProjection: (model) -> false @@ -25,25 +32,8 @@ class SuperModel @trigger 'error' @removeEventsFromModel(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) - + modelLoaded: (model) -> @trigger 'loaded-one', model: model - @trigger 'loaded-all' if @finished() @removeEventsFromModel(model) removeEventsFromModel: (model) -> @@ -96,21 +86,218 @@ class SuperModel @addModel(model) 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: -> - 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() \ No newline at end of file diff --git a/app/views/editor/level/edit.coffee b/app/views/editor/level/edit.coffee index b685d457f..57a233902 100644 --- a/app/views/editor/level/edit.coffee +++ b/app/views/editor/level/edit.coffee @@ -58,7 +58,7 @@ module.exports = class EditorLevelView extends View onLevelLoaded: -> @files = new DocumentFiles(@level) - @files.fetch() + @supermodel.addModelResource(@files, @files.url).load() onAllLoaded: -> @level.unset('nextLevel') if _.isString(@level.get('nextLevel')) diff --git a/app/views/editor/thang/edit.coffee b/app/views/editor/thang/edit.coffee index 660280e54..7e468741f 100644 --- a/app/views/editor/thang/edit.coffee +++ b/app/views/editor/thang/edit.coffee @@ -56,17 +56,21 @@ module.exports = class ThangTypeEditView extends View @insertSubView(new ErrorView()) ) - @thangType.fetch() - @thangType.loadSchema() + thang_res = @supermodel.addModelResource(@thangType, 'thang_type') + 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, 'sync', @onThangTypeSync) + @refreshAnimation = _.debounce @refreshAnimation, 500 onThangTypeSync: -> return unless @thangType.loaded and ThangType.hasSchema() @startsLoading = false @files = new DocumentFiles(@thangType) - @files.fetch() + @supermodel.addModelResource(@files, @files.url).load() @render() getRenderData: (context={}) -> diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index a6355ec22..c92fc7c39 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -26,12 +26,6 @@ module.exports = class CocoView extends Backbone.View # load progress properties loadProgress: - num: 0 - denom: 0 - showing: false - resources: [] # models and collections - requests: [] # jqxhr's - somethings: [] # everything else progress: 0 # Setup, Teardown @@ -48,6 +42,10 @@ module.exports = class CocoView extends Backbone.View @listenToShortcuts() @updateProgressBar = _.debounce @updateProgressBar, 100 # Backbone.Mediator handles subscription setup/teardown automatically + + @listenToOnce(@supermodel, 'loaded-all', ()=>@onLoaded) + @listenToOnce(@supermodel, 'superModel:updateProgress', @updateProgress) + super options destroy: -> @@ -87,8 +85,13 @@ module.exports = class CocoView extends Backbone.View super() return @template if _.isString(@template) @$el.html @template(@getRenderData()) + + if not @supermodel.finished() + @showLoading() + else + @hideLoading() + @afterRender() - @showLoading() if @loading() @$el.i18n() @ @@ -103,81 +106,18 @@ module.exports = class CocoView extends Backbone.View context afterRender: -> - - # Resource and request loading management for any given view - - addResourceToLoad: (modelOrCollection, name, value=1) -> - res = {resource:modelOrCollection, value:value, name:name, loaded: modelOrCollection.loaded} + @hideLoading() - @loadProgress.resources.push res - @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 + updateProgress: (progress)=> @loadProgress.progress = progress if progress > @loadProgress.progress - @updateProgressBar() - - if num is denom - @onLoaded() + @updateProgressBar(progress) - updateProgressBar: => - prog = "#{parseInt(@loadProgress.progress*100)}%" + updateProgressBar: (progress) => + prog = "#{parseInt(progress*100)}%" @$el.find('.loading-screen .progress-bar').css('width', prog) onLoaded: -> - @render() + #@render() # Error handling for loading diff --git a/app/views/play/level/level_loading_view.coffee b/app/views/play/level/level_loading_view.coffee index 32da3377e..679e78592 100644 --- a/app/views/play/level/level_loading_view.coffee +++ b/app/views/play/level/level_loading_view.coffee @@ -15,6 +15,7 @@ module.exports = class LevelLoadingView extends View tip = _.sample(tips) $(tip).removeClass('to-remove') @$el.find('.to-remove').remove() + @hideLoading() onLevelLoaderProgressChanged: (e) -> @progress = e.progress @@ -33,7 +34,7 @@ module.exports = class LevelLoadingView extends View _.delay @reallyUnveil, 1000 reallyUnveil: => - return if @destroyed + return if @destroyed or @progress < 1 loadingDetails = @$el.find('.loading-details') duration = parseFloat loadingDetails.css 'transition-duration' loadingDetails.css 'top', -loadingDetails.outerHeight(true) diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index bd60b867f..a50231a86 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -86,6 +86,7 @@ module.exports = class PlayLevelView extends View @listenToOnce(@supermodel, 'error', @onLevelLoadError) @saveScreenshot = _.throttle @saveScreenshot, 30000 + @insertSubView @loadingView = new LoadingView {} if @isEditorPreview f = => @supermodel.shouldSaveBackups = (model) -> @@ -361,11 +362,11 @@ module.exports = class PlayLevelView extends View .css('transform', "rotate(#{@pointerRotation}rad) translate(-3px, #{@pointerRadialDistance}px)") .css('top', target_top - 50) .css('left', target_left - 50) - setTimeout((=> + setTimeout(()=> @animatePointer() clearInterval(@pointerInterval) @pointerInterval = setInterval(@animatePointer, 1200) - ), 1) + , 1) animatePointer: ->