From a1c0e8591fdadeeb5f828803edfc0654c221fc53 Mon Sep 17 00:00:00 2001 From: silentwings <3885930+silentwings@users.noreply.github.com> Date: Wed, 10 Oct 2018 17:16:26 +0100 Subject: [PATCH] make Battle derived from Channel The chat for each battle now takes place in a channel. Channels use for this purpose have names prefixed by __battle__ When a client joins/leaves a battle, he is auto-joined/left to this channel. (-> such channels can register with ChanServ and set a founder, topic, operators, mutes, bans, etc.) To speak in a battle, use SAY/SAYEX/SAYFROM into its channel. -- Changes to protocol -- - JOINBATTLE (server) has an extra arg, containing the name of the channel associated to the battle - removed SAYBATTLEPRIVATE SAYBATTLEPRIVATEEX - deprecated SAYBATTLE SAYBATTLEEX (-> use SAY/SAYEX) - added !registerbattle and !mute/!unmute to ChanServ - deprecated SETCHANNELKEY and ChanServ !lock/!unlock (-> use !setkey) - fix #283: LEFT is reflected back to the user who leaves a channel Clients should send the 'u' compatibility flag to signal that they are prepared for the above. Full backwards compatibility exists for clients lacking 'u'. --- ChanServ.py | 29 ++- Client.py | 1 - DataHandler.py | 67 +++--- protocol/Battle.py | 150 ++++++++++-- protocol/Channel.py | 73 ++++-- protocol/Protocol.py | 559 ++++++++++++++++--------------------------- 6 files changed, 443 insertions(+), 436 deletions(-) diff --git a/ChanServ.py b/ChanServ.py index 2fb75fa5..42994a35 100644 --- a/ChanServ.py +++ b/ChanServ.py @@ -118,6 +118,7 @@ def HandleCommand(self, chan, user, cmd, args=None): channel.operators = set() channel.channelMessage('#%s has been unregistered'%chan) self.db().unRegister(client, channel) + self.Respond('LEAVE %s' % chan) return '#%s: Successfully unregistered.' % chan else: return '#%s: You must contact one of the server moderators or the owner of the channel to unregister a channel' % chan @@ -169,20 +170,23 @@ def HandleCommand(self, chan, user, cmd, args=None): return '#%s: Successfully removed <%s> from operator list' % (chan, args) else: return '#%s: You do not have permission to deop users' % chan - elif cmd == 'lock': + elif cmd == 'lock' or cmd == 'unlock': + return 'This command no longer exists, use setkey/removekey' + elif cmd == 'setkey': if access in ['mod', 'founder', 'op']: - if not args: return '#%s: You must specify a channel key to lock a channel' % chan + if not args: return '#%s: You must specify a key for the channel' % chan + if channel.identity=='battle': return 'This is not currently possible, instead you can close and re-open the battle with a new password!' channel.setKey(client, args) self.db().setKey(channel, args) - ## STUBS ARE BELOW - return '#%s: Locked' % chan + return '#%s: Set key' % chan else: return '#%s: You do not have permission to lock the channel' % chan - elif cmd == 'unlock': + elif cmd == 'removekey': if access in ['mod', 'founder', 'op']: + if channel.identity=='battle': return 'This is not currently possible, instead you can close and re-open the battle without a password!' channel.setKey(client, '*') self.db().setKey(channel, '*') - return '#%s: Unlocked' % chan + return '#%s: Removed key' % chan else: return '#%s: You do not have permission to unlock the channel' % chan elif cmd == 'kick': @@ -200,7 +204,7 @@ def HandleCommand(self, chan, user, cmd, args=None): else: return '#%s: user <%s> not found' % (chan, target) else: return '#%s: You do not have permission to kick users from the channel' % chan - elif cmd == 'history': + elif cmd == 'history': #FIXME: battles if access in ['mod', 'founder', 'op']: enable = not channel.store_history if self.db().setHistory(channel, enable): @@ -253,17 +257,20 @@ def HandleCommand(self, chan, user, cmd, args=None): return '#%s: <%s> unbanned' % (chan, target.username) return '#%s: user <%s> not found in banlist' % (chan, target_username) else: - return '#%s: You do not have permission to ban users from this channel' % chan - if cmd == 'register': + return '#%s: You do not have permission to ban users from this channel' % chan + #FIXME: chanserv has no mute command + #FIXME: accept ymdhs durations for mutes/bans + #FIXME: ban should interact with ip & email ? + if cmd == 'register': #FIXME: way to register battles if client.isMod(): if not args: args = user self.Respond('JOIN %s' % chan) if not chan in self._root.channels: - return '# Channel %s does not exist.' % (chan) + return '# Channel %s does not exist.' % chan channel = self._root.channels[chan] target = self._root.protocol.clientFromUsername(args, True) if target: - channel.setFounder(client, target) + channel.register(client, target) self.db().register(channel, target) # register channel in db return '#%s: Successfully registered to <%s>' % (chan, args.split(' ',1)[0]) else: diff --git a/Client.py b/Client.py index a89f728a..85f88f85 100644 --- a/Client.py +++ b/Client.py @@ -85,7 +85,6 @@ def __init__(self, root, address, session_id): self.battle_bots = {} self.current_battle = None - self.battle_bans = [] self.went_ingame = 0 self.spectator = False self.battlestatus = {'ready':'0', 'id':'0000', 'ally':'0000', 'mode':'0', 'sync':'00', 'side':'00', 'handicap':'0000000'} diff --git a/DataHandler.py b/DataHandler.py index f40f2fe4..71a4f3ae 100644 --- a/DataHandler.py +++ b/DataHandler.py @@ -5,7 +5,7 @@ import ChanServ import ip2country import datetime -from protocol import Protocol, Channel +from protocol import Protocol, Channel, Battle import logging from logging.handlers import TimedRotatingFileHandler @@ -64,11 +64,11 @@ def __init__(self): self.cert = None # lists of online stuff - self.channels = {} - self.usernames = {} - self.clients = {} - self.user_ids = {} - self.battles = {} + self.channels = {} #channame->channel/battle + self.battles = {} #battle_id->battle + self.usernames = {} #username->client + self.user_ids = {} #user_id->client + self.clients = {} #session_id->client self.bridge_location_bots = {} @@ -106,45 +106,46 @@ def _fk_pragma_on_connect(dbapi_con, con_record): self.verificationdb = SQLUsers.VerificationsHandler(self, self.engine) self.bandb = SQLUsers.BansHandler(self, self.engine) + self.parseFiles() + self.protocol = Protocol.Protocol(self) + self.channeldb = SQLUsers.ChannelsHandler(self, self.engine) channels = self.channeldb.all_channels() operators = self.channeldb.all_operators() + # set up channels/battles from db for name in channels: - channel = channels[name] - - owner_user_id = None - client = self.userdb.clientFromID(channel['owner_user_id']) - if client and client.id: - owner_user_id = client.id - assert(name not in self.channels) - newchan = Channel.Channel(self, name) - newchan.chanserv = bool(owner_user_id) - newchan.id = channel['id'] - newchan.owner_user_id = owner_user_id - newchan.operators = set() - if channel['key'] in ('', None, '*'): - newchan.key=None - else: - newchan.key = channel['key'] - newchan.antispam = channel['antispam'] - topic_client = self.userdb.clientFromID(channel['topic_user_id']) + dbchannel = channels[name] + channel = Channel.Channel(self, name) + if name.startswith('__battle__'): + channel = Battle.Battle(self, name) + + owner = self.userdb.clientFromID(dbchannel['owner_user_id']) + if owner: + channel.owner_user_id = owner.id + + channel.chanserv = True + channel.antispam = dbchannel['antispam'] + channel.store_history = dbchannel['store_history'] + channel.id = dbchannel['id'] + channel.key = dbchannel['key'] + if channel.key in ('', None, '*'): + channel.key = None + + channel.topic_user_id = dbchannel['topic_user_id'] + topic_client = self.userdb.clientFromID(dbchannel['topic_user_id']) topic_name = 'ChanServ' if topic_client: topic_name = topic_client.username - newchan.topic={'user':topic_name, 'text':channel['topic'], 'time':int(time.time())} - newchan.store_history = channel['store_history'] - self.channels[name] = newchan - + channel.topic={'user':topic_name, 'text':dbchannel['topic'], 'time':int(time.time())} + self.channels[name] = channel + for op in operators: dbchannel = self.channeldb.channel_from_id(op['channel_id']) if dbchannel: - self.channels[dbchannel.name].operators.add(op['user_id']) - - self.parseFiles() - self.protocol = Protocol.Protocol(self) - + self.channels[dbchannel.name].operators.add(op['user_id']) + self.chanserv = ChanServ.ChanServClient(self, (self.online_ip, 0), self.session_id) for name in channels: self.chanserv.HandleProtocolCommand("JOIN %s" %(name)) diff --git a/protocol/Battle.py b/protocol/Battle.py index a75ca3da..a244600f 100644 --- a/protocol/Battle.py +++ b/protocol/Battle.py @@ -1,43 +1,141 @@ +from Channel import Channel -class Battle(): - def __init__(self, root, id, type, natType, password, port, maxplayers, - hashcode, rank, maphash, map, title, modname, - passworded, host, users): +class Battle(Channel): + def __init__(self, root, name): + + Channel.__init__(self, root, name) + self.identity = 'battle' + + # battle + self.battle_id = None #FIXME: it would be great to remove this and use battle.name to identify battles, but it causes a big change to protocol -> wait for #58 + self.host = None + self.type = '' + self.natType = '' + self.port = 0 - self._root = root - self.id = id #name - self.passworded = passworded #key - self.password = password #key - self.users = users #users (set) # list with all session_ids of joined users - self.host = host #founder # client.session_id - self.mutelist = {} #mutelist - - self.title = title + self.title = '' + self.map = '' + self.maphash = None + self.modname = '' + self.hashcode = None self.engine = '' self.version = '' - self.modname = modname - self.map = map - self.maxplayers = maxplayers - - self.hashcode = hashcode - self.maphash = maphash - self.type = type + self.rank = 0 + self.maxplayers = 0 + self.spectators = 0 # duplicated info? self.locked = False - self.rank = rank + + self.pending_users = set() + self.bots = {} self.script_tags = {} self.startrects = {} self.disabled_units = [] - self.port = port - self.natType = natType - self.replay_script = {} + self.replay_script = {} #FIXME: inaccessible via protocol self.replay = False self.sending_replay_script = False - self.pending_users = set() #users who requested to join, but haven't heard back from the host yet - self.spectators = 0 # duplicated info? + + def joinBattle(self, client): + # client joins battle + notifies others + if client.compat['u']: + client.Send('JOINBATTLE %s %s %s' % (self.battle_id, self.hashcode, self.name)) + self.addUser(client) # join the battles channel + else: + client.Send('JOINBATTLE %s %s' % (self.battle_id, self.hashcode)) + self.users.add(client.session_id) + + scriptPassword = client.scriptPassword + host = self._root.protocol.clientFromSession(self.host) + if scriptPassword and host.compat['sp']: + host.Send('JOINEDBATTLE %s %s %s' % (self.battle_id, client.username, scriptPassword)) + if client!=host: + client.Send('JOINEDBATTLE %s %s %s' % (self.battle_id, client.username, scriptPassword)) + else: + host.Send('JOINEDBATTLE %s %s' % (self.battle_id, client.username)) + if client!=host: + client.Send('JOINEDBATTLE %s %s' % (self.battle_id, client.username)) + + scripttags = [] + for tag, val in self.script_tags.items(): + scripttags.append('%s=%s'%(tag, val)) + client.Send('SETSCRIPTTAGS %s'%'\t'.join(scripttags)) + if self.disabled_units: + client.Send('DISABLEUNITS %s' % ' '.join(self.disabled_units)) + self._root.broadcast('JOINEDBATTLE %s %s' % (self.battle_id, client.username), ignore=set([self.host, client.session_id])) + + if self.natType > 0: + if battle.host == client.session_id: + raise NameError('%s is having an identity crisis' % (client.name)) + if client.udpport: + self._root.usernames[host].Send('CLIENTIPPORT %s %s %s' % (username, client.ip_address, client.udpport)) + + specs = 0 + for sessionid in self.users: + battle_client = self._root.protocol.clientFromSession(sessionid) + if battle_client and battle_client.battlestatus['mode'] == '0': + specs += 1 + battlestatus = self.calc_battlestatus(battle_client) + client.Send('CLIENTBATTLESTATUS %s %s %s' % (battle_client.username, battlestatus, battle_client.teamcolor)) + + for iter in self.bots: + bot = self.bots[iter] + client.Send('ADDBOT %s %s' % (self.battle_id, iter)+' %(owner)s %(battlestatus)s %(teamcolor)s %(AIDLL)s' % (bot)) + + for allyno in self.startrects: + rect = self.startrects[allyno] + client.Send('ADDSTARTRECT %s' % (allyno)+' %(left)s %(top)s %(right)s %(bottom)s' % (rect)) + + client.battlestatus = {'ready':'0', 'id':'0000', 'ally':'0000', 'mode':'0', 'sync':'00', 'side':'00', 'handicap':'0000000'} + client.teamcolor = '0' + client.current_battle = self.battle_id + client.Send('REQUESTBATTLESTATUS') + + def leaveBattle(self, client): + self.removeUser(client) + + client.scriptPassword = None + client.current_battle = None + client.hostport = None + + for bot in list(client.battle_bots): + del client.battle_bots[bot] + if bot in self.bots: + del self.bots[bot] + self._root.broadcast_battle('REMOVEBOT %s %s' % (self.battle_id, bot), battle.id) + self._root.broadcast('LEFTBATTLE %s %s'%(self.battle_id, client.username)) + if client.session_id == self.host: + return + oldspecs = self.spectators + specs = 0 + for session_id in self.users: + user = self._root.protocol.clientFromSession(session_id) + if user and user.battlestatus['mode'] == '0': + specs += 1 + self.spectators = specs + if oldspecs != specs: + self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' % (self.battle_id, self.spectators, self.locked, self.maphash, self.map)) + + def calc_battlestatus(self, client): + battlestatus = client.battlestatus + status = self._root.protocol._bin2dec('0000%s%s0000%s%s%s%s%s0'%(battlestatus['side'], + battlestatus['sync'], battlestatus['handicap'], + battlestatus['mode'], battlestatus['ally'], + battlestatus['id'], battlestatus['ready'])) + return status + def hasBotflag(self): + host = self._root.protocol.clientFromSession(self.host) + return host.bot + + def canChangeSettings(self, client): + return client.session_id == self.host + def setKey(): + return #todo: currently there is no way to inform clients when a new channel/battle key is set/unset + def passworded(self): + return 0 if self.key in ('*', None) else 1 + diff --git a/protocol/Channel.py b/protocol/Channel.py index 768c91de..f3bc573e 100644 --- a/protocol/Channel.py +++ b/protocol/Channel.py @@ -2,12 +2,13 @@ class Channel(): def __init__(self, root, name): - self.id = 0 self._root = root + self.identity = 'channel' # db fields + self.id = 0 #id 0 is used for all unregistered channels self.name = name - self.key = None + self.key = None self.owner_user_id = None self.topic = None self.topic_time = None @@ -19,13 +20,13 @@ def __init__(self, root, name): self.store_history = False # non-db fields + self.operators = set() #user_ids self.users = set() # session_ids - self.operators = set() self.bridged_users = set() #bridged_ids - self.mutelist = {} - self.ban = {} - self.bridged_ban = {} + self.mutelist = {} #user_ids + self.ban = {} #user_ids + self.bridged_ban = {} #bridged_ids self.chanserv = False @@ -35,14 +36,50 @@ def broadcast(self, message, ignore=set([])): def channelMessage(self, message): self.broadcast('CHANNELMESSAGE %s %s' % (self.name, message)) - def register(self, client, owner_user_id): # fixme: unused? - self.owner_user_id = owner_user_id + def register(self, client, target): + self.setFounder(client, target) def addUser(self, client): if client.session_id in self.users: return self.users.add(client.session_id) + client.channels.add(self.name) + client.Send('JOIN %s' % self.name) self.broadcast('JOINED %s %s' % (self.name, client.username), set([client.session_id])) + + clientlist = "" + for session_id in self.users: + if clientlist: + clientlist += " " + channeluser = self._root.protocol.clientFromSession(session_id) + assert(channeluser) + clientlist += channeluser.username + client.Send('CLIENTS %s %s' % (self.name, clientlist)) + + bridgedClientList = "" + for bridged_id in self.bridged_users: + if clientlist: + clientlist += " " + bridgedClient = self._root.protocol.bridgedClientFromID(bridged_id) + assert(bridgedClient) + bridgedClientList += bridgedClient.username + if client.compat['u']: + client.Send('CLIENTSFROM %s %s' % (self.name, bridgedClientList)) + + topic = self.topic + if topic: + if client.compat['et']: + topictime = int(topic['time']) + else: + topictime = int(topic['time'])*1000 + try: + top = topic['text'] + except: + top = "Invalid unicode-encoding (should be utf-8)" + logging.info("%s for channel topic: %s" %(top, self.name)) + client.Send('CHANNELTOPIC %s %s %s %s'%(self.name, topic['user'], topictime, top)) + elif client.compat['et']: # supports sendEmptyTopic + client.Send('NOCHANNELTOPIC %s' % self.name) def removeUser(self, client, reason=None): if self.name in client.channels: @@ -51,9 +88,10 @@ def removeUser(self, client, reason=None): return self.users.remove(client.session_id) if reason and len(reason) > 0: - self.broadcast('LEFT %s %s %s' % (self.name, client.username, reason)) + self.broadcast('LEFT %s %s %s' % (self.name, client.username, reason)) else: self.broadcast('LEFT %s %s' % (self.name, client.username)) + client.Send('LEFT %s %s' % (self.name, client.username)) def addBridgedUser(self, client, bridgedClient): bridged_id = bridgedClient.bridged_id @@ -123,16 +161,19 @@ def setKey(self, client, key): if key in ('*', None): if self.key: self.key = None - self.channelMessage('<%s> unlocked this channel' % client.username) + self.channelMessage('<%s> removed the password of this %s' % (client.username, self.identity)) else: self.key = key - self.channelMessage('<%s> locked this channel with a password' % client.username) + self.channelMessage('<%s> set a new password for this %s' % (client.username, self.identity)) + def hasKey(self): + return not key in ('*', None) + def setFounder(self, client, target): if not target: return self.owner_user_id = target.user_id - self.channelMessage("<%s> has been set as this channel's founder by <%s>" % (target.username, client.username)) + self.channelMessage("<%s> has been set as this %s's founder by <%s>" % (target.username, self.identity, client.username)) def opUser(self, client, target): if not target: @@ -140,7 +181,7 @@ def opUser(self, client, target): if target.user_id in self.operators: return self.operators.add(target.user_id) - self.channelMessage("<%s> has been added to this channel's operator list by <%s>" % (target.username, client.username)) + self.channelMessage("<%s> has been added to this %s's operator list by <%s>" % (target.username, self.identity, client.username)) def deopUser(self, client, target): if not target: @@ -148,7 +189,7 @@ def deopUser(self, client, target): if not target.user_id in self.operators: return self.operators.remove(target.user_id) - self.channelMessage("<%s> has been removed from this channel's operator list by <%s>" % (target.username, client.username)) + self.channelMessage("<%s> has been removed from this %s's operator list by <%s>" % (target.username, self.identity, client.username)) def banUser(self, client, target, reason=''): if self.isFounder(target): return @@ -158,7 +199,7 @@ def banUser(self, client, target, reason=''): return self.ban[target.user_id] = reason self.removeUser(client, target, reason) - self.channelMessage('<%s> has been removed from this channel by <%s>' % (target.username, client.username)) + self.channelMessage('<%s> has been removed from this %s by <%s>' % (target.username, self.identity, client.username)) def unbanUser(self, client, target): if not target: @@ -188,7 +229,7 @@ def muteUser(self, client, target, duration=0): return if self.isFounder(target): return - if client.db_id in self.mutelist: + if client.user_id in self.mutelist: return try: duration = float(duration) diff --git a/protocol/Protocol.py b/protocol/Protocol.py index dfd1cf9a..94d2b495 100755 --- a/protocol/Protocol.py +++ b/protocol/Protocol.py @@ -202,6 +202,7 @@ def versiontuple(version): 'p': 'agreementPlain', # AGREEMENT is plaintext 'a': 'accountIDs', # deprecated / replaced by 'l' 'm': 'matchmaking', # deprecated / not used any more (keept to avoid sending errors to clients) + 'u': 'say2', # use SAY(EX) to speak in battles, support SAYFROM } class Protocol: @@ -233,11 +234,11 @@ def _logoutUser(self, client, reason): self.in_UNBRIDGECLIENTFROM(client, bridgedClient.location, bridgedClient.external_id) for location in client.bridged_locations: del self._root.bridge_location_bots[location] + if client.current_battle: + self.in_LEAVEBATTLE(client) for chan in list(client.channels): channel = self._root.channels[chan] self.in_LEAVE(client, chan, reason) - if client.current_battle: - self.in_LEAVEBATTLE(client) for battle_id, battle in self._root.battles.items(): if client.session_id in battle.pending_users: battle.pending_users.remove(client.session_id) @@ -429,14 +430,6 @@ def _calc_status(self, client, _status): status = self._bin2dec('%s%s%s%s%s%s%s'%(bot, access, rank1, rank2, rank3, away, ingame)) client.status = status - def _calc_battlestatus(self, client): - battlestatus = client.battlestatus - status = self._bin2dec('0000%s%s0000%s%s%s%s%s0'%(battlestatus['side'], - battlestatus['sync'], battlestatus['handicap'], - battlestatus['mode'], battlestatus['ally'], - battlestatus['id'], battlestatus['ready'])) - return status - def _time_format(self, seconds): 'given a duration in seconds, returns a human-readable relative time' minutesleft = float(seconds) / 60 @@ -614,28 +607,6 @@ def _dictToTags(self, dictionary): res += key + "=" + dictionary[key] return res - def _canForceBattle(self, client): - ' returns true when client can force sth. to a battle / username in current battle (=client is host & username is in battle)' - battle = self.getCurrentBattle(client) - if not battle: - return False - if not client.session_id == battle.host: - return False - return battle - - def _canForceBattleUser(self, client, username): - 'returns the user when client is allowed to force a command on it, false if not' - battle = self._canForceBattle(client) - if not battle: - return False - user=self.clientFromUsername(username) - if not user: - return False - if not user.session_id in battle.users: - return False - return user - - def _informErrors(self, client): if client.lobby_id in ("SpringLobby 0.188 (win x32)", "SpringLobby 0.200 (win x32)"): client.Send("SAYPRIVATE ChanServ The autoupdater of SpringLobby 0.188 is broken, please manually update: https://springrts.com/phpbb/viewtopic.php?f=64&t=31224") @@ -676,7 +647,6 @@ def getBridgedClient(self, location, external_id): bridge_user_id = self._root.bridge_location_bots.get(location) if not bridge_user_id: return None - print(location, external_id, bridge_user_id) client = self.clientFromID(bridge_user_id) bridged_id = external_id + '@' + location bridgedClient = client.bridged_clients.get(bridged_id) @@ -698,12 +668,6 @@ def bridgedClientFromUsername(self, username): bridgedClient = client.bridged_clients.get(bridged_id) return bridgedClient - def hasBotflag(self, battle): - host = self.clientFromSession(battle.host) - if not host: - return False - return host.bot - def clientFromUsername(self, username, fromdb = False): 'given a username, returns a client object from memory or the database' client = self._root.clientFromUsername(username) @@ -720,7 +684,7 @@ def broadcast_AddBattle(self, battle): def broadcast_RemoveBattle(self, battle): for cid, client in self._root.usernames.items(): - client.Send('BATTLECLOSED %s' % battle.id) + client.Send('BATTLECLOSED %s' % battle.battle_id) # the sourceClient is only sent for SAY*, and RING commands def broadcast_SendBattle(self, battle, data, sourceClient=None): @@ -783,14 +747,14 @@ def client_AddBattle(self, client, battle): battle.ip = translated_ip battle.host = host.session_id # session_id -> username if client.compat['cl']: #supports cleanupBattles - return 'BATTLEOPENED %s %s %s %s %s %s %s %s %s %s %s\t%s\t%s\t%s\t%s' %(battle.id, battle.type, battle.natType, host.username, battle.ip, battle.port, battle.maxplayers, battle.passworded, battle.rank, battle.maphash, battle.engine, battle.version, battle.map, battle.title, battle.modname) + return 'BATTLEOPENED %s %s %s %s %s %s %s %s %s %s %s\t%s\t%s\t%s\t%s' %(battle.battle_id, battle.type, battle.natType, host.username, battle.ip, battle.port, battle.maxplayers, battle.passworded(), battle.rank, battle.maphash, battle.engine, battle.version, battle.map, battle.title, battle.modname) # give client without version support a hint, that this battle is incompatible to his version if not (battle.engine == 'spring' and (battle.version == self._root.min_spring_version or battle.version == self._root.min_spring_version + '.0')): title = 'Incompatible (%s %s) %s' %(battle.engine, battle.version, battle.title) else: title = battle.title - return 'BATTLEOPENED %s %s %s %s %s %s %s %s %s %s %s\t%s\t%s' % (battle.id, battle.type, battle.natType, host.username, battle.ip, battle.port, battle.maxplayers, battle.passworded, battle.rank, battle.maphash, battle.map, title, battle.modname) + return 'BATTLEOPENED %s %s %s %s %s %s %s %s %s %s %s\t%s\t%s' % (battle.battle_id, battle.type, battle.natType, host.username, battle.ip, battle.port, battle.maxplayers, battle.passworded(), battle.rank, battle.maphash, battle.map, title, battle.modname) def is_ignored(self, client, ignoredClient): # verify that this is an online client (only those have an .ignored attr) @@ -922,17 +886,11 @@ def in_LOGIN(self, client, username, password='', cpu='0', local_ip='', sentence @optional.ip local_ip: LAN IP address, sent to clients when they have the same WAN IP as host @optional.sentence.str lobby_id: Lobby name and version @optional.sentence.int user_id: User ID provided by lobby - @optional.sentence.str compat_flags: Compatibility flags, sent in space-separated form, as follows: + @optional.sentence.str compat_flags: Compatibility flags, sent in space-separated form, see lobby protocol docs for details - flag: description - ----------------- - a: Send account IDs as an additional parameter to ADDUSER. Account IDs persist across renames. - b: If client is hosting a battle, prompts them with JOINBATTLEREQUEST when a user tries to join their battle - sp: If client is hosting a battle, sends them other clients' script passwords as an additional argument to JOINEDBATTLE. - et: When client joins a channel, sends NOCHANNELTOPIC if the channel has no topic. - ''' assert(type(password) == str) - + ''' + good, reason = self._validUsernameSyntax(username) if (not good): @@ -950,7 +908,7 @@ def in_LOGIN(self, client, username, password='', cpu='0', local_ip='', sentence return user_id = 0 - ## represents after logging in + # represents after logging in user_or_error = None @@ -970,7 +928,7 @@ def in_LOGIN(self, client, username, password='', cpu='0', local_ip='', sentence flags.add('b') else: flags.add(flag) - + unsupported = "" for flag in flags: client.compat[flag] = True @@ -1074,11 +1032,11 @@ def _SendLoginInfo(self, client): for battleid, battle in self._root.battles.items(): client.RealSend(self.client_AddBattle(client, battle)) - client.RealSend('UPDATEBATTLEINFO %s %i %i %s %s' % (battle.id, battle.spectators, battle.locked, battle.maphash, battle.map)) + client.RealSend('UPDATEBATTLEINFO %s %i %i %s %s' % (battle.battle_id, battle.spectators, battle.locked, battle.maphash, battle.map)) for session_id in battle.users: battleclient = self.clientFromSession(session_id) if not battleclient.session_id == battle.host: - client.RealSend('JOINEDBATTLE %s %s' % (battle.id, battleclient.username)) + client.RealSend('JOINEDBATTLE %s %s' % (battle.battle_id, battleclient.username)) # client status is sent last, so battle status is calculated correctly updated at clients for sessid, addclient in self._root.clients.items(): @@ -1124,24 +1082,28 @@ def in_SAY(self, client, chan, msg): if not msg: return if not chan in self._root.channels: return - channel = self._root.channels[chan] if not client.session_id in channel.users: self.out_FAILED(client, "SAY", "Not present in channel", True) return - msg = self.SayHooks.hook_SAY(self, client, channel, msg) if not msg or not msg.strip(): return - if channel.isMuted(client): client.Send('CHANNELMESSAGE %s You are %s.' % (chan, channel.getMuteMessage(client))) return - - self._root.broadcast('SAID %s %s %s' % (chan, client.username, msg), chan, set([]), client) if channel.store_history: self.userdb.add_channel_message(channel.id, client.user_id, msg) - + + if not client.compat['u']: + if hasattr(client, 'current_battle') and client.current_battle: + battle = self._root.battles[client.current_battle] + if battle.name==chan: + self.broadcast_SendBattle(battle, 'SAIDBATTLE %s %s' % (client.username, msg), client) + return + + self._root.broadcast('SAID %s %s %s' % (chan, client.username, msg), chan, set([]), client) + def in_SAYEX(self, client, chan, msg): ''' Send an action to all users in specified channel. @@ -1156,6 +1118,7 @@ def in_SAYEX(self, client, chan, msg): return if not chan in self._root.channels: return + channel = self._root.channels[chan] user = client.username msg = self.SayHooks.hook_SAY(self, client, channel, msg) @@ -1163,10 +1126,18 @@ def in_SAYEX(self, client, chan, msg): if client.session_id in channel.users: if channel.isMuted(client): client.Send('CHANNELMESSAGE %s You are %s.' % (chan, channel.getMuteMessage(client))) - else: - self._root.broadcast('SAIDEX %s %s %s' % (chan, client.username, msg), chan, set([]), client) + return if channel.store_history: self.userdb.add_channel_message(channel.id, client.user_id, msg) + + if not client.compat['u']: + if hasattr(client, 'current_battle'): + battle = self._root.battles[client.current_battle] + if battle.name==chan: + self.broadcast_SendBattle(battle, 'SAIDBATTLEEX %s %s' % (client.username, msg), client) + return + + self._root.broadcast('SAIDEX %s %s %s' % (chan, client.username, msg), chan, set([]), client) def in_SAYPRIVATE(self, client, user, msg): ''' @@ -1302,6 +1273,8 @@ def in_UNBRIDGECLIENTFROM(self, client, location, external_id): def in_JOINFROM(self, client, chan, location, external_id): # bridged client joins a channel + if not client.compat['u']: + return if not chan in self._root.channels: return channel = self._root.channels[chan] @@ -1316,6 +1289,8 @@ def in_JOINFROM(self, client, chan, location, external_id): def in_LEAVEFROM(self, client, chan, location, external_id): # bridged client leaves a channel + if not client.compat['u']: + return if not chan in self._root.channels: return channel = self._root.channels[chan] @@ -1333,10 +1308,14 @@ def in_SAYFROM(self, client, chan, location, external_id, msg): channel = self._root.channels[chan] bridgedClient = self.getBridgedClient(location, external_id) if not bridgedClient or bridgedClient.bridge_user_id != client.user_id: - return + return + if not client.compat['u']: + msg = '<' + bridgedClient.username + '> ' + msg + self.in_SAY(client, chan, msg) + return if not bridgedClient.bridged_id in channel.bridged_users: self.out_FAILED(client, "SAYFROM", "Bridged user not present in channel", True) - return + return self._root.broadcast('SAIDFROM %s %s %s' % (chan, bridgedClient.username, msg), chan, set([]), client) def in_IGNORE(self, client, tags): @@ -1544,7 +1523,7 @@ def in_FRIENDLIST(self, client): def in_JOIN(self, client, chan, key=None): ''' - Attempt to join target channel. + Attempt to join target channel. @required.str channel: The target channel. @optional.str password: The password to use for joining if channel is locked. @@ -1554,9 +1533,8 @@ def in_JOIN(self, client, chan, key=None): if not ok: client.Send('JOINFAILED %s' % reason) return - + user = client.username - # FIXME: unhardcode this if (client.bot or client.lobby_id.startswith("SPADS")) and chan in ("newbies") and client.username != "ChanServ": #client.Send('JOINFAILED %s No bots allowed in #%s!' %(chan, chan)) @@ -1564,12 +1542,14 @@ def in_JOIN(self, client, chan, key=None): if chan == 'moderator' and not 'mod' in client.accesslevels: self.out_FAILED(client, "JOIN", "Only moderators allowed in this channel! access=%s" %(client.access), True) return - if not chan: - self.out_FAILED(client, "JOIN", 'Invalid channel: %s' %(chan), True) + self.out_FAILED(client, 'JOIN', 'Invalid channel: %s' %(chan), True) return if not chan in self._root.channels: - channel = Channel.Channel(self._root, chan) + if chan.startswith('__battle__'): + client.Send('JOINFAILED cannot create channel %s with prefix __battle__, these names are reserved for battles' % chan) + return + channel = Channel.Channel(self._root, chan) self._root.channels[chan] = channel else: channel = self._root.channels[chan] @@ -1577,6 +1557,9 @@ def in_JOIN(self, client, chan, key=None): # https://github.com/springlobby/springlobby/issues/782 #self.out_FAILED(client, "JOIN", 'Already in channel %s' %(chan), True) return + if channel.identity=='battle' and client.username!='ChanServ' and not client.bot: + client.Send('JOINFAILED %s is a battle, please use JOINBATTLE to access it' % chan) + return if not channel.isFounder(client) and not 'mod' in client.accesslevels: if client.user_id in channel.ban: client.Send('JOINFAILED %s You are banned from the channel %s' % (chan, channel.ban[client.user_id])) @@ -1585,55 +1568,12 @@ def in_JOIN(self, client, chan, key=None): client.Send('JOINFAILED %s Invalid key' % chan) return assert(chan not in client.channels) - client.channels.add(chan) - client.Send('JOIN %s'%chan) - channel.addUser(client) - assert(client.session_id in channel.users) - - clientlist = "" - for session_id in channel.users: - if clientlist: - clientlist += " " - channeluser = self.clientFromSession(session_id) - assert(channeluser) - clientlist += channeluser.username - client.Send('CLIENTS %s %s' % (chan, clientlist)) - - bridgedClientList = "" - for bridged_id in channel.bridged_users: - if clientlist: - clientlist += " " - bridgedClient = self.bridgedClientFromID(bridged_id) - assert(bridgedClient) - bridgedClientList += bridgedClient.username - client.Send('BRIDGEDCLIENTS %s %s' % (chan, bridgedClientList)) - topic = channel.topic - if topic: - if client.compat['et']: - topictime = int(topic['time']) - else: - topictime = int(topic['time'])*1000 - try: - top = topic['text'] - except: - top = "Invalid unicode-encoding (should be utf-8)" - logging.info("%s for channel topic: %s" %(top, chan)) - client.Send('CHANNELTOPIC %s %s %s %s'%(chan, topic['user'], topictime, top)) - elif client.compat['et']: # supports sendEmptyTopic - client.Send('NOCHANNELTOPIC %s' % chan) + channel.addUser(client) def in_SETCHANNELKEY(self, client, chan, key='*'): - ''' - Lock target channel with a password, or unlocks target channel. - - @required.str channel: The target channel. - @optional.str password: The password to set. To unlock a channel, leave this blank or set to '*'. - ''' - if chan in self._root.channels: - channel = self._root.channels[chan] - if channel.isOp(client): - channel.setKey(client, key) + # deprecated + self.in_SAYPRIVATE(client, 'ChanServ !setkey #' + chan + ' ' + key) def in_LEAVE(self, client, chan, reason=None): ''' @@ -1644,12 +1584,18 @@ def in_LEAVE(self, client, chan, reason=None): if not chan in self._root.channels: return channel = self._root.channels[chan] + if channel.identity=='battle' and client.username!='ChanServ' and not client.bot: + self.out_FAILED(client, 'LEAVE', '%s is a battle, use LEAVEBATTLE to leave it' % chan, True) + return + if not client.user_id in channel.users: + self.out_FAILED(client, 'LEAVE', 'not in channel %s' % chan, True) + return channel.removeUser(client, reason) assert(not client.session_id in channel.users) if len(self._root.channels[chan].users) == 0: del self._root.channels[chan] - def in_OPENBATTLE(self, client, type, natType, password, port, maxplayers, hashcode, rank, maphash, sentence_args): + def in_OPENBATTLE(self, client, type, natType, key, port, maxplayers, hashcode, rank, maphash, sentence_args): ''' Host a new battle with the arguments specified. @@ -1662,7 +1608,7 @@ def in_OPENBATTLE(self, client, type, natType, password, port, maxplayers, hashc #1: Hole punching #2: Fixed source ports - @required.str password: The password to use, or "*" to use no password. + @required.str key: The password to use, or "*" to use no password. @required.int port: @required.int maxplayers: @required.sint modhash: Mod hash, as returned by unitsync.dll. @@ -1714,23 +1660,18 @@ def in_OPENBATTLE(self, client, type, natType, password, port, maxplayers, hashc battle_id = self._getNextBattleId() - if password == '*': - passworded = 0 - else: - passworded = 1 - try: battle_id = int(battle_id) type = int(type) natType = int(natType) - passworded = int(passworded) + key = str(key) port = int(port) maphash = int32(maphash) hashcode = int32(hashcode) maxplayers = int32(maxplayers) except Exception as e: - self.out_OPENBATTLEFAILED(client, 'Invalid argument type, send this to your lobby dev: id=%s type=%s natType=%s passworded=%s port=%s maphash=%s gamehash=%s - %s' % - (battle_id, type, natType, passworded, port, maphash, hashcode, str(e).replace("\n", ""))) + self.out_OPENBATTLEFAILED(client, 'Invalid argument type, send this to your lobby dev: id=%s type=%s natType=%s key=%s port=%s maphash=%s gamehash=%s - %s' % + (battle_id, type, natType, key, port, maphash, hashcode, str(e).replace("\n", ""))) return False if port < 1 or port > 65535: @@ -1742,98 +1683,50 @@ def in_OPENBATTLE(self, client, type, natType, password, port, maxplayers, hashc return if maxplayers > 10 and not client.bot: maxplayers = 10 - self.out_SERVERMSG(client, "Without botflag its not allowed to host battles with > 10 players. Your battle was restricted to 10 players") + self.out_SERVERMSG(client, "A botflag is required to host battles with > 10 players. Your battle was restricted to 10 players") - client.current_battle = battle_id - - battle = Battle.Battle( - root=self._root, id=battle_id, type=type, natType=int(natType), - password=password, port=port, maxplayers=maxplayers, hashcode=hashcode, - rank=rank, maphash=maphash, map=map, title=title, modname=modname, - passworded=passworded, host=client.session_id, users={client.session_id}) + battle_name = '__battle__' + str(client.user_id) + if battle_name in self._root.channels: + battle = self._root.channels[battle_name] + else: + battle = Battle.Battle(self._root, battle_name) + self._root.channels[battle_name] = battle + + battle.battle_id = battle_id + battle.host = client.session_id + battle.key = key + battle.type = type + battle.natType = natType + battle.port = port + battle.title = title + battle.map = map + battle.maphash = maphash + battle.modname = modname + battle.hashcode = hashcode battle.engine=engine battle.version=version - - self._root.battles[battle_id] = battle + battle.rank = rank + battle.maxplayers = maxplayers + + self._root.battles[battle.battle_id] = battle self.broadcast_AddBattle(battle) - client.Send('OPENBATTLE %s' % battle_id) - client.Send('JOINBATTLE %s %s' % (battle_id, hashcode)) + + battle.joinBattle(client) + client.Send('OPENBATTLE %s' % battle.battle_id) + client.Send('JOINBATTLE %s %s' % (battle.battle_id, hashcode)) client.Send('REQUESTBATTLESTATUS') def in_SAYBATTLE(self, client, msg): - ''' - Send a message to all users in your current battle. - - @required.str message: The message to send. - ''' - if not msg: return + # deprecated, see 'u' compat flag battle = self.getCurrentBattle(client) - if not battle: - return - user = client.username - self.broadcast_SendBattle(battle, 'SAIDBATTLE %s %s' % (user, msg), client) + if not battle: return + self.in_SAY(client, battle.name, msg) def in_SAYBATTLEEX(self, client, msg): - ''' - Send an action to all users in your current battle. - - @required.str message: The action to send. - ''' + # deprecated, see 'u' compat flag battle = self.getCurrentBattle(client) - if not battle: - return - self.broadcast_SendBattle(battle, 'SAIDBATTLEEX %s %s' % (client.username, msg), client) - - def _joinBattle(self, client, battle): - ''' - Makes a client join a battle / updates changes / sends to all clients - ''' - battle_users = battle.users - battle_bots = battle.bots - startrects = battle.startrects - client.Send('JOINBATTLE %s %s' % (battle.id, battle.hashcode)) - battle.users.add(client.session_id) - scripttags = [] - for tag, val in battle.script_tags.items(): - scripttags.append('%s=%s'%(tag, val)) - client.Send('SETSCRIPTTAGS %s'%'\t'.join(scripttags)) - if battle.disabled_units: - client.Send('DISABLEUNITS %s' % ' '.join(battle.disabled_units)) - self._root.broadcast('JOINEDBATTLE %s %s' % (battle.id, client.username), ignore=set([battle.host, client.session_id])) - - scriptPassword = client.scriptPassword - host = self.clientFromSession(battle.host) - if host.compat['sp'] and scriptPassword: # supports scriptPassword - host.Send('JOINEDBATTLE %s %s %s' % (battle.id, client.username, scriptPassword)) - client.Send('JOINEDBATTLE %s %s %s' % (battle.id, client.username, scriptPassword)) - else: - host.Send('JOINEDBATTLE %s %s' % (battle.id, client.username)) - client.Send('JOINEDBATTLE %s %s' % (battle.id, client.username)) - - if battle.natType > 0: - if battle.host == client.session_id: - raise NameError('%s is having an identity crisis' % (client.name)) - if client.udpport: - self._root.usernames[host].Send('CLIENTIPPORT %s %s %s' % (username, client.ip_address, client.udpport)) - - specs = 0 - for sessionid in battle.users: - battle_client = self.clientFromSession(sessionid) - if battle_client and battle_client.battlestatus['mode'] == '0': - specs += 1 - battlestatus = self._calc_battlestatus(battle_client) - client.Send('CLIENTBATTLESTATUS %s %s %s' % (battle_client.username, battlestatus, battle_client.teamcolor)) - - for iter in battle_bots: - bot = battle_bots[iter] - client.Send('ADDBOT %s %s' % (battle.id, iter)+' %(owner)s %(battlestatus)s %(teamcolor)s %(AIDLL)s' % (bot)) - for allyno in startrects: - rect = startrects[allyno] - client.Send('ADDSTARTRECT %s' % (allyno)+' %(left)s %(top)s %(right)s %(bottom)s' % (rect)) - client.battlestatus = {'ready':'0', 'id':'0000', 'ally':'0000', 'mode':'0', 'sync':'00', 'side':'00', 'handicap':'0000000'} - client.teamcolor = '0' - client.current_battle = battle.id - client.Send('REQUESTBATTLESTATUS') + if not battle: return + self.in_SAYEX(client, battle.name, msg) def in_JOINBATTLEACCEPT(self, client, username): ''' @@ -1842,12 +1735,11 @@ def in_JOINBATTLEACCEPT(self, client, username): @required.str username: The user to allow into your battle. ''' - battle_id = client.current_battle user = self.clientFromUsername(username) if not user: self.out_FAILED(client, 'JOINBATTLEACCEPT', "Couldn't find user %s" %(username), True) return - battle = self._root.battles[battle_id] + battle = self.getCurrentBattle(client) if not client.session_id == battle.host: self.out_FAILED(client, 'JOINBATTLEACCEPT', "client isn't the specified host %s vs %s" %(client.session_id, battle.host), True) return @@ -1855,7 +1747,7 @@ def in_JOINBATTLEACCEPT(self, client, username): self.out_FAILED(client, 'JOINBATTLEACCEPT', "client isn't in pending users %s %s" %(client.username, username), True) return battle.pending_users.remove(user.session_id) - self._joinBattle(user, battle) + battle.joinBattle(user) def in_JOINBATTLEDENY(self, client, username, reason=None): ''' @@ -1868,19 +1760,18 @@ def in_JOINBATTLEDENY(self, client, username, reason=None): user = self.clientFromUsername(username) if not user: return - battle_id = client.current_battle - battle = self._root.battles[battle_id] + battle = self.getCurrentBattle(client) if not client.username == battle.host: return if user.session_id in battle.pending_users: battle.pending_users.remove(user.session_id) user.Send('JOINBATTLEFAILED %s%s' % ('Denied by host', (' ('+reason+')' if reason else ''))) - def in_JOINBATTLE(self, client, battle_id, password=None, scriptPassword=None): + def in_JOINBATTLE(self, client, battle_id, key=None, scriptPassword=None): ''' Attempt to join target battle. @required.int battleID: The ID of the battle to join. - @optional.str password: The password to use if the battle requires one. + @optional.str key: The password to use if the battle requires one. @optional.str scriptPassword: A password unique to your user, to verify users connecting to the actual game. ''' if scriptPassword: client.scriptPassword = scriptPassword @@ -1893,24 +1784,28 @@ def in_JOINBATTLE(self, client, battle_id, password=None, scriptPassword=None): username = client.username if client.current_battle in self._root.battles: - client.Send('JOINBATTLEFAILED You are already in a battle.') + client.Send('JOINBATTLEFAILED You are already in a battle') return if battle_id not in self._root.battles: - client.Send('JOINBATTLEFAILED Battle does not exist.') + client.Send('JOINBATTLEFAILED Battle does not exist') return battle = self._root.battles[battle_id] if client.session_id in battle.users: # user is already in battle client.Send('JOINBATTLEFAILED Client is already in battle') return host = self.clientFromSession(battle.host) - if battle.passworded == 1 and not battle.password == password: - client.Send('JOINBATTLEFAILED Incorrect password.') - return - if battle.locked: - client.Send('JOINBATTLEFAILED Battle is locked.') - return - if host.compat['b'] and not (host.bot and 'mod' in client.accesslevels): # supports battleAuth + if not battle.isFounder(client) and not 'mod' in client.accesslevels: + if not battle.key in ('*', None) and not battle.key == key: + client.Send('JOINBATTLEFAILED Incorrect password') + return + if client.user_id in battle.ban: + client.Send('JOINBATTLEFAILED You are banned from the battle') + return + if battle.locked: + client.Send('JOINBATTLEFAILED Battle is locked') + return + if host.compat['b'] and not (battle.hasBotflag() and 'mod' in client.accesslevels): # supports battleAuth if client.session_id in battle.pending_users: client.Send('JOINBATTLEFAILED waiting for JOINBATTLEACCEPT/JOINBATTLEDENIED from host') else: @@ -1921,7 +1816,7 @@ def in_JOINBATTLE(self, client, battle_id, password=None, scriptPassword=None): client_ip = client.ip_address host.Send('JOINBATTLEREQUEST %s %s' % (username, client_ip)) return - self._joinBattle(client, battle) + battle.joinBattle(client) def in_SETSCRIPTTAGS(self, client, scripttags): ''' @@ -1930,9 +1825,9 @@ def in_SETSCRIPTTAGS(self, client, scripttags): @required.str scriptTags: A tab-separated list of key=value pairs. ''' - battle = self._canForceBattle(client) - if not battle: - self.out_FAILED(client, "SETSCRIPTTAGS", "You are not allowed to change settings as client in a game!", True) + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + self.out_FAILED(client, "SETSCRIPTTAGS", "You are not allowed to change settings in this battle", True) return setscripttags = self._parseTags(scripttags) @@ -1950,8 +1845,9 @@ def in_REMOVESCRIPTTAGS(self, client, tags): @required.str tags: A space-separated list of tags. ''' - battle = self._canForceBattle(client) - if not battle: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + self.out_FAILED(client, "REMOVESCRIPTTAGS", "You are not allowed to change settings in this battle", True) return rem = set() @@ -1970,43 +1866,17 @@ def in_LEAVEBATTLE(self, client): ''' Leave current battle. ''' - client.scriptPassword = None - - username = client.username - battle_id = client.current_battle - client.current_battle = None - client.hostport = None - if not battle_id: + battle = self.getCurrentBattle(client) + if not battle: self.out_FAILED(client, "LEAVEBATTLE", "not in battle") return - if not battle_id in self._root.battles: + if not battle.battle_id in self._root.battles: self.out_FAILED(client, "LEAVEBATTLE", "couldn't find battle") - return - battle = self._root.battles[battle_id] + return + battle.leaveBattle(client) if battle.host == client.session_id: self.broadcast_RemoveBattle(battle) - del self._root.battles[battle_id] - return - battle.users.remove(client.session_id) - - for bot in list(client.battle_bots): - del client.battle_bots[bot] - if bot in battle.bots: - del battle.bots[bot] - self._root.broadcast_battle('REMOVEBOT %s %s' % (battle_id, bot), battle_id) - self._root.broadcast('LEFTBATTLE %s %s'%(battle_id, client.username)) - - oldspecs = battle.spectators - - specs = 0 - for session_id in battle.users: - user = self.clientFromSession(session_id) - if user and user.battlestatus['mode'] == '0': - specs += 1 - - battle.spectators = specs - if oldspecs != specs: - self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' % (battle.id, battle.spectators, battle.locked, battle.maphash, battle.map)) + del self._root.battles[battle.battle_id] def in_MYBATTLESTATUS(self, client, _battlestatus, _myteamcolor): ''' @@ -2051,7 +1921,7 @@ def in_MYBATTLESTATUS(self, client, _battlestatus, _myteamcolor): elif mode == '0': spectators += 1 - oldstatus = self._calc_battlestatus(client) + oldstatus = battle.calc_battlestatus(client) oldcolor = client.teamcolor client.battlestatus.update({'ready':ready, 'id':id1+id2+id3+id4, 'ally':ally1+ally2+ally3+ally4, 'mode':mode, 'sync':sync1+sync2, 'side':side1+side2+side3+side4}) client.teamcolor = myteamcolor @@ -2060,14 +1930,14 @@ def in_MYBATTLESTATUS(self, client, _battlestatus, _myteamcolor): battle.spectators = spectators if oldspecs != spectators: - self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' % (battle.id, battle.spectators, battle.locked, battle.maphash, battle.map)) + self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' % (battle.battle_id, battle.spectators, battle.locked, battle.maphash, battle.map)) - newstatus = self._calc_battlestatus(client) + newstatus = battle.calc_battlestatus(client) statuscmd = 'CLIENTBATTLESTATUS %s %s %s'%(client.username, newstatus, myteamcolor) if oldstatus == newstatus and client.teamcolor == oldcolor: #nothing changed, just send back to client client.Send(statuscmd) return - self._root.broadcast_battle(statuscmd, battle.id) + self._root.broadcast_battle(statuscmd, battle.battle_id) def in_UPDATEBATTLEINFO(self, client, SpectatorCount, locked, maphash, mapname): ''' @@ -2094,11 +1964,11 @@ def in_UPDATEBATTLEINFO(self, client, SpectatorCount, locked, maphash, mapname): self.out_SERVERMSG(client, "UPDATEBATTLEINFO failed - invalid mapname send: %s" %(str(mapname)), True) return - oldstr = 'UPDATEBATTLEINFO %s %i %i %s %s' % (battle.id, battle.spectators, battle.locked, battle.maphash, battle.map) + oldstr = 'UPDATEBATTLEINFO %s %i %i %s %s' % (battle.battle_id, battle.spectators, battle.locked, battle.maphash, battle.map) battle.locked = int(locked) battle.maphash = maphash battle.map = mapname - newstr = 'UPDATEBATTLEINFO %s %i %i %s %s' % (battle.id, battle.spectators, battle.locked, battle.maphash, battle.map) + newstr = 'UPDATEBATTLEINFO %s %i %i %s %s' % (battle.battle_id, battle.spectators, battle.locked, battle.maphash, battle.map) if oldstr != newstr: self._root.broadcast(newstr) @@ -2127,7 +1997,7 @@ def in_MYSTATUS(self, client, _status): client.went_ingame = None if client.session_id == battle.host: if client.hostport: - self._root.broadcast_battle('HOSTPORT %i' % client.hostport, battle_id, host) + self._root.broadcast_battle('HOSTPORT %i' % client.hostport, battle.battle_id, host) elif was_ingame and not client.is_ingame and client.went_ingame: ingame_time = (time.time() - client.went_ingame) / 60 if ingame_time >= 1: @@ -2170,9 +2040,10 @@ def in_GETCHANNELMESSAGES(self, client, chan, lastid): @required.str lastid: messages to get since this id ''' if not chan in self._root.channels: - return - + return channel = self._root.channels[chan] + if channel.id == 0: + return # unregistered channels use id 0 if not client.session_id in channel.users: self.out_FAILED(client, "GETCHANNELMESSAGES", "Can't get channel messages when not joined", True) return @@ -2237,9 +2108,9 @@ def in_ADDSTARTRECT(self, client, allyno, left, top, right, bottom): @required.float right: The right side of the rectangle. @required.float bottom: The bottom side of the rectangle. ''' - battle = self._canForceBattle(client) - if not battle: - return + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return try: allyno = int32(allyno) rect = { @@ -2261,8 +2132,8 @@ def in_REMOVESTARTRECT(self, client, allyno): @required.int allyno: The ally number for the rectangle. ''' - battle = self._canForceBattle(client) - if not battle: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): return allyno = int32(allyno) try: @@ -2279,9 +2150,9 @@ def in_DISABLEUNITS(self, client, units): @required.str units: A string-separated list of unit names to disable. ''' - battle = self._canForceBattle(client) - if not battle: - return + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return units = units.split(' ') disabled_units = [] for unit in units: @@ -2299,8 +2170,8 @@ def in_ENABLEUNITS(self, client, units): @required.str units: A string-separated list of unit names to enable. ''' - battle = self._canForceBattle(client) - if not battle: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): return units = units.split(' ') enabled_units = [] @@ -2317,9 +2188,10 @@ def in_ENABLEALLUNITS(self, client): Enable all units. [host] ''' - battle = self._canForceBattle(client) - if not battle: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): return + battle.disabled_units = [] self._root.broadcast_battle('ENABLEALLUNITS', client.current_battle) @@ -2331,15 +2203,18 @@ def in_HANDICAP(self, client, username, value): @required.str username: The player to handicap. @required.int handicap: The percentage of handicap to give (1-100). ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: return if not value.isdigit() or not int(value) in range(0, 101): return - user.battlestatus['handicap'] = self._dec2bin(value, 7) - self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, self._calc_battlestatus(user), user.teamcolor), user.current_battle) + battle = self.getCurrentBattle(client) + self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, battle.calc_battlestatus(user), user.teamcolor), user.current_battle) def in_KICKFROMBATTLE(self, client, username): ''' @@ -2348,13 +2223,15 @@ def in_KICKFROMBATTLE(self, client, username): @required.str username: The player to kick. ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): return - - if client.bot and 'mod' in user.accesslevels: # disallow kicking mods from bot hosted battles + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: return - + if 'mod' in user.accesslevels: + return + user.Send('FORCEQUITBATTLE %s' %(client.username)) self.in_LEAVEBATTLE(user) @@ -2367,11 +2244,17 @@ def in_FORCETEAMNO(self, client, username, teamno): @required.str username: The target player. @required.int teamno: The team to assign them. ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: return + user.battlestatus['id'] = self._dec2bin(teamno, 4) - self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, self._calc_battlestatus(user), user.teamcolor), user.current_battle) + battle = self.getCurrentBattle(client) + if not battle: return + self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, battle.calc_battlestatus(user), user.teamcolor), user.current_battle) def in_FORCEALLYNO(self, client, username, allyno): ''' @@ -2381,11 +2264,17 @@ def in_FORCEALLYNO(self, client, username, allyno): @required.str username: The target player. @required.int teamno: The ally team to assign them. ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: return + user.battlestatus['ally'] = self._dec2bin(allyno, 4) - self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, self._calc_battlestatus(user), user.teamcolor), user.current_battle) + battle = self.getCurrentBattle(client) + if not battle: return + self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, battle.calc_battlestatus(user), user.teamcolor), user.current_battle) def in_FORCETEAMCOLOR(self, client, username, teamcolor): ''' @@ -2396,12 +2285,17 @@ def in_FORCETEAMCOLOR(self, client, username, teamcolor): @required.sint teamcolor: The color to assign, represented with hex 0xBBGGRR as a signed integer. ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): return - + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: + return + user.teamcolor = teamcolor - self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, self._calc_battlestatus(user), user.teamcolor), user.current_battle) + battle = self.getCurrentBattle(client) + if not battle: return + self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, battle.calc_battlestatus(user), user.teamcolor), user.current_battle) def in_FORCESPECTATORMODE(self, client, username): ''' @@ -2411,20 +2305,22 @@ def in_FORCESPECTATORMODE(self, client, username): @required.str username: The target player. ''' - user = self._canForceBattleUser(client, username) - if not user: + battle = self.getCurrentBattle(client) + if not battle or not battle.canChangeSettings(client): + return + user = self.clientFromUsername(username) + if not user or not user.session_id in battle.users: return - if not user.battlestatus['mode'] == '1': + if not user.battlestatus['mode'] == '1': # ??! return battle = self.getCurrentBattle(user) if not battle: return - battle.spectators += 1 user.battlestatus['mode'] = '0' - self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, self._calc_battlestatus(user), user.teamcolor), user.current_battle) - self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' %(battle.id, battle.spectators, battle.locked, battle.maphash, battle.map)) + self._root.broadcast_battle('CLIENTBATTLESTATUS %s %s %s'%(username, battle.calc_battlestatus(user), user.teamcolor), user.current_battle) + self._root.broadcast('UPDATEBATTLEINFO %s %i %i %s %s' %(battle.battle_id, battle.spectators, battle.locked, battle.maphash, battle.map)) def in_ADDBOT(self, client, name, battlestatus, teamcolor, AIDLL): ''' @@ -2444,9 +2340,9 @@ def in_ADDBOT(self, client, name, battlestatus, teamcolor, AIDLL): if name in battle.bots: self.out_FAILED(client, "ADDBOT", "Bot already exists!", True) return - client.battle_bots[name] = battle.id + client.battle_bots[name] = battle.battle_id battle.bots[name] = {'owner':client.username, 'battlestatus':battlestatus, 'teamcolor':teamcolor, 'AIDLL':AIDLL} - self._root.broadcast_battle('ADDBOT %s %s %s %s %s %s'%(battle.id, name, client.username, battlestatus, teamcolor, AIDLL), battle.id) + self._root.broadcast_battle('ADDBOT %s %s %s %s %s %s'%(battle.battle_id, name, client.username, battlestatus, teamcolor, AIDLL), battle.battle_id) def in_UPDATEBOT(self, client, name, battlestatus, teamcolor): ''' @@ -2464,10 +2360,7 @@ def in_UPDATEBOT(self, client, name, battlestatus, teamcolor): if name in battle.bots: if client.username == battle.bots[name]['owner'] or client.session_id == battle.host: battle.bots[name].update({'battlestatus':battlestatus, 'teamcolor':teamcolor}) - self._root.broadcast_battle('UPDATEBOT %s %s %s %s'%(battle.id, name, battlestatus, teamcolor), battle.id) - - - + self._root.broadcast_battle('UPDATEBOT %s %s %s %s'%(battle.battle_id, name, battlestatus, teamcolor), battle.battle_id) def in_REMOVEBOT(self, client, name): ''' @@ -2484,7 +2377,7 @@ def in_REMOVEBOT(self, client, name): if client.username == battle.bots[name]['owner'] or client.session_id == battle.host: del self._root.usernames[battle.bots[name]['owner']].battle_bots[name] del battle.bots[name] - self._root.broadcast_battle('REMOVEBOT %s %s'%(battle.id, name), battle.id) + self._root.broadcast_battle('REMOVEBOT %s %s'%(battle.battle_id, name), battle.battle_id) def in_GETINGAMETIME(self, client, username=None): ''' @@ -2770,7 +2663,7 @@ def in_SETMINSPRINGVERSION(self, client, version): legacyBattleIds = [] for battleId, battle in self._root.battles.items(): - if self.hasBotflag(battle) and not self._validEngineVersion(battle.engine, battle.version): + if battle.hasBotflag() and not self._validEngineVersion(battle.engine, battle.version): legacyBattleIds.append(battleId) host = self.clientFromSession(battle.host) self.broadcast_SendBattle(battle, 'SAIDBATTLEEX %s -- This battle will close -- %s %s or later is now required by the server. Please join a battle with the new Spring version!' % (host.username, 'Spring', version)) @@ -3113,42 +3006,10 @@ def in_JSON(self, client, rawcmd): battle = self.getCurrentBattle(client) if not battle: #needs to be in battle return - data = {"PROMOTE": {"battleid": battle.id}} + data = {"PROMOTE": {"battleid": battle.battle_id}} self._root.broadcast('JSON ' + json.dumps(data, separators=(',', ':'))) return - if "BATTLEMUTE" in cmd: - params = cmd["BATTLEMUTE"] - if not "mod" in client.accesslevels: - self.out_JSON(client, "FAILED", {"msg": "Moderator permissions needed: %s" %(client.access)}) - return - if not "username" in cmd["BATTLEMUTE"]: - return - - username = cmd["BATTLEMUTE"]["username"] - badclient = self.clientFromUsername(username) - if not badclient: - self.out_JSON(client, "FAILED", {"msg": 'Client not found'}) - - duration = 60 #minutes - if "duration" in params: - try: - duration = int(duration) - except: - self.out_JSON(client, "FAILED", {"msg": 'Duration must be a int (the ban duration in minutes)'}) - return - - battle = self.getCurrentBattle(badclient) - if not bad_client.user_id in battle.mutelist: - battle.mutelist[bad_client.user_id] = datetime.datetime.now() - - battle.mutelist[bad_client.user_id] += datetime.delta(minutes=duration) - enddate = battle.mutelist[bad_client.user_id] - - self.broadcast_SendBattle(battle, 'SAIDBATTLE %s <%s> was muted until %s by %s.' % (battle.host.username, baduser.username, enddate, client.username), client) - - return - self.out_JSON(client, "FAILED", {"msg": "Unknown command: %s" %(rawcmd)}) # Begin outgoing protocol section #