diff --git a/app/core/initialize.coffee b/app/core/initialize.coffee index c961ae164..2fec30501 100644 --- a/app/core/initialize.coffee +++ b/app/core/initialize.coffee @@ -71,7 +71,7 @@ setUpBackboneMediator = -> if false # Debug which events are being fired originalPublish = Backbone.Mediator.publish Backbone.Mediator.publish = -> - console.log 'Publishing event:', arguments... + console.log 'Publishing event:', arguments... unless /(tick|frame-changed)/.test(arguments[0]) originalPublish.apply Backbone.Mediator, arguments setUpMoment = -> diff --git a/app/lib/Angel.coffee b/app/lib/Angel.coffee index eaa0d762c..6c651c44c 100644 --- a/app/lib/Angel.coffee +++ b/app/lib/Angel.coffee @@ -25,9 +25,10 @@ module.exports = class Angel extends CocoClass super() @say 'Got my wings.' isIE = window.navigator and (window.navigator.userAgent.search('MSIE') isnt -1 or window.navigator.appName is 'Microsoft Internet Explorer') - if isIE or @shared.headless - # Since IE is so slow to serialize without transferable objects, we can't trust it. - # We also noticed the headless_client simulator needing more time. (This does both Simulators, though.) + slowerSimulations = isIE #or @shared.headless + # Since IE is so slow to serialize without transferable objects, we can't trust it. + # We also noticed the headless_client simulator needing more time. (This does both Simulators, though.) If we need to use lots of headless clients, enable this. + if slowerSimulations @infiniteLoopIntervalDuration *= 10 @infiniteLoopTimeoutDuration *= 10 @abortTimeoutDuration *= 10 @@ -89,7 +90,7 @@ module.exports = class Angel extends CocoClass # We have to abort like an infinite loop if we see one of these; they're not really recoverable when 'non-user-code-problem' - Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem + Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem, god: @shared.god if @shared.firstWorld @infinitelyLooped(false, true) # For now, this should do roughly the right thing if it happens during load. else @@ -108,9 +109,9 @@ module.exports = class Angel extends CocoClass when 'console-log' @log event.data.args... when 'user-code-problem' - Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem + Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem, god: @shared.god when 'world-load-progress-changed' - Backbone.Mediator.publish 'god:world-load-progress-changed', progress: event.data.progress + Backbone.Mediator.publish 'god:world-load-progress-changed', progress: event.data.progress, god: @shared.god unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or (@shared.firstWorld and not @shared.spectate) @worker.postMessage func: 'serializeFramesSoFar' # Stream it! @@ -126,7 +127,8 @@ module.exports = class Angel extends CocoClass beholdGoalStates: (goalStates, overallStatus, preload=false) -> return if @aborting - Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus + Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus, god: @shared.god + @shared.god.trigger 'goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus @finishWork() if @shared.headless beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) -> @@ -176,8 +178,9 @@ module.exports = class Angel extends CocoClass return if @aborting problem = type: 'runtime', level: 'error', id: 'runtime_InfiniteLoop', message: 'Code never finished. It\'s either really slow or has an infinite loop.' problem.message = 'Escape pressed; code aborted.' if escaped - Backbone.Mediator.publish 'god:user-code-problem', problem: problem - Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem + Backbone.Mediator.publish 'god:user-code-problem', problem: problem, god: @shared.god + Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem, god: @shared.god + @shared.god.trigger 'infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem # For Simulator. TODO: refactor all the god:* Mediator events to be local events. @reportLoadError() if nonUserCodeProblem @fireWorker() @@ -308,7 +311,7 @@ module.exports = class Angel extends CocoClass i = 0 while i < work.testWorld.totalFrames frame = work.testWorld.getFrame i++ - Backbone.Mediator.publish 'god:world-load-progress-changed', progress: 1 + Backbone.Mediator.publish 'god:world-load-progress-changed', progress: 1, god: @shared.god work.testWorld.ended = true system.finish work.testWorld.thangs for system in work.testWorld.systems work.t2 = now() diff --git a/app/lib/AudioPlayer.coffee b/app/lib/AudioPlayer.coffee index 3a0abfb34..b9e2d3f55 100644 --- a/app/lib/AudioPlayer.coffee +++ b/app/lib/AudioPlayer.coffee @@ -93,11 +93,13 @@ class AudioPlayer extends CocoClass return defaults[message.length % defaults.length] preloadInterfaceSounds: (names) -> + return unless me.get 'volume' for name in names filename = "/file/interface/#{name}#{@ext}" @preloadSound filename, name playInterfaceSound: (name, volume=1) -> + return unless volume and me.get 'volume' filename = "/file/interface/#{name}#{@ext}" if @hasLoadedSound filename @playSound name, volume @@ -107,6 +109,7 @@ class AudioPlayer extends CocoClass playSound: (name, volume=1, delay=0, pos=null) -> return console.error 'Trying to play empty sound?' unless name + return unless volume and me.get 'volume' audioOptions = {volume: volume, delay: delay} filename = if _.string.startsWith(name, '/file/') then name else '/file/' + name unless @hasLoadedSound filename @@ -120,9 +123,8 @@ class AudioPlayer extends CocoClass return false unless createjs.Sound.loadComplete filename true - # TODO: load Interface sounds somehow, somewhere, somewhen - preloadSoundReference: (sound) -> + return unless me.get 'volume' return unless name = @nameForSoundReference sound filename = '/file/' + name @preloadSound filename, name diff --git a/app/lib/God.coffee b/app/lib/God.coffee index b22f6dcd7..5fe349a76 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -25,6 +25,7 @@ module.exports = class God extends CocoClass workerCode: options.workerCode or '/javascripts/workers/worker_world.js' # Either path or function headless: options.headless # Whether to just simulate the goals, or to deserialize all simulation results spectate: options.spectate + god: @ godNick: @nick workQueue: [] firstWorld: true @@ -61,6 +62,7 @@ module.exports = class God extends CocoClass setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap onTomeCast: (e) -> + return unless e.god is @ @lastSubmissionCount = e.submissionCount @lastFlagHistory = (flag for flag in e.flagHistory when flag.source isnt 'code') @lastDifficulty = e.difficulty @@ -142,9 +144,9 @@ module.exports = class God extends CocoClass when 'console-log' console.log "|#{@nick}'s debugger|", event.data.args... when 'debug-value-return' - Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized + Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized, god: @ when 'debug-world-load-progress-changed' - Backbone.Mediator.publish 'god:debug-world-load-progress-changed', progress: event.data.progress + Backbone.Mediator.publish 'god:debug-world-load-progress-changed', progress: event.data.progress, god: @ onNewWorldCreated: (e) -> @currentUserCodeMap = @filterUserCodeMapWhenFromWorld e.world.userCodeMap diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index d3b78f1f4..84ed7fe45 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -418,20 +418,23 @@ module.exports = class LevelLoader extends CocoClass # Initial Sound Loading playJingle: -> - return if @headless + return if @headless or not me.get('volume') + volume = 0.5 + if me.level() < 3 + volume = 0.25 # Start softly, since they may not be expecting it # Apparently the jingle, when it tries to play immediately during all this loading, you can't hear it. # Add the timeout to fix this weird behavior. f = -> jingles = ['ident_1', 'ident_2'] - AudioPlayer.playInterfaceSound jingles[Math.floor Math.random() * jingles.length] + AudioPlayer.playInterfaceSound jingles[Math.floor Math.random() * jingles.length], volume setTimeout f, 500 loadAudio: -> - return if @headless + return if @headless or not me.get('volume') AudioPlayer.preloadInterfaceSounds ['victory'] loadLevelSounds: -> - return if @headless + return if @headless or not me.get('volume') scripts = @level.get 'scripts' return unless scripts diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 10ce5cdc6..075d4776f 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -45,6 +45,7 @@ module.exports = class Simulator extends CocoClass humansGameID: humanGameID ogresGameID: ogresGameID simulator: @simulator + background: Boolean(@options.background) error: (errorData) -> console.warn "There was an error fetching two games! #{JSON.stringify errorData}" if errorData?.responseText?.indexOf("Old simulator") isnt -1 @@ -84,8 +85,8 @@ module.exports = class Simulator extends CocoClass @handleSingleSimulationError error commenceSingleSimulation: -> - Backbone.Mediator.subscribeOnce 'god:infinite-loop', @handleSingleSimulationInfiniteLoop, @ - Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processSingleGameResults, @ + @listenToOnce @god, 'infinite-loop', @handleSingleSimulationInfiniteLoop + @listenToOnce @god, 'goals-calculated', @processSingleGameResults @god.createWorld @generateSpellsObject() handleSingleSimulationError: (error) -> @@ -96,7 +97,7 @@ module.exports = class Simulator extends CocoClass process.exit(0) @cleanupAndSimulateAnotherTask() - handleSingleSimulationInfiniteLoop: -> + handleSingleSimulationInfiniteLoop: (e) -> console.log 'There was an infinite loop in the single game!' return if @destroyed if @options.headlessClient and @options.simulateOnlyOneGame @@ -105,7 +106,6 @@ module.exports = class Simulator extends CocoClass @cleanupAndSimulateAnotherTask() processSingleGameResults: (simulationResults) -> - return console.error "Weird, we destroyed the Simulator before it processed results?" if @destroyed try taskResults = @formTaskResultsObject simulationResults catch error @@ -165,6 +165,7 @@ module.exports = class Simulator extends CocoClass @simulateAnotherTaskAfterDelay() handleNoGamesResponse: -> + @noTasks = true info = 'Finding game to simulate...' console.log info @trigger 'statusUpdate', info @@ -223,7 +224,7 @@ module.exports = class Simulator extends CocoClass @god.setLevel @level.serialize(@supermodel, @session, @otherSession) @god.setLevelSessionIDs (session.sessionID for session in @task.getSessions()) @god.setWorldClassMap @world.classMap - @god.setGoalManager new GoalManager(@world, @level.get 'goals') + @god.setGoalManager new GoalManager @world, @level.get('goals'), null, {headless: true} humanFlagHistory = _.filter @session.get('state')?.flagHistory ? [], (event) => event.source isnt 'code' and event.team is (@session.get('team') ? 'humans') ogreFlagHistory = _.filter @otherSession.get('state')?.flagHistory ? [], (event) => event.source isnt 'code' and event.team is (@otherSession.get('team') ? 'ogres') @god.lastFlagHistory = humanFlagHistory.concat ogreFlagHistory @@ -232,8 +233,8 @@ module.exports = class Simulator extends CocoClass @god.lastDifficulty = 0 commenceSimulationAndSetupCallback: -> - Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @ - Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @ + @listenToOnce @god, 'infinite-loop', @onInfiniteLoop + @listenToOnce @god, 'goals-calculated', @processResults @god.createWorld @generateSpellsObject() # Search for leaks, headless-client only. @@ -260,14 +261,13 @@ module.exports = class Simulator extends CocoClass process.exit() @hd = new @memwatch.HeapDiff() - onInfiniteLoop: -> + onInfiniteLoop: (e) -> return if @destroyed console.warn 'Skipping infinitely looping game.' @trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds." _.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000 processResults: (simulationResults) -> - return console.error "Weird, we destroyed the Simulator before it processed results?" if @destroyed try taskResults = @formTaskResultsObject simulationResults catch error @@ -317,9 +317,13 @@ module.exports = class Simulator extends CocoClass cleanupAndSimulateAnotherTask: => return if @destroyed @cleanupSimulation() - @fetchAndSimulateTask() + if @options.background or @noTasks + @fetchAndSimulateOneGame() + else + @fetchAndSimulateTask() cleanupSimulation: -> + @stopListening @god @world = null @level = null diff --git a/app/lib/surface/MusicPlayer.coffee b/app/lib/surface/MusicPlayer.coffee index b218ea05b..2a3443644 100644 --- a/app/lib/surface/MusicPlayer.coffee +++ b/app/lib/surface/MusicPlayer.coffee @@ -16,6 +16,7 @@ module.exports = class MusicPlayer extends CocoClass 'playback:real-time-playback-ended': 'onRealTimePlaybackEnded' 'music-player:enter-menu': 'onEnterMenu' 'music-player:exit-menu': 'onExitMenu' + 'level:set-volume': 'onSetVolume' constructor: -> super arguments... @@ -26,6 +27,9 @@ module.exports = class MusicPlayer extends CocoClass onPlayMusic: (e) -> return if application.isIPadApp # Hard to measure, but just guessing this will save memory. + unless me.get 'volume' + @lastMusicEventIgnoredWhileMuted = e + return src = e.file src = "/file#{src}#{AudioPlayer.ext}" if (not e.file) or src is @currentMusic?.src @@ -97,6 +101,11 @@ module.exports = class MusicPlayer extends CocoClass @currentMusic = @previousMusic @restartCurrentMusic() + onSetVolume: (e) -> + return unless e.volume and @lastMusicEventIgnoredWhileMuted + @onPlayMusic @lastMusicEventIgnoredWhileMuted + @lastMusicEventIgnoredWhileMuted = null + destroy: -> me.off 'change:music', @onMusicSettingChanged, @ @fadeOutCurrentMusic() diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee index 974767b2f..d9a8943b1 100644 --- a/app/lib/surface/Surface.coffee +++ b/app/lib/surface/Surface.coffee @@ -49,7 +49,6 @@ module.exports = Surface = class Surface extends CocoClass navigateToSelection: true choosing: false # 'point', 'region', 'ratio-region' coords: null # use world defaults, or set to false/true to override - playJingle: false showInvisible: false frameRate: 30 # Best as a divisor of 60, like 15, 30, 60, with RAF_SYNCHED timing. diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee index 5b4cd3081..378068507 100644 --- a/app/lib/world/GoalManager.coffee +++ b/app/lib/world/GoalManager.coffee @@ -16,8 +16,9 @@ module.exports = class GoalManager extends CocoClass nextGoalID: 0 nicks: ['GoalManager'] - constructor: (@world, @initialGoals, @team) -> + constructor: (@world, @initialGoals, @team, options) -> super() + @options = options or {} @init() init: -> @@ -107,6 +108,7 @@ module.exports = class GoalManager extends CocoClass @addNewSubscription(channel, f(channel)) notifyGoalChanges: -> + return if @options.headless overallStatus = @checkOverallStatus() event = goalStates: @goalStates diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 8da29ea70..f381bdfd1 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -199,7 +199,6 @@ module.exports = class SuperModel extends Backbone.Model @progress = newProg @trigger('update-progress', @progress) @trigger('loaded-all') if @finished() - Backbone.Mediator.publish 'supermodel:load-progress-changed', progress: @progress setMaxProgress: (@maxProgress) -> resetProgress: -> @progress = 0 diff --git a/app/schemas/subscriptions/god.coffee b/app/schemas/subscriptions/god.coffee index 6fb79fca1..de6f0c606 100644 --- a/app/schemas/subscriptions/god.coffee +++ b/app/schemas/subscriptions/god.coffee @@ -27,13 +27,16 @@ worldUpdatedEventSchema = c.object {required: ['world', 'firstWorld', 'goalState finished: {type: 'boolean'} module.exports = - 'god:user-code-problem': c.object {required: ['problem']}, + 'god:user-code-problem': c.object {required: ['problem', 'god']}, + god: {type: 'object'} problem: {type: 'object'} - 'god:non-user-code-problem': c.object {required: ['problem']}, + 'god:non-user-code-problem': c.object {required: ['problem', 'god']}, + god: {type: 'object'} problem: {type: 'object'} - 'god:infinite-loop': c.object {required: ['firstWorld']}, + 'god:infinite-loop': c.object {required: ['firstWorld', 'god']}, + god: {type: 'object'} firstWorld: {type: 'boolean'} nonUserCodeProblem: {type: 'boolean'} @@ -41,17 +44,21 @@ module.exports = 'god:streaming-world-updated': worldUpdatedEventSchema - 'god:goals-calculated': c.object {required: ['goalStates']}, + 'god:goals-calculated': c.object {required: ['goalStates', 'god']}, + god: {type: 'object'} goalStates: goalStatesSchema preload: {type: 'boolean'} overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]} - 'god:world-load-progress-changed': c.object {required: ['progress']}, + 'god:world-load-progress-changed': c.object {required: ['progress', 'god']}, + god: {type: 'object'} progress: {type: 'number', minimum: 0, maximum: 1} - 'god:debug-world-load-progress-changed': c.object {required: ['progress']}, + 'god:debug-world-load-progress-changed': c.object {required: ['progress', 'god']}, + god: {type: 'object'} progress: {type: 'number', minimum: 0, maximum: 1} - 'god:debug-value-return': c.object {required: ['key']}, + 'god:debug-value-return': c.object {required: ['key', 'god']}, + god: {type: 'object'} key: {type: 'string'} value: {} diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee index 3a43ab4ab..c94a1a67d 100644 --- a/app/schemas/subscriptions/misc.coffee +++ b/app/schemas/subscriptions/misc.coffee @@ -48,9 +48,6 @@ module.exports = session: {type: 'object'} level: {type: 'object'} - 'supermodel:load-progress-changed': c.object {required: ['progress']}, - progress: {type: 'number', minimum: 0, maximum: 1} - 'buy-gems-modal:update-products': { } 'buy-gems-modal:purchase-initiated': c.object {required: ['productID']}, diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee index 7840b6e95..d2e2d1acc 100644 --- a/app/schemas/subscriptions/tome.coffee +++ b/app/schemas/subscriptions/tome.coffee @@ -7,13 +7,14 @@ module.exports = preload: {type: 'boolean'} realTime: {type: 'boolean'} - 'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty']}, - spells: [type: 'object'] - preload: [type: 'boolean'] - realTime: [type: 'boolean'] - submissionCount: [type: 'integer'] - flagHistory: [type: 'array'] - difficulty: [type: 'integer'] + 'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty', 'god']}, + spells: {type: 'object'} + preload: {type: 'boolean'} + realTime: {type: 'boolean'} + submissionCount: {type: 'integer'} + flagHistory: {type: 'array'} + difficulty: {type: 'integer'} + god: {type: 'object'} 'tome:manual-cast': c.object {title: 'Manually Cast Spells', description: 'Published when you wish to manually recast all spells', required: []}, realTime: {type: 'boolean'} diff --git a/app/views/ladder/SimulateTabView.coffee b/app/views/ladder/SimulateTabView.coffee index 114c43521..bcf67afc6 100644 --- a/app/views/ladder/SimulateTabView.coffee +++ b/app/views/ladder/SimulateTabView.coffee @@ -16,12 +16,7 @@ module.exports = class SimulateTabView extends CocoView @simulatorsLeaderboardData = new SimulatorsLeaderboardData(me) @simulatorsLeaderboardDataRes = @supermodel.addModelResource(@simulatorsLeaderboardData, 'top_simulators', {cache: false}) @simulatorsLeaderboardDataRes.load() - require 'vendor/aether-javascript' - require 'vendor/aether-python' - require 'vendor/aether-coffeescript' - require 'vendor/aether-lua' - require 'vendor/aether-clojure' - require 'vendor/aether-io' + require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'clojure', 'io'] onLoaded: -> super() diff --git a/app/views/play/SpectateView.coffee b/app/views/play/SpectateView.coffee index ec11d5e05..7ebe8332a 100644 --- a/app/views/play/SpectateView.coffee +++ b/app/views/play/SpectateView.coffee @@ -187,7 +187,7 @@ module.exports = class SpectateLevelView extends RootView # callbacks onInfiniteLoop: (e) -> - return unless e.firstWorld + return unless e.firstWorld and e.god is @god @openModalView new InfiniteLoopModal() window.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @world.name, label: @world.name @@ -196,7 +196,7 @@ module.exports = class SpectateLevelView extends RootView initSurface: -> webGLSurface = $('canvas#webgl-surface', @$el) normalSurface = $('canvas#normal-surface', @$el) - @surface = new Surface @world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, spectateGame: true, playerNames: @findPlayerNames() + @surface = new Surface @world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), spectateGame: true, playerNames: @findPlayerNames() worldBounds = @world.getBounds() bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}] @surface.camera.setBounds(bounds) diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee index 86607e56c..0b58f2783 100644 --- a/app/views/play/level/LevelLoadingView.coffee +++ b/app/views/play/level/LevelLoadingView.coffee @@ -44,12 +44,14 @@ module.exports = class LevelLoadingView extends CocoView @$el.addClass('manually-sized').css('height', newHeight) onLevelLoaded: (e) -> + return if @level @level = e.level @prepareGoals e @prepareTip() @prepareIntro() onSessionLoaded: (e) -> + return if @session @session = e.session if e.session.get('creator') is me.id prepareGoals: (e) -> diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 460db98b4..52fef8a94 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -19,6 +19,7 @@ LevelComponent = require 'models/LevelComponent' Article = require 'models/Article' Camera = require 'lib/surface/Camera' AudioPlayer = require 'lib/AudioPlayer' +Simulator = require 'lib/simulator/Simulator' # subviews LevelLoadingView = require './LevelLoadingView' @@ -49,7 +50,7 @@ module.exports = class PlayLevelView extends RootView isEditorPreview: false subscriptions: - 'level:set-volume': (e) -> createjs.Sound.setVolume(if e.volume is 1 then 0.6 else e.volume) # Quieter for now until individual sound FX controls work again. + 'level:set-volume': 'onSetVolume' 'level:show-victory': 'onShowVictory' 'level:restart': 'onRestartLevel' 'level:highlight-dom': 'onHighlightDOM' @@ -139,11 +140,11 @@ module.exports = class PlayLevelView extends RootView trackLevelLoadEnd: -> return if @isEditorPreview @loadEndTime = new Date() - loadDuration = @loadEndTime - @loadStartTime - console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s" + @loadDuration = @loadEndTime - @loadStartTime + console.debug "Level unveiled after #{(@loadDuration / 1000).toFixed(2)}s" unless @observing - application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: loadDuration - application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID + application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: @loadDuration + application.tracker?.trackTiming @loadDuration, 'Level Load Time', @levelID, @levelID # CocoView overridden methods ############################################### @@ -195,6 +196,7 @@ module.exports = class PlayLevelView extends RootView @worldLoadFakeResources.push @supermodel.addSomethingResource "world_simulation_#{percent}%", 1 onWorldLoadProgressChanged: (e) -> + return unless e.god is @god return unless @worldLoadFakeResources @lastWorldLoadPercent ?= 0 worldLoadPercent = Math.floor 100 * e.progress @@ -240,7 +242,7 @@ module.exports = class PlayLevelView extends RootView @god.setGoalManager @goalManager insertSubviews: -> - @insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID + @insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID, god: @god @insertSubView new LevelPlaybackView session: @session, level: @level @insertSubView new GoalsView {} @insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags' @@ -273,6 +275,7 @@ module.exports = class PlayLevelView extends RootView # Load Completed Setup ###################################################### onSessionLoaded: (e) -> + return if @session Backbone.Mediator.publish "ipad:language-chosen", language: e.session.get('codeLanguage') ? "python" # Just the level and session have been loaded by the level loader if e.level.get('slug') is 'zero-sum' @@ -288,7 +291,7 @@ module.exports = class PlayLevelView extends RootView e.session.set 'heroConfig', {"thangType":raider,"inventory":{}} else if e.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] and not _.size e.session.get('heroConfig')?.inventory ? {} @setupManager?.destroy() - @setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, courseID: @courseID, courseInstanceID: @courseInstanceID}) + @setupManager = new LevelSetupManager({supermodel: @supermodel, level: e.level, levelID: @levelID, parent: @, session: e.session, courseID: @courseID, courseInstanceID: @courseInstanceID}) @setupManager.open() @onRealTimeMultiplayerLevelLoaded e.session if e.level.get('type') in ['hero-ladder', 'course-ladder'] @@ -321,7 +324,7 @@ module.exports = class PlayLevelView extends RootView initSurface: -> webGLSurface = $('canvas#webgl-surface', @$el) normalSurface = $('canvas#normal-surface', @$el) - @surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, observing: @observing, playerNames: @findPlayerNames()) + @surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), observing: @observing, playerNames: @findPlayerNames()) worldBounds = @world.getBounds() bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}] @surface.camera.setBounds(bounds) @@ -365,10 +368,17 @@ module.exports = class PlayLevelView extends RootView # TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()? application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing $(window).trigger 'resize' + _.delay (=> @perhapsStartSimulating?()), 10 * 1000 + + onSetVolume: (e) -> + createjs.Sound.setVolume(if e.volume is 1 then 0.6 else e.volume) # Quieter for now until individual sound FX controls work again. + if e.volume and not @ambientSound + @playAmbientSound() playAmbientSound: -> return if @destroyed return if @ambientSound + return unless me.get 'volume' return unless file = {Dungeon: 'ambient-dungeon', Grass: 'ambient-grass'}[@level.get('terrain')] src = "/file/interface/#{file}#{AudioPlayer.ext}" unless AudioPlayer.getStatus(src)?.loaded @@ -384,6 +394,65 @@ module.exports = class PlayLevelView extends RootView Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false @surface.focusOnHero() + perhapsStartSimulating: -> + return unless @shouldSimulate() + require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'clojure', 'io'] + @simulateNextGame() + + simulateNextGame: -> + return @simulator.fetchAndSimulateOneGame() if @simulator + @simulator = new Simulator background: true + # Crude method of mitigating Simulator memory leak issues + fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame + @simulator.fetchAndSimulateOneGame = => + if @simulator.simulatedByYou >= 10 + console.log '------------------- Destroying Simulator and making a new one -----------------' + @simulator.destroy() + @simulator = null + @simulateNextGame() + else + fetchAndSimulateOneGameOriginal.apply @simulator + @simulator.fetchAndSimulateOneGame() + + shouldSimulate: -> + # Crude heuristics are crude. + defaultCores = 2 + cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari + defaultHeapLimit = 793000000 + heapLimit = window.performance?.memory?.jsHeapSizeLimit or defaultHeapLimit # Only available on Chrome, basically just says 32- vs. 64-bit + levelType = @level.get 'type', true + gamesSimulated = me.get('simulatedBy') + console.debug "Should we start simulating? Cores:", window.navigator.hardwareConcurrency, "Heap limit:", window.performance?.memory?.jsHeapSizeLimit, "Load duration:", @loadDuration + return false unless $.browser?.desktop + return false if $.browser?.msie or $.browser?.msedge + return false if $.browser.linux + return false if me.level() < 8 + if levelType is 'course' + return false + else if levelType is 'hero' and gamesSimulated + return false if cores < 8 + return false if heapLimit < defaultHeapLimit + return false if @loadDuration > 10000 + else if levelType is 'hero-ladder' and gamesSimulated + return false if cores < 4 + return false if heapLimit < defaultHeapLimit + return false if @loadDuration > 15000 + else if levelType is 'hero-ladder' and not gamesSimulated + return false if cores < 8 + return false if heapLimit <= defaultHeapLimit + return false if @loadDuration > 20000 + else if levelType is 'course-ladder' + return false if cores <= defaultCores + return false if heapLimit < defaultHeapLimit + return false if @loadDuration > 18000 + else + console.warn "Unwritten level type simulation heuristics; fill these in for new level type #{levelType}?" + return false if cores < 8 + return false if heapLimit < defaultHeapLimit + return false if @loadDuration > 10000 + console.debug "We should have the power. Begin background ladder simulation." + true + # callbacks onCtrlS: (e) -> @@ -457,7 +526,7 @@ module.exports = class PlayLevelView extends RootView application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing onInfiniteLoop: (e) -> - return unless e.firstWorld + return unless e.firstWorld and e.god is @god @openModalView new InfiniteLoopModal nonUserCodeProblem: e.nonUserCodeProblem application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing @@ -557,6 +626,7 @@ module.exports = class PlayLevelView extends RootView @goalManager?.destroy() @scriptManager?.destroy() @setupManager?.destroy() + @simulator?.destroy() if ambientSound = @ambientSound # Doesn't seem to work; stops immediately. createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop() diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee index 4f5d026e1..e179e4546 100644 --- a/app/views/play/level/tome/CastButtonView.coffee +++ b/app/views/play/level/tome/CastButtonView.coffee @@ -123,6 +123,7 @@ module.exports = class CastButtonView extends CocoView onGoalsCalculated: (e) -> # When preloading, with real-time playback enabled, we highlight the submit button when we think they'll win. + return unless e.god is @god return unless e.preload return if @options.level.get 'hidesRealTimePlayback' return if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] # Don't show it until they actually win for this first one. diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee index 5eeda9d5f..a06a36c7c 100644 --- a/app/views/play/level/tome/Spell.coffee +++ b/app/views/play/level/tome/Spell.coffee @@ -47,7 +47,7 @@ module.exports = class Spell @source = @originalSource = p.aiSource @thangs = {} if @canRead() # We can avoid creating these views if we'll never use them. - @view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker} + @view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god} @view.render() # Get it ready and code loaded in advance @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level @tabView.render() diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 659f4a07f..638bdc8a8 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -883,6 +883,7 @@ module.exports = class SpellView extends CocoView @spellHasChanged = false onUserCodeProblem: (e) -> + return unless e.god is @options.god return @onInfiniteLoop e if e.problem.id is 'runtime_InfiniteLoop' return unless e.problem.userInfo.methodName is @spell.name return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID @@ -893,6 +894,7 @@ module.exports = class SpellView extends CocoView @updateAether false, false onNonUserCodeProblem: (e) -> + return unless e.god is @options.god return unless @spellThang problem = @spellThang.aether.createUserCodeProblem type: 'runtime', kind: 'Unhandled', message: "Unhandled error: #{e.problem.message}" @spellThang.aether.addProblem problem diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee index 41a655dcf..3e8bf1fb3 100644 --- a/app/views/play/level/tome/TomeView.coffee +++ b/app/views/play/level/tome/TomeView.coffee @@ -62,7 +62,7 @@ module.exports = class TomeView extends CocoView @createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] @spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level - @castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session + @castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god @teamSpellMap = @generateTeamSpellMap(@spells) unless programmableThangs.length @cast() @@ -136,6 +136,7 @@ module.exports = class TomeView extends CocoView observing: @options.observing levelID: @options.levelID level: @options.level + god: @options.god for thangID, spellKeys of @thangSpells thang = world.getThangByID thangID @@ -168,7 +169,7 @@ module.exports = class TomeView extends CocoView difficulty = sessionState.difficulty ? 0 if @options.observing difficulty = Math.max 0, difficulty - 1 # Show the difficulty they won, not the next one. - Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty + Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty, god: @options.god onToggleSpellList: (e) -> @spellList?.rerenderEntries() diff --git a/scripts/mongodb/migrations/2015-11-28-add-concept-stats-to-users.js b/scripts/mongodb/migrations/2015-11-28-add-concept-stats-to-users.js new file mode 100644 index 000000000..bbfa4348c --- /dev/null +++ b/scripts/mongodb/migrations/2015-11-28-add-concept-stats-to-users.js @@ -0,0 +1,53 @@ +// Adds up all concept statistics for all non-anoymous users. +// Usage: +// mongo
:/