BattleSim/server/index.coffee

532 lines
20 KiB
CoffeeScript

http = require 'http'
Primus = require 'primus'
Emitter = require('primus-emitter')
express = require 'express'
path = require 'path'
{_} = require 'underscore'
{BattleServer} = require './server'
commands = require './commands'
auth = require('./auth')
generations = require './generations'
{Room} = require('./rooms')
errors = require '../shared/errors'
assets = require('../assets')
database = require('./database')
redis = require('./redis')
ratings = require('./ratings')
config = require('./config')
alts = require('./alts')
replays = require('./replays')
modify = require('./modify')
randomTeam = require('./randomTeams')
learnsets = require '../shared/learnsets'
MAX_MESSAGE_LENGTH = 250
MAX_RANK_DISPLAYED = 25
# A MD5 hash of all the JavaScript files used by the client. This is passed to
# each new connection via the .jade template, and when the client connects. If
# the two versions differ, the server had restarted at some point and now is
# serving new client files.
CLIENT_VERSION = assets.getVersion()
@createServer = (port) ->
app = express()
httpServer = http.createServer(app)
primus = new Primus(httpServer, transformer: 'sockjs')
primus.use('emitter', Emitter)
primus.save(path.join(__dirname, "../client/vendor/js/primus.js"))
server = new BattleServer()
# Configuration
app.set("views", "client/templates")
app.set('view engine', 'jade')
app.use(express.logger()) if config.IS_LOCAL
app.use(express.compress()) # gzip
app.use(express.cookieParser())
app.use(auth.middleware())
app.use(express.methodOverride())
app.use(app.router)
app.use(express.static(path.join(__dirname, "../public"))) if config.IS_LOCAL
# Helpers
app.locals.asset_path = assets.getAbsolute
# Routing
renderHomepage = (req, res) ->
res.render('index.jade', user: req.user, CLIENT_VERSION: CLIENT_VERSION)
app.get("/", renderHomepage)
app.get("/battles/:id", renderHomepage)
app.get("/replays/:id", replays.routes.show)
app.delete("/replays/:id", replays.routes.destroy)
app.get("/replays", replays.routes.index)
app.get "/pokemon/:id", (req, res) ->
thispokemon = req.params.id
thispokemon = thispokemon.replace("_", " ")
pokeObj = {}
pokeObj.name = thispokemon
pokeObj.formedata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData[thispokemon]
pokeObj.speciesdata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData[thispokemon]
pokeObj.learnablemoves = learnsets.learnableMoves(generations.GenerationJSON, {species:thispokemon, forme:"default"}, 7)
moveObj = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].MoveData
res.render('pokemon.jade', data:pokeObj, move_data:moveObj)
app.get "/pokemon/:id/json", (req, res) ->
thispokemon = req.params.id
thispokemon = thispokemon.replace("_", " ")
pokeObj = {}
pokeObj.name = thispokemon
pokeObj.formedata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData[thispokemon]
pokeObj.speciesdata = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData[thispokemon]
pokeObj.learnablemoves = learnsets.learnableMoves(generations.GenerationJSON, {species:thispokemon, forme:"default"}, 7)
moveObj = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].MoveData
res.json(data:pokeObj, move_data:moveObj)
app.get '/leaderboard', (req, res) ->
page = req.param('page')
perPage = req.param('per_page')
ratings.listRatings page, perPage, (err, results) ->
temparr = results.map((e) -> e.username)
ratioarr = []
ratings.getRatios temparr, (err, ratios)->
for username in temparr
index = temparr.indexOf(username)
results[index].ratio = ratios[username]
if err
res.json(500, err.message)
else
res.render('leaderboard.jade', players: results)
app.get '/leaderboard/json', (req, res) ->
page = req.param('page')
perPage = req.param('per_page')
ratings.listRatings page, perPage, (err, results) ->
temparr = results.map((e) -> e.username)
ratioarr = []
ratings.getRatios temparr, (err, ratios)->
for username in temparr
index = temparr.indexOf(username)
results[index].ratio = ratios[username]
if err
res.json(500, err.message)
else
res.json(players: results)
app.get '/tiers', (req, res) ->
formes = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData
species = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData
res.render('tiers.jade', formes: formes, species: species)
app.get '/tiers/json', (req, res) ->
formes = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].FormeData
species = generations.GenerationJSON[generations.DEFAULT_GENERATION.toUpperCase()].SpeciesData
res.json(formes: formes)
app.get '/ping', (req, res) ->
res.json(200, {response: "pong"})
app.get '/pokeuse/json', (req, res) ->
user= req.user
res.json(400) if user.name != "Deukhoofd"
page = req.param('page')
perPage = req.param('per_page')
q = new database.Teams()
q = q.query('orderBy', 'created_at')
.fetch()
.then (teams) ->
teams = teams.toJSON()
pkmn = getpokeusedata(teams)
res.json(teams: pkmn)
getpokeusedata = (json) ->
occurences = []
for team in json
for pokemon in team.pokemon
specie = pokemon.species
if specie != null
inarr = _.where(occurences, {name: specie})
if inarr.length == 0
newobj = {name: specie, occurence: 1}
occurences.push(newobj)
else
inarr[0].occurence = inarr[0].occurence + 1
occurences = _.sortBy(occurences, "occurence").reverse()
totalpokemon = 0
for pkmn in occurences
totalpokemon += pkmn.occurence
for pkmn in occurences
pkmn.percentage = (pkmn.occurence/totalpokemon) * 100
return occurences
lobby = new Room("Lobby")
server.rooms.push(lobby)
# Start responding to websocket clients
primus.on 'connection', (spark) ->
spark.send('version', CLIENT_VERSION)
spark.on 'login', (id, token) ->
return unless _.isFinite(id)
return unless _.isString(token)
requ = spark.request
auth.matchToken requ, id, token, (err, json) ->
if err
return spark.send('errorMessage', errors.INVALID_SESSION)
auth.getBanTTL json.name, (err, ttl) ->
if err
return spark.send('errorMessage', errors.INVALID_SESSION)
else if ttl != -2 # -2 means the ban does not exist
auth.getBanReason json.name, (err, reason) ->
spark.send('errorMessage', errors.BANNED, reason, Number(ttl))
spark.end()
return
else
user = server.findOrCreateUser(json, spark)
if !user.name || !user.id
console.error("MISSING INFORMATION: #{json}")
spark.end()
return
attachEvents(user, spark)
server.join(spark)
spark.loggedIn = true
spark.send('loginSuccess')
lobby.add(spark)
# After stuff
alts.listUserAlts user.name, (err, alts) ->
spark.send('altList', alts)
primus.on 'error', (err) ->
console.error(err.message, err.stack)
primus.on 'disconnection', (spark) ->
return unless spark.loggedIn
server.leave(spark)
spark.emit("cancelFindBattle") unless spark.user.hasSparks()
attachEvents = (user, spark) ->
spark.on 'sendChat', (roomId, message) ->
return unless _.isString(message)
message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
return unless 0 < message.length < MAX_MESSAGE_LENGTH
return unless room = server.getRoom(roomId)
server.limit user, 'chat', max: 5, duration: 3000, (err, limit) ->
if err || limit.remaining == 0
server.runIfUnmuted user, roomId, ->
server.mute(user.name, '[AUTOMATED] Chat rate-limit.', 10 * 60)
room = server.getRoom(roomId)
room.announce('warning', "#{user.name} was automatically muted for 10 minutes.")
else if message[0] == '/' && message[1] == '/'
message = message[1...]
server.userMessage(user, room, message)
else if message[0] == '/'
command = message.replace(/\s+.*$/, '')
args = message.substr(command.length).replace(/^\s+/, '')
command = command.substr(1)
args = args.split(',')
commands.executeCommand(server, user, room, command, args...)
else
server.userMessage(user, room, message)
if limit.remaining == 1
user.announce(room.name, 'warning', 'You are chatting too fast. Please slow down.')
spark.on 'leaveChatroom', (roomId) ->
return unless _.isString(roomId)
server.getRoom(roomId)?.remove(spark)
#########
# TEAMS #
#########
# Takes a temporary id and team JSON. Saves to server, and returns the real
# unique id that was persisted onto the DB.
spark.on 'saveTeam', (team, callback) ->
return unless _.isObject(team)
return unless _.isFunction(callback)
attributes = _.pick(team, 'id', 'name', 'generation')
attributes['trainer_id'] = user.id
attributes['contents'] = JSON.stringify(team.pokemon)
new database.Team(attributes)
.save().then (team) ->
callback(team.id)
spark.on 'requestTeams', (fetchall = false) ->
console.log(fetchall)
q = new database.Teams()
if !(fetchall && user.authority == auth.levels.OWNER)
q = q.query('where', trainer_id: user.id)
q = q.query('orderBy', 'created_at')
.fetch()
.then (teams) ->
spark.send('receiveTeams', teams.toJSON())
spark.on 'destroyTeam', (teamId) ->
return unless _.isFinite(teamId)
attributes = {
id: teamId
}
attributes['trainer_id'] = user.id unless config.IS_LOCAL
database.Team.query().where(attributes).delete()
.then ->
# Do nothing, just execute the promise. We assume it was deleted.
return
.catch (err) ->
console.error(err)
spark.on 'getRandomTeamsAdmin', (format, number) ->
if user.authority == auth.levels.OWNER
teamArr = []
for [1..number]
randomTeam.createTeamBookshelf format, [], (team) ->
teamArr.push(team)
teams = new database.Teams(teamArr)
spark.send('receiveTeams', teams.toJSON())
####################
# PRIVATE MESSAGES #
####################
spark.on 'privateMessage', (toUser, message) ->
return unless _.isString(toUser)
return unless _.isString(message)
message = message.trim().replace(/[\u0300-\u036F\u20D0-\u20FF\uFE20-\uFE2F]/g, '')
return unless 0 < message.length < MAX_MESSAGE_LENGTH
if server.users.contains(toUser)
recipient = server.users.get(toUser)
recipient.send('privateMessage', user.name, user.name, message)
user.send('privateMessage', toUser, user.name, message)
else
user.error(errors.PRIVATE_MESSAGE, toUser, "This user is offline.")
##############
# CHALLENGES #
##############
spark.on 'challenge', (challengeeId, generation, team, conditions, altName) ->
return unless _.isString(challengeeId)
return unless _.isString(generation)
return unless _.isObject(team)
return unless _.isArray(conditions)
return unless !altName || _.isString(altName)
alts.isAltOwnedBy user.name, altName, (err, valid) ->
return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid
server.registerChallenge(user, challengeeId, generation, team, conditions, altName)
spark.on 'cancelChallenge', (challengeeId) ->
return unless _.isString(challengeeId)
server.cancelChallenge(user, challengeeId)
spark.on 'acceptChallenge', (challengerId, team, altName) ->
return unless _.isString(challengerId)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
alts.isAltOwnedBy user.name, altName, (err, valid) ->
return user.error(errors.INVALID_ALT_NAME, "You do not own this alt") unless valid
server.acceptChallenge(user, challengerId, team, altName)
spark.on 'rejectChallenge', (challengerId) ->
return unless _.isString(challengerId)
server.rejectChallenge(user, challengerId)
########
# ALTS #
########
spark.on 'createAlt', (altName) ->
altName = String(altName).trim()
if !alts.isAltNameValid(altName)
return user.error(errors.INVALID_ALT_NAME, "Invalid Alt Name")
alts.createAlt user.name, altName, (err, success) ->
return user.error(errors.INVALID_ALT_NAME, err.message) if err
user.send('altCreated', altName) if success
###########
# REPLAYS #
###########
spark.on 'saveReplay', (battleId, callback) ->
battle = server.findBattle(battleId)
return callback?("The battle could not be found.") unless battle
return callback?("The battle is not yet done.") unless battle.isOver()
replays.create(user, battle.battle) # unwrap the facade
.then((replayId) -> callback?(null, replayId))
.catch replays.TooManyBattlesSaved, (err) ->
callback?(err.message)
.catch (err) ->
callback?('Something went wrong saving the replay.')
console.error(err.message)
console.error(err.stack)
###########
# BATTLES #
###########
spark.on 'getBattleList', (callback) ->
return unless _.isFunction(callback)
# TODO: Make this more efficient
# TODO: Order by age
# NOTE: Cache this? Even something like a 5 second expiration
# may improve server performance greatly
currentTime = Date.now()
battleMetadata = ([
controller.battle.id,
controller.battle.playerNames[0],
controller.battle.playerNames[1],
currentTime - controller.battle.createdAt
] for controller in server.getOngoingBattles())
callback(battleMetadata)
spark.on 'findBattle', (format, team, altName=null) ->
return unless _.isString(format)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
# Note: If altName == null, then isAltOwnedBy will return true
alts.isAltOwnedBy user.name, altName, (err, valid) ->
if not valid
user.error(errors.INVALID_ALT_NAME, "You do not own this alt")
else
validationErrors = server.queuePlayer(user.name, team, format, altName, "ranked")
if validationErrors.length > 0
user.error(errors.FIND_BATTLE, validationErrors)
spark.on 'cancelFindBattle', ->
server.removePlayer(user.name)
user.send("findBattleCanceled")
spark.on 'findBattleunranked', (format, team, altName=null) ->
return unless _.isString(format)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
# Note: If altName == null, then isAltOwnedBy will return true
alts.isAltOwnedBy user.name, altName, (err, valid) ->
if not valid
user.error(errors.INVALID_ALT_NAME, "You do not own this alt")
else
validationErrors = server.queuePlayer(user.name, team, format, altName, "unranked")
if validationErrors.length > 0
user.error(errors.FIND_BATTLE, validationErrors)
spark.on 'cancelFindBattleunranked', ->
server.removePlayerunranked(user.name)
user.send("findBattleCanceledUnranked")
spark.on 'findBattleRandom', (format, team, altName=null) ->
return unless _.isString(format)
return unless _.isObject(team)
return unless !altName || _.isString(altName)
# Note: If altName == null, then isAltOwnedBy will return true
alts.isAltOwnedBy user.name, altName, (err, valid) ->
if not valid
user.error(errors.INVALID_ALT_NAME, "You do not own this alt")
else
validationErrors = server.queuePlayer(user.name, team, format, altName, "random")
if validationErrors.length > 0
user.error(errors.FIND_BATTLE, validationErrors)
spark.on 'cancelFindBattleRandom', ->
server.removePlayerrandom(user.name)
user.send("findBattleCanceledRandom")
spark.on 'sendMove', (battleId, moveName, slot, forTurn, options, callback) ->
return unless _.isString(moveName)
return unless _.isFinite(slot)
return unless _.isFinite(forTurn)
return unless !options || _.isObject(options)
return unless _.isFunction(callback)
if battle = server.findBattle(battleId)
battle.makeMove(user.name, moveName, slot, forTurn, options)
callback()
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'sendSwitch', (battleId, toSlot, fromSlot, forTurn, callback) ->
return unless _.isFinite(toSlot)
return unless _.isFinite(fromSlot)
return unless _.isFinite(forTurn)
return unless _.isFunction(callback)
if battle = server.findBattle(battleId)
battle.makeSwitch(user.name, toSlot, fromSlot, forTurn)
callback()
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'sendCancelAction', (battleId, forTurn) ->
return unless _.isFinite(forTurn)
if battle = server.findBattle(battleId)
battle.undoCompletedRequest(user.name, forTurn)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'arrangeTeam', (battleId, arrangement) ->
return unless _.isArray(arrangement)
if battle = server.findBattle(battleId)
battle.arrangeTeam(user.name, arrangement)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'spectateBattle', (battleId) ->
if battle = server.findBattle(battleId)
battle.add(spark)
else
user.error(errors.BATTLE_DNE, battleId)
spark.on 'forfeit', (battleId) ->
if battle = server.findBattle(battleId)
battle.forfeit(user.name)
else
user.error(errors.BATTLE_DNE, battleId)
battleSearch = ->
server.beginBattles (err, battleIds) ->
if err then return
for id in battleIds
battle = server.findBattle(id)
playerIds = battle.getPlayerIds()
ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey)
ratings.getRanks ratingKeys, (err, fullRanks) ->
ranks = _.compact(fullRanks)
return unless ranks.length == fullRanks.length
if 1 <= Math.max(ranks...) <= MAX_RANK_DISPLAYED
playerNames = battle.getPlayerNames()
playerNames = playerNames.map((p, i) -> "#{p} (Rank ##{ranks[i]})")
message = """A high-level match is being played!
<span class="fake_link spectate" data-battle-id="#{id}">
#{playerNames.join(" vs. ")}</span>!"""
lobby.message(message)
setTimeout(battleSearch, 5 * 1000)
battleSearchUnranked = ->
server.beginBattlesunranked (err, battleIds) ->
if err then return
for id in battleIds
battle = server.findBattle(id)
playerIds = battle.getPlayerIds()
ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey)
ratings.getRanks ratingKeys, (err, fullRanks) ->
ranks = _.compact(fullRanks)
setTimeout(battleSearchUnranked, 5 * 1000)
battleSearchRandom = ->
server.beginBattlesrandom (err, battleIds) ->
if err then return
for id in battleIds
battle = server.findBattle(id)
playerIds = battle.getPlayerIds()
ratingKeys = playerIds.map((id) -> battle.getPlayer(id).ratingKey)
ratings.getRanks ratingKeys, (err, fullRanks) ->
ranks = _.compact(fullRanks)
setTimeout(battleSearchRandom, 5 * 1000)
battleSearch()
battleSearchUnranked()
battleSearchRandom()
httpServer.listen(port)
primus