mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-13 09:11:22 -05:00
663c220eaf
This heavily refactors SpellView and adds infrastructure for receiving and reporting Errors raised by the web-dev iFrame. The web-dev error system, the Aether error system, and the Ace html-worker avoid disturbing each others' errors/annotations (though currently Aether+web-dev errors won't coexist), and they clear/update their own asynchronously. Show web-dev iFrame errors as Ace annotations Add functional error banners (with poor messages) Improve error banners, don't allow duplicate Problems Refactor setAnnotations override Convert all constructor calls for Problems Add comments, clean up Clean up Don't clear things unnecessarily Clean up error message sending from iFrame Add web-dev:error schema Clarify error message attributes Refactor displaying AetherProblems Refactor displaying user problem banners Refactor onWebDevError Set ace styles on updating @problems Clean up, fix off-by-1 error Add comment Show stale web-dev errors differently Some web-dev errors are generated by "stale" code — code that's still running in the iFrame but doesn't have the player's recent changes. This shows those errors differently than if they weren't "stale", and suggests they re-run their code. Hook up web-dev event schema Destroy ignored duplicate problems Functionalize a bit of stuff Fix ProblemAlertView never loading
239 lines
9.9 KiB
CoffeeScript
239 lines
9.9 KiB
CoffeeScript
SpellView = require './SpellView'
|
|
SpellTopBarView = require './SpellTopBarView'
|
|
{me} = require 'core/auth'
|
|
{createAetherOptions} = require 'lib/aether_utils'
|
|
utils = require 'core/utils'
|
|
|
|
module.exports = class Spell
|
|
loaded: false
|
|
view: null
|
|
topBarView: null
|
|
|
|
constructor: (options) ->
|
|
@spellKey = options.spellKey
|
|
@pathComponents = options.pathComponents
|
|
@session = options.session
|
|
@otherSession = options.otherSession
|
|
@spectateView = options.spectateView
|
|
@spectateOpponentCodeLanguage = options.spectateOpponentCodeLanguage
|
|
@observing = options.observing
|
|
@supermodel = options.supermodel
|
|
@skipProtectAPI = options.skipProtectAPI
|
|
@worker = options.worker
|
|
@level = options.level
|
|
|
|
p = options.programmableMethod
|
|
@commentI18N = p.i18n
|
|
@commentContext = p.context
|
|
@languages = p.languages ? {}
|
|
@languages.javascript ?= p.source
|
|
@name = p.name
|
|
@permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams
|
|
@team = @permissions.readwrite[0] ? 'common'
|
|
if @canWrite()
|
|
@setLanguage options.language
|
|
else if @otherSession and @team is @otherSession.get 'team'
|
|
@setLanguage @otherSession.get('submittedCodeLanguage') or @otherSession.get('codeLanguage')
|
|
else
|
|
@setLanguage 'javascript'
|
|
|
|
@source = @originalSource
|
|
@parameters = p.parameters
|
|
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
|
|
if sessionSource isnt '// Should fill in some default source\n' # TODO: figure out why session is getting this default source in there and stop it
|
|
@source = sessionSource
|
|
if p.aiSource and not @otherSession and not @canWrite()
|
|
@source = @originalSource = p.aiSource
|
|
@isAISource = true
|
|
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, god: options.god, @supermodel, levelID: options.levelID}
|
|
@view.render() # Get it ready and code loaded in advance
|
|
@topBarView = new SpellTopBarView
|
|
hintsState: options.hintsState
|
|
spell: @
|
|
supermodel: @supermodel
|
|
codeLanguage: @language
|
|
level: options.level
|
|
session: options.session
|
|
courseID: options.courseID
|
|
@topBarView.render()
|
|
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
|
|
|
destroy: ->
|
|
@view?.destroy()
|
|
@topBarView?.destroy()
|
|
@thang = null
|
|
@worker = null
|
|
|
|
setLanguage: (@language) ->
|
|
@language = 'html' if @level.isType('web-dev')
|
|
@displayCodeLanguage = utils.capitalLanguages[@language]
|
|
#console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript
|
|
@originalSource = @languages[@language] ? @languages.javascript
|
|
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
|
|
|
|
if @level.isType('web-dev')
|
|
# Pull apart the structural wrapper code and the player code, remember the wrapper code, and strip indentation on player code.
|
|
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
|
|
playerCodeLines = playerCode.split('\n')
|
|
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
|
|
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
|
|
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, '☃' # ☃ serves as placeholder for constructHTML
|
|
@originalSource = playerCode
|
|
|
|
# Translate comments chosen spoken language.
|
|
return unless @commentContext
|
|
context = $.extend true, {}, @commentContext
|
|
if @commentI18N
|
|
spokenLanguage = me.get 'preferredLanguage'
|
|
while spokenLanguage
|
|
spokenLanguage = spokenLanguage.substr 0, spokenLanguage.lastIndexOf('-') if fallingBack?
|
|
if spokenLanguageContext = @commentI18N[spokenLanguage]?.context
|
|
context = _.merge context, spokenLanguageContext
|
|
break
|
|
fallingBack = true
|
|
try
|
|
@originalSource = _.template @originalSource, context
|
|
@wrapperCode = _.template @wrapperCode, context
|
|
catch e
|
|
console.error "Couldn't create example code template of", @originalSource, "\nwith context", context, "\nError:", e
|
|
|
|
if /loop/.test(@originalSource) and @level.isType('course', 'course-ladder')
|
|
# Temporary hackery to make it look like we meant while True: in our sample code until we can update everything
|
|
@originalSource = switch @language
|
|
when 'python' then @originalSource.replace /loop:/, 'while True:'
|
|
when 'javascript' then @originalSource.replace /loop {/, 'while (true) {'
|
|
when 'lua' then @originalSource.replace /loop\n/, 'while true then\n'
|
|
when 'coffeescript' then @originalSource
|
|
else @originalSource
|
|
|
|
constructHTML: (source) ->
|
|
@wrapperCode.replace '☃', source
|
|
|
|
addPicoCTFProblem: ->
|
|
return @originalSource unless problem = @level.picoCTFProblem
|
|
description = """
|
|
-- #{problem.name} --
|
|
#{problem.description}
|
|
""".replace /<p>(.*?)<\/p>/gi, '$1'
|
|
("// #{line}" for line in description.split('\n')).join('\n') + '\n' + @originalSource
|
|
|
|
addThang: (thang) ->
|
|
if @thang?.thang.id is thang.id
|
|
@thang.thang = thang
|
|
else
|
|
@thang = {thang: thang, aether: @createAether(thang), castAether: null}
|
|
|
|
removeThangID: (thangID) ->
|
|
@thang = null if @thang?.thang.id is thangID
|
|
|
|
canRead: (team) ->
|
|
(team ? me.team) in @permissions.read or (team ? me.team) in @permissions.readwrite
|
|
|
|
canWrite: (team) ->
|
|
(team ? me.team) in @permissions.readwrite
|
|
|
|
getSource: ->
|
|
@view?.getSource() ? @source
|
|
|
|
transpile: (source) ->
|
|
if source
|
|
@source = source
|
|
else
|
|
source = @getSource()
|
|
unless @language is 'html'
|
|
@thang?.aether.transpile source
|
|
null
|
|
|
|
# NOTE: By default, I think this compares the current source code with the source *last saved to the server* (not the last time it was run)
|
|
hasChanged: (newSource=null, currentSource=null) ->
|
|
(newSource ? @originalSource) isnt (currentSource ? @source)
|
|
|
|
hasChangedSignificantly: (newSource=null, currentSource=null, cb) ->
|
|
unless aether = @thang?.aether
|
|
console.error @toString(), 'couldn\'t find a spellThang with aether', @thang
|
|
cb false
|
|
if @worker
|
|
workerMessage =
|
|
function: 'hasChangedSignificantly'
|
|
a: (newSource ? @originalSource)
|
|
spellKey: @spellKey
|
|
b: (currentSource ? @source)
|
|
careAboutLineNumbers: true
|
|
careAboutLint: true
|
|
@worker.addEventListener 'message', (e) =>
|
|
workerData = JSON.parse e.data
|
|
if workerData.function is 'hasChangedSignificantly' and workerData.spellKey is @spellKey
|
|
@worker.removeEventListener 'message', arguments.callee, false
|
|
cb(workerData.hasChanged)
|
|
@worker.postMessage JSON.stringify(workerMessage)
|
|
else
|
|
cb(aether.hasChangedSignificantly((newSource ? @originalSource), (currentSource ? @source), true, true))
|
|
|
|
createAether: (thang) ->
|
|
writable = @permissions.readwrite.length > 0 and not @isAISource
|
|
skipProtectAPI = @skipProtectAPI or not writable or @level.isType('game-dev')
|
|
problemContext = @createProblemContext thang
|
|
includeFlow = @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') and not skipProtectAPI
|
|
aetherOptions = createAetherOptions
|
|
functionName: @name
|
|
codeLanguage: @language
|
|
functionParameters: @parameters
|
|
skipProtectAPI: skipProtectAPI
|
|
includeFlow: includeFlow
|
|
problemContext: problemContext
|
|
useInterpreter: true
|
|
aether = new Aether aetherOptions
|
|
if @worker
|
|
workerMessage =
|
|
function: 'createAether'
|
|
spellKey: @spellKey
|
|
options: aetherOptions
|
|
@worker.postMessage JSON.stringify workerMessage
|
|
aether
|
|
|
|
updateLanguageAether: (@language) ->
|
|
@thang?.aether?.setLanguage @language
|
|
@thang?.castAether = null
|
|
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
|
|
if @worker
|
|
workerMessage =
|
|
function: 'updateLanguageAether'
|
|
newLanguage: @language
|
|
@worker.postMessage JSON.stringify workerMessage
|
|
@transpile()
|
|
|
|
toString: ->
|
|
"<Spell: #{@spellKey}>"
|
|
|
|
createProblemContext: (thang) ->
|
|
# Create problemContext Aether can use to craft better error messages
|
|
# stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak)
|
|
# thisMethods: methods available on the 'this' object
|
|
# thisProperties: properties available on the 'this' object
|
|
# commonThisMethods: methods that are available sometimes, but not awlays
|
|
|
|
# NOTE: Assuming the first createProblemContext call has everything we need, and we'll use that forevermore
|
|
return @problemContext if @problemContext?
|
|
|
|
@problemContext = { stringReferences: [], thisMethods: [], thisProperties: [] }
|
|
# TODO: These should be read from the database
|
|
@problemContext.commonThisMethods = ['moveRight', 'moveLeft', 'moveUp', 'moveDown', 'attack', 'findNearestEnemy', 'buildXY', 'moveXY', 'say', 'move', 'distance', 'findEnemies', 'findFriends', 'addFlag', 'findFlag', 'removeFlag', 'findFlags', 'attackRange', 'cast', 'buildTypes', 'jump', 'jumpTo', 'attackXY']
|
|
return @problemContext unless thang?
|
|
|
|
# Populate stringReferences
|
|
for key, value of thang.world?.thangMap
|
|
if (value.isAttackable or value.isSelectable) and value.id not in @problemContext.stringReferences
|
|
@problemContext.stringReferences.push value.id
|
|
|
|
# Populate thisMethods and thisProperties
|
|
if thang.programmableProperties?
|
|
for prop in thang.programmableProperties
|
|
if _.isFunction(thang[prop])
|
|
@problemContext.thisMethods.push prop
|
|
else
|
|
@problemContext.thisProperties.push prop
|
|
|
|
# TODO: See SpellPaletteView.createPalette() for other interesting contextual properties
|
|
|
|
@problemContext
|