class ReplayPlayer(Hub): ''' Emulates a normal server by outputting the same messages that a server once did. ''' def __init__(self, filename, *args, **kwargs): super(ReplayPlayer, self).__init__(*args, **kwargs) self.tickPeriod = TICK_PERIOD self.finished = False self.loop = WeakLoopingCall(self, 'tick') self.agentIds = [] self.nextAgentId = 0 self.settings, self.msgIterator = readReplay(open(filename, 'rb')) def popSettings(self): result, self.settings = self.settings, None return result def start(self): self.loop.start(self.tickPeriod) def stop(self): if self.loop.running: self.loop.stop() def tick(self): while True: try: msg = next(self.msgIterator) except StopIteration: self.finished = True if self.node: while self.agentIds: agentId = self.agentIds.pop(0) # Give 2 seconds before ending the replay reactor.callLater(2, self.node.agentDisconnected, agentId) self.loop.stop() break self.node.gotServerCommand(msg) if isinstance(msg, TickMsg): break def connectNewAgent(self, authTag=0): agentId = self.nextAgentId self.nextAgentId += 1 self.agentIds.append(agentId) return agentId def disconnectAgent(self, agentId): self.agentIds.remove(agentId) def sendRequestToGame(self, agentId, msg): pass
class StartGameWhenReadyTrigger(Trigger): def __init__(self, level): super(StartGameWhenReadyTrigger, self).__init__(level) self.loop = WeakLoopingCall(self, 'check') def doActivate(self): if not self.loop.running: self.loop.start(3, False) def doDeactivate(self): if self.loop.running: self.loop.stop() def check(self): try: self.world.voteArbiter.startNewGameIfReady() except Exception: log.exception('Error trying to start new game')
class AchievementManager(object): SAVE_PERIOD = 20 achievementDefs = availableAchievements def __init__(self, game): self.game = game self.world = world = game.world self.lastSave = timeNow() # All players recorded this game, indexed by user id. self.allPlayers = {} defs = self.achievementDefs self.oncePerGameAchievements = [c(world) for c in defs.oncePerGame] self.oncePerTeamPerGameAchievements = [ c(world, team) for c in defs.oncePerTeamPerGame for team in world.teams ] self.loop = WeakLoopingCall(self, 'save') self.started = False self.stopped = False def save(self, force=False): for playerAchievements in list(self.allPlayers.values()): playerAchievements.saveProgress(force=force) def start(self): assert not self.started self.started = True for player in self.world.players: self.playerAdded(player) self.world.onPlayerAdded.addListener(self.playerAdded) self.loop.start(self.SAVE_PERIOD) for a in self.oncePerGameAchievements: a.onUnlocked.addListener(self.achievementUnlocked) a.start() for a in self.oncePerTeamPerGameAchievements: a.start() a.onUnlocked.addListener(self.achievementUnlocked) def stop(self): assert not self.stopped self.stopped = True self.loop.stop() self.world.onPlayerAdded.removeListener(self.playerAdded) self.save(force=True) for a in self.oncePerGameAchievements: a.stop() a.onUnlocked.removeListener(self.achievementUnlocked) for a in self.oncePerTeamPerGameAchievements: a.stop() a.onUnlocked.removeListener(self.achievementUnlocked) for p in list(self.allPlayers.values()): p.stop() @defer.inlineCallbacks def playerAdded(self, player): if self.world.isServer and not player.joinComplete: # We want to identify by user rather than nickname if the user # is authenticated. yield player.onJoinComplete.wait() name = player.identifyingName if name not in self.allPlayers: a = PlayerAchievements(self, player) self.allPlayers[name] = a a.start() else: self.allPlayers[name].rejoined(player) def triggerAchievement(self, player, achievementId): ''' Called from stats manager to trigger a stats-related achievement. Never raises an exception. ''' try: a = self.allPlayers[ player.identifyingName].achievements[achievementId] a.achievementTriggered() except Exception: log.exception('Error triggering achievement') def achievementUnlocked(self, achievement, player): if player.user is not None: player.user.achievementUnlocked(achievement.idstring) self.game.sendServerCommand( AchievementUnlockedMsg(player.id, achievement.idstring))
class AddBotsForLobbyTrigger(Trigger): ''' Adds a number of bots so even a single player can mess around in the lobby until others arrive. Keeps the total number of bots at least up to a particular minimum, adding and removing bots as necessary. ''' def __init__(self, level, playerCount=6): from trosnoth.bots.base import listAIs super(AddBotsForLobbyTrigger, self).__init__(level) self.playerCount = playerCount self.loop = WeakLoopingCall(self, 'recheck') self.agents = [] self.newAgents = set() self.aiNames = listAIs(playableOnly=True) def doActivate(self): if not self.loop.running: self.loop.start(3, False) def doDeactivate(self): if self.loop.running: self.loop.stop() self._stopAllAgents() @defer.inlineCallbacks def recheck(self): self._graduateNewAgents() yield self._adjustAgentsToTarget() def _graduateNewAgents(self): for agent in list(self.newAgents): if agent.player is not None: self.agents.append(agent) self.newAgents.remove(agent) def _stopAllAgents(self): if len(self.agents) != 0: log.info('AIInjector: Stopping all agents') for agent in self.agents: agent.stop() self.world.game.detachAgent(agent) self.agents = [] @defer.inlineCallbacks def _adjustAgentsToTarget(self): worldPlayers = len(self.world.game.world.players) newAgents = len(self.newAgents) if self.playerCount > worldPlayers + newAgents: yield self._addAgents(self.playerCount - worldPlayers - newAgents) else: self._removeAgents(worldPlayers + newAgents - self.playerCount) @defer.inlineCallbacks def _addAgents(self, count): log.info('AIInjector: Adding %d agents', count) for i in range(count): agent = yield self.world.game.addBot(random.choice(self.aiNames)) self.newAgents.add(agent) def _removeAgents(self, count): if count != 0: log.info('AIInjector: Removing %d agents', count) for i in range(count): if len(self.agents) == 0: break agent = self.agents.pop(0) agent.stop() self.world.game.detachAgent(agent)
class AchievementBox(framework.CompoundElement): achievementDefs = availableAchievements def __init__(self, app, player, achievementId): super(AchievementBox, self).__init__(app) self.app = app self.player = player self.achievements = [achievementId] self.width = 453 self.height = 75 self._setColours() self.area = Area( FullScreenAttachedPoint(ScaledSize(0, -100), 'midbottom'), ScaledSize(self.width, self.height), 'midbottom') self.smlBox = Area( FullScreenAttachedPoint(ScaledSize(-self.width / 2 + 6, -104), 'midbottom'), ScaledSize(66, 66), 'bottomleft') self.titleText = TextElement( self.app, "ACHIEVEMENT UNLOCKED!", self.fonts.achievementTitleFont, Location( FullScreenAttachedPoint( ScaledSize(73 / 2, -100 - self.height + 10), 'midbottom'), 'midtop'), self.borderColour) self.nameText = TextElement( self.app, self.achievementDefs.getAchievementDetails(achievementId)[0], self.fonts.achievementNameFont, Location( FullScreenAttachedPoint(ScaledSize(73 / 2, -100 - 13), 'midbottom'), 'midbottom'), self.colours.black) self.elements = [self.titleText, self.nameText] self._updateImage() self.cycler = WeakLoopingCall(self, 'cycleAchievements') self.cycler.start(5.0, now=False) def addAchievement(self, achievementId): self.achievements.append(achievementId) self.titleText.setText("%d ACHIEVEMENTS UNLOCKED!" % len(self.achievements)) def _updateImage(self): try: filepath = self.app.theme.getPath('achievements', '%s.png' % self.achievements[0]) except IOError: filepath = self.app.theme.getPath('achievements', 'default.png') image = pygame.image.load(filepath).convert() image = pygame.transform.smoothscale( image, ScaledSize(64, 64).getSize(self.app)) if type(self.elements[-1]) == PictureElement: self.elements.pop() self.image = PictureElement( self.app, image, Location( FullScreenAttachedPoint(ScaledSize(-self.width / 2 + 7, -105), 'midbottom'), 'bottomleft')) self.elements.append(self.image) def cycleAchievements(self): del self.achievements[0] if len(self.achievements) == 0: self.cycler.stop() return elif len(self.achievements) == 1: self.titleText.setText("ACHIEVEMENT UNLOCKED!") else: self.titleText.setText("%d ACHIEVEMENTS UNLOCKED!" % len(self.achievements)) self.nameText.setText( self.achievementDefs.getAchievementDetails( self.achievements[0])[0]) self._updateImage() def _setColours(self): self.colours = self.app.theme.colours self.fonts = self.app.screenManager.fonts if self.player.teamId == 'A': self.borderColour = self.colours.achvBlueBorder self.bgColour = self.colours.achvBlueBackground else: self.borderColour = self.colours.achvRedBorder self.bgColour = self.colours.achvRedBackground def _getRect(self, area): return area.getRect(self.app) def draw(self, surface): mainRect = self._getRect(self.area) boxRect = self._getRect(self.smlBox) surface.fill(self.bgColour, mainRect) pygame.draw.rect(surface, self.borderColour, mainRect, 2) surface.fill(self.colours.white, boxRect) pygame.draw.rect(surface, self.borderColour, boxRect, 1) super(AchievementBox, self).draw(surface)
class GameInterface(framework.CompoundElement, ConcreteAgent): '''Interface for when we are connected to a game.''' achievementDefs = availableAchievements def __init__(self, app, game, onDisconnectRequest=None, onConnectionLost=None, replay=False, authTag=0): super(GameInterface, self).__init__(app, game=game) self.localState.onShoxwave.addListener(self.localShoxwaveFired) self.localState.onGameInfoChanged.addListener(self.gameInfoChanged) self.world.onOpenChatReceived.addListener(self.openChat) self.world.onTeamChatReceived.addListener(self.teamChat) self.world.onReset.addListener(self.worldReset) self.world.onGrenadeExplosion.addListener(self.grenadeExploded) self.world.onTrosballExplosion.addListener(self.trosballExploded) self.world.onBomberExplosion.addListener(self.trosballExploded) self.world.uiOptions.onChange.addListener(self.uiOptionsChanged) self.timingsLoop = WeakLoopingCall(self, '_sendPing') self.timingsLoop.start(1, now=False) self.subscribedPlayers = set() self.onDisconnectRequest = Event() if onDisconnectRequest is not None: self.onDisconnectRequest.addListener(onDisconnectRequest) self.onConnectionLost = Event() if onConnectionLost is not None: self.onConnectionLost.addListener(onConnectionLost) self.game = game self.joinDialogReason = None self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) self.runningPlayerInterface = None self.updateKeyMapping() self.gameViewer = viewManager.GameViewer(self.app, self, game, replay) if replay: self.joinController = None else: self.joinController = JoinGameController(self.app, self, self.game) self.detailsInterface = DetailsInterface(self.app, self) self.winnerMsg = WinnerMsg(app) self.timingInfo = TimingInfo( app, self, Location(Canvas(307, 768), 'midbottom'), app.screenManager.fonts.timingsFont, ) self.gameInfoDisplay = GameInfoDisplay( app, self, Region(topleft=Screen(0.01, 0.05), size=Canvas(330, 200))) self.hotkeys = hotkey.Hotkeys(self.app, self.keyMapping, self.detailsInterface.doAction) self.terminal = None self.joinInProgress = False self.vcInterface = None if replay: self.vcInterface = ViewControlInterface(self.app, self) self.ready = False defer.maybeDeferred(game.addAgent, self, authTag=authTag).addCallback(self.addedAgent) self.setElements() def _sendPing(self): for i in range(3): data = bytes([random.randrange(256)]) if data not in self.localState.pings: self.sendRequest(PingMsg(data)) return def gameInfoChanged(self): self.gameInfoDisplay.refreshInfo() def addedAgent(self, result): self.ready = True if self.joinController: self.joinDialogReason = 'automatic' self.joinController.start() def spectatorWantsToJoin(self): if self.runningPlayerInterface or not self.joinController: return self.joinDialogReason = 'from menu' self.joinController.maybeShowJoinDialog(autoJoin=True) def sendRequest(self, msg): if not self.ready: # Not yet completely connected to game return super(GameInterface, self).sendRequest(msg) def worldReset(self, *args, **kwarsg): self.winnerMsg.hide() if self.ready and self.joinController: self.joinController.gotWorldReset() self.gameViewer.reset() def updateKeyMapping(self): # Set up the keyboard mapping. try: # Try to load keyboard mappings from the user's personal settings. with open(getPath(user, 'keymap'), 'r') as f: config = f.read() self.keyMapping.load(config) if self.runningPlayerInterface: self.runningPlayerInterface.keyMappingUpdated() except IOError: pass @ConnectionLostMsg.handler def connectionLost(self, msg): self.cleanUp() if self.joinController: self.joinController.hide() self.onConnectionLost.execute() def joined(self, player): '''Called when joining of game is successful.''' pygame.key.set_repeat() self.gameViewer.worldgui.overridePlayer(self.localState.player) self.runningPlayerInterface = pi = PlayerInterface(self.app, self) self.detailsInterface.setPlayer(pi.player) self.setElements() self.joinController.hide() self.gameViewer.leaderboard.update() def spectate(self): ''' Called by join controller if user selects to only spectate. ''' self.vcInterface = ViewControlInterface(self.app, self) self.setElements() self.joinController.hide() def joinDialogCancelled(self): if self.joinDialogReason == 'automatic': self.disconnect() else: self.spectate() def stop(self): super(GameInterface, self).stop() self.localState.onShoxwave.removeListener(self.localShoxwaveFired) self.localState.onGameInfoChanged.removeListener(self.gameInfoChanged) self.world.onOpenChatReceived.removeListener(self.openChat) self.world.onTeamChatReceived.removeListener(self.teamChat) self.world.onReset.removeListener(self.worldReset) self.world.onGrenadeExplosion.removeListener(self.grenadeExploded) self.world.onTrosballExplosion.removeListener(self.trosballExploded) self.world.onBomberExplosion.removeListener(self.trosballExploded) self.world.uiOptions.onChange.removeListener(self.uiOptionsChanged) self.timingsLoop.stop() self.gameViewer.stop() if self.runningPlayerInterface is not None: self.runningPlayerInterface.stop() def setElements(self): spectate = replay = False if self.runningPlayerInterface: self.elements = [ self.gameViewer, self.runningPlayerInterface, self.gameInfoDisplay, self.hotkeys, self.detailsInterface, self.winnerMsg, self.timingInfo ] else: self.elements = [ self.gameViewer, self.gameInfoDisplay, self.hotkeys, self.detailsInterface, self.winnerMsg, self.timingInfo ] if self.vcInterface is not None: self.elements.insert(2, self.vcInterface) if self.joinController: spectate = True else: replay = True self.detailsInterface.menuManager.setMode(spectate=spectate, replay=replay) def toggleTerminal(self): if self.terminal is None: locs = {'app': self.app} if hasattr(self.app, 'getConsoleLocals'): locs.update(self.app.getConsoleLocals()) self.terminal = console.TrosnothInteractiveConsole( self.app, self.app.screenManager.fonts.consoleFont, Region(size=Screen(1, 0.4), bottomright=Screen(1, 1)), locals=locs) self.terminal.interact().addCallback(self._terminalQuit) from trosnoth.utils.utils import timeNow if self.terminal in self.elements: if timeNow() > self._termWaitTime: self.elements.remove(self.terminal) else: self._termWaitTime = timeNow() + 0.1 self.elements.append(self.terminal) self.setFocus(self.terminal) def _terminalQuit(self, result): if self.terminal in self.elements: self.elements.remove(self.terminal) self.terminal = None def disconnect(self): self.cleanUp() self.onDisconnectRequest.execute() def joinGame(self, nick, head, team, timeout=10): if self.joinInProgress: return if team is None: teamId = NEUTRAL_TEAM_ID else: teamId = team.id self.joinInProgress = True self.sendJoinRequest(teamId, nick, head) WeakCallLater(timeout, self, '_joinTimedOut') def setPlayer(self, player): if not player: self.gameViewer.worldgui.removeOverride() self.lostPlayer() super(GameInterface, self).setPlayer(player) if player: if __debug__ and globaldebug.enabled: globaldebug.localPlayerId = player.id self.joinInProgress = False self.joined(player) @CannotJoinMsg.handler def joinFailed(self, msg): self.joinInProgress = False self.joinController.joinFailed(msg.reasonId) def _joinTimedOut(self): if self.player or not self.joinInProgress: return self.joinInProgress = False self.joinController.joinFailed('timeout') def cleanUp(self): if self.gameViewer.timerBar is not None: self.gameViewer.timerBar = None pygame.key.set_repeat(300, 30) def uiOptionsChanged(self): if self.world.uiOptions.showGameOver: winner = self.world.uiOptions.winningTeam if winner: self.winnerMsg.show( '{} win'.format(winner), self.app.theme.colours.chatColour(winner), ) else: self.winnerMsg.show('Game drawn', (128, 128, 128)) else: self.winnerMsg.hide() @PlayerCoinsSpentMsg.handler def discard(self, msg): pass @AwardPlayerCoinMsg.handler def playerAwardedCoin(self, msg): if not self.localState.player: return if msg.sound and msg.playerId == self.localState.player.id: self.playSound('gotCoin') @PlayerHasElephantMsg.handler def gotElephant(self, msg, _lastElephantPlayer=[None]): player = self.world.getPlayer(msg.playerId) if player and player != _lastElephantPlayer[0]: message = '%s now has %s!' % (player.nick, player.world.uiOptions.elephantName) self.detailsInterface.newMessage(message) _lastElephantPlayer[0] = player @PlayerHasTrosballMsg.handler def gotTrosball(self, msg, _lastTrosballPlayer=[None]): player = self.world.playerWithId.get(msg.playerId) if player != _lastTrosballPlayer[0]: _lastTrosballPlayer[0] = player if player is None: message = 'The ball has been dropped!' else: message = '%s has the ball!' % (player.nick, ) self.detailsInterface.newMessage(message) @AddPlayerMsg.handler def addPlayer(self, msg): player = self.world.getPlayer(msg.playerId) if player and player not in self.subscribedPlayers: self.subscribedPlayers.add(player) team = player.team if player.team else self.world.rogueTeamName message = '%s has joined %s' % (player.nick, team) self.detailsInterface.newMessage(message) player.onDied.addListener(partial(self.playerDied, player)) @SetPlayerTeamMsg.handler def changeTeam(self, msg): self.defaultHandler(msg) # Make sure the local player changes team player = self.world.getPlayer(msg.playerId) if player: message = '%s has joined %s' % (player.nick, self.world.getTeamName(msg.teamId)) self.detailsInterface.newMessage(message) @RemovePlayerMsg.handler def handle_RemovePlayerMsg(self, msg): player = self.world.getPlayer(msg.playerId) if player: message = '%s has left the game' % (player.nick, ) self.detailsInterface.newMessage(message) self.subscribedPlayers.discard(player) def lostPlayer(self): self.runningPlayerInterface.stop() self.runningPlayerInterface = None self.setElements() @CannotBuyUpgradeMsg.handler def notEnoughCoins(self, msg): if msg.reasonId == NOT_ENOUGH_COINS_REASON: text = 'Your team does not have enough coins.' elif msg.reasonId == CANNOT_REACTIVATE_REASON: text = 'You already have that item.' elif msg.reasonId == PLAYER_DEAD_REASON: text = 'You cannot buy an upgrade while dead.' elif msg.reasonId == GAME_NOT_STARTED_REASON: text = 'Upgrades can' 't be bought at this time.' elif msg.reasonId == TOO_CLOSE_TO_EDGE_REASON: text = 'You are too close to the zone edge.' elif msg.reasonId == TOO_CLOSE_TO_ORB_REASON: text = 'You are too close to the orb.' elif msg.reasonId == NOT_IN_DARK_ZONE_REASON: text = 'You are not in a dark friendly zone.' elif msg.reasonId == INVALID_UPGRADE_REASON: text = 'Upgrade not recognised by server.' elif msg.reasonId == DISABLED_UPGRADE_REASON: text = 'That upgrade is currently disabled.' else: text = 'You cannot buy that item at this time.' self.detailsInterface.newMessage(text) self.defaultHandler(msg) @PlayerHasUpgradeMsg.handler def gotUpgrade(self, msg): player = self.world.getPlayer(msg.playerId) if player: self.detailsInterface.upgradeUsed(player, msg.upgradeType) upgradeClass = self.world.getUpgradeType(msg.upgradeType) existing = player.items.get(upgradeClass) if not existing: if (self.detailsInterface.player is None or self.detailsInterface.player.isFriendsWith(player)): self.playSound('buyUpgrade') self.defaultHandler(msg) @ChatFromServerMsg.handler def gotChatFromServer(self, msg): self.detailsInterface.newMessage(msg.text.decode('utf-8'), error=msg.error) @TaggingZoneMsg.handler def zoneTagged(self, msg): try: zone = self.world.zoneWithId[msg.zoneId] zoneLabel = zone.defn.label except KeyError: zoneLabel = '<?>' if msg.playerId != NO_PLAYER: try: player = self.world.playerWithId[msg.playerId] except KeyError: nick = '<?>' else: nick = player.nick message = '%s tagged zone %s' % (nick, zoneLabel) self.detailsInterface.newMessage(message) def playerDied(self, target, killer, deathType): if deathType == OFF_MAP_DEATH_HIT: messages = [ 'fell into the void', 'looked into the abyss', 'dug too greedily and too deep' ] message = '%s %s' % (target.nick, random.choice(messages)) elif deathType == TROSBALL_DEATH_HIT: message = '%s was killed by the Trosball' % (target.nick, ) elif deathType == BOMBER_DEATH_HIT: message = '%s head asplode' % (target.nick, ) thisPlayer = self.detailsInterface.player if thisPlayer and target.id == thisPlayer.id: self.detailsInterface.doAction(ACTION_CLEAR_UPGRADE) else: if killer is None: message = '%s was killed' % (target.nick, ) self.detailsInterface.newMessage(message) else: message = '%s killed %s' % (killer.nick, target.nick) self.detailsInterface.newMessage(message) @RespawnMsg.handler def playerRespawn(self, msg): player = self.world.getPlayer(msg.playerId) if player: message = '%s is back in the game' % (player.nick, ) self.detailsInterface.newMessage(message) @CannotRespawnMsg.handler def respawnFailed(self, msg): if msg.reasonId == GAME_NOT_STARTED_REASON: message = 'The game has not started yet.' elif msg.reasonId == ALREADY_ALIVE_REASON: message = 'You are already alive.' elif msg.reasonId == BE_PATIENT_REASON: message = 'You cannot respawn yet.' elif msg.reasonId == ENEMY_ZONE_REASON: message = 'Cannot respawn outside friendly zone.' elif msg.reasonId == FROZEN_ZONE_REASON: message = 'That zone has been frozen!' else: message = 'You cannot respawn here.' self.detailsInterface.newMessage( message, self.app.theme.colours.errorMessageColour) def sendPrivateChat(self, player, targetId, text): self.sendRequest(ChatMsg(PRIVATE_CHAT, targetId, text=text.encode())) def sendTeamChat(self, player, text): self.sendRequest(ChatMsg(TEAM_CHAT, player.teamId, text=text.encode())) def sendPublicChat(self, player, text): self.sendRequest(ChatMsg(OPEN_CHAT, text=text.encode())) def openChat(self, text, sender): text = ': ' + text self.detailsInterface.newChat(text, sender) def teamChat(self, team, text, sender): player = self.detailsInterface.player if player and player.isFriendsWithTeam(team): text = ' (team): ' + text self.detailsInterface.newChat(text, sender) @AchievementUnlockedMsg.handler def achievementUnlocked(self, msg): player = self.world.getPlayer(msg.playerId) if not player: return achievementName = self.achievementDefs.getAchievementDetails( msg.achievementId)[0] self.detailsInterface.newMessage( '%s has unlocked "%s"!' % (player.nick, achievementName), self.app.theme.colours.achievementMessageColour) focusPlayer = self.detailsInterface.player if (focusPlayer is not None and focusPlayer.id == msg.playerId): self.detailsInterface.localAchievement(msg.achievementId) @ShotFiredMsg.handler def shotFired(self, msg): self.defaultHandler(msg) try: shot = self.world.getShot(msg.shotId) except KeyError: return pos = shot.pos dist = self.distance(pos) self.playSound('shoot', self.getSoundVolume(dist)) def grenadeExploded(self, pos, radius): self.gameViewer.worldgui.addExplosion(pos) dist = self.distance(pos) self.playSound('explodeGrenade', self.getSoundVolume(dist)) def trosballExploded(self, player): self.gameViewer.worldgui.addTrosballExplosion(player.pos) dist = self.distance(player.pos) self.playSound('explodeGrenade', self.getSoundVolume(dist)) @FireShoxwaveMsg.handler def shoxwaveExplosion(self, msg): localPlayer = self.localState.player if localPlayer and msg.playerId == localPlayer.id: return self.gameViewer.worldgui.addShoxwaveExplosion((msg.xpos, msg.ypos)) def localShoxwaveFired(self): localPlayer = self.localState.player self.gameViewer.worldgui.addShoxwaveExplosion(localPlayer.pos) @UpgradeChangedMsg.handler def upgradeChanged(self, msg): self.detailsInterface.upgradeDisplay.refresh() def distance(self, pos): return distance(self.gameViewer.viewManager.getTargetPoint(), pos) def getSoundVolume(self, distance): 'The volume for something that far away from the player' # Up to 500px away is within the "full sound zone" - full sound distFromScreen = max(0, distance - 500) # 1000px away from "full sound zone" is 0 volume: return 1 - min(1, (distFromScreen / 1000.)) def playSound(self, action, volume=1): self.app.soundPlayer.play(action, volume) @PlaySoundMsg.handler def playSoundFromServerCommand(self, msg): self.app.soundPlayer.playFromServerCommand( msg.filename.decode('utf-8')) @TickMsg.handler def handle_TickMsg(self, msg): super(GameInterface, self).handle_TickMsg(msg) self.timingInfo.ticksSeen += 1
class AIAgent(ConcreteAgent): ''' Base class for an AI agent. ''' def __init__(self, game, aiClass, fromLevel, nick=None, *args, **kwargs): super(AIAgent, self).__init__(game=game, *args, **kwargs) self.aiClass = aiClass self.fromLevel = fromLevel self._initialisationNick = nick self.ai = None self.team = None self.requestedNick = None self._onBotSet = Event([]) self._loop = WeakLoopingCall(self, '_tick') def __str__(self): if self.ai: bot = self.ai else: bot = 'None (was {})'.format(self.aiClass.__name__) return '{}<{}>'.format(self.__class__.__name__, bot) def start(self, team=None): self.team = team self._loop.start(2) def stop(self): super(AIAgent, self).stop() self._loop.stop() if self.ai: self.ai.disable() self.ai = None def detached(self): super().detached() self.stop() def _tick(self): if self.ai is not None: return if self.fromLevel and self._initialisationNick: nick = self._initialisationNick else: nick = self.aiClass.nick if self.team is None: teamId = NEUTRAL_TEAM_ID else: teamId = self.team.id self._joinGame(nick, teamId) def _joinGame(self, nick, teamId): self.requestedNick = nick self.sendJoinRequest(teamId, nick, bot=True, fromLevel=self.fromLevel) @CannotJoinMsg.handler def _joinFailed(self, msg): r = msg.reasonId nick = self.requestedNick if r == GAME_FULL_REASON: message = 'full' elif r == UNAUTHORISED_REASON: message = 'not authenticated' elif r == NICK_USED_REASON: message = 'nick in use' elif r == USER_IN_GAME_REASON: message = 'user already in game' # Should never happen elif r == ALREADY_JOINED_REASON: message = 'tried to join twice' # Should never happen else: message = repr(r) log.error('Join failed for AI %r (%s)', nick, message) self.stop() def setPlayer(self, player): if player is None and self.ai: self.ai.disable() self.ai = None super(AIAgent, self).setPlayer(player) if player: self.requestedNick = None self.ai = self.aiClass(self.world, self.localState.player, self) self._onBotSet() @defer.inlineCallbacks def getBot(self): ''' @return: a Deferred which fires with this agent's Bot object, as soon as it has one. ''' if self.ai is not None: defer.returnValue(self.ai) yield self._onBotSet.wait() defer.returnValue(self.ai) @TickMsg.handler def handle_TickMsg(self, msg): super(AIAgent, self).handle_TickMsg(msg) if self.ai: self.ai.consumeMsg(msg) @ResyncPlayerMsg.handler def handle_ResyncPlayerMsg(self, msg): super(AIAgent, self).handle_ResyncPlayerMsg(msg) if self.ai: self.ai.playerResynced() @WorldResetMsg.handler def handle_WorldResetMsg(self, msg): super().handle_WorldResetMsg(msg) if self.ai: self.ai.worldReset() def defaultHandler(self, msg): super(AIAgent, self).defaultHandler(msg) if self.ai: self.ai.consumeMsg(msg)
class LagAnalyser(object): TIME_SPAN = 5 NOTE_TIME = 0.1 def __init__(self): self.lastNote = None self.ignoreUntil = None self.reset() self.monitor = WeakLoopingCall(self, 'notePeriod') self.monitor.start(self.NOTE_TIME) def reset(self): self.startTime = timeNow() self.ticks = [self.startTime] def notePeriod(self): ''' Called periodically to allow the analyser to notice if the reactor is not running smoothly. ''' now = timeNow() if self.lastNote is None: self.lastNote = now return period = now - self.lastNote self.lastNote = now if period - self.NOTE_TIME >= self.NOTE_TIME: # Reactor congestion: reset tally self.ignoreUntil = now + self.NOTE_TIME def gotTick(self): now = timeNow() if self.ignoreUntil is not None: if now > self.ignoreUntil: self.reset() else: return self.ticks.append(now) limit = now - self.TIME_SPAN while self.ticks[0] < limit: self.ticks.pop(0) def getIdealUILag(self): ''' Based on the ticks received, calculates the time which the UI should lag behind receiving messages, in order for things to be displayed smoothly. ''' final = self.ticks[-1] initial = max(self.startTime, final - self.TIME_SPAN) oldT = initial oldValue = upper = lower = 0 for t in self.ticks: value = oldValue + (t - oldT) upper = max(upper, value) value -= TICK_PERIOD lower = min(lower, value) oldT, oldValue = t, value return upper - lower
class WebServer(ServerState): def __init__(self, authFactory, serverPort): ServerState.__init__(self, pages) self.authFactory = authFactory self.serverPort = serverPort self.nextEventListeners = [] self._loop = WeakLoopingCall(self, 'keepEventPipeAlive') self._loop.start(5, False) self.gameServer = None self.authFactory.onPrimaryGameChanged.addListener( self.gameServerChanged) def gameServerChanged(self, gameServer): if self.gameServer: world = self.gameServer.game.world world.onPlayerAdded.removeListener(self.playerCountChanged) world.onPlayerRemoved.removeListener(self.playerCountChanged) world.onStartMatch.removeListener(self.levelChanged) world.onTeamScoreChanged.removeListener(self.teamScoreChanged) self.gameServer = gameServer if gameServer: world = gameServer.game.world world.onPlayerAdded.addListener(self.playerCountChanged) world.onPlayerRemoved.addListener(self.playerCountChanged) world.onStartMatch.addListener(self.levelChanged) world.onTeamScoreChanged.addListener(self.teamScoreChanged) self.transmitEvent(self.getInitialEvents()) def inLobby(self): game = self.gameServer.game return game.world.uiOptions.showReadyStates def getPlayerCount(self): game = self.gameServer.game return len([p for p in game.world.players if not p.bot]) def playerCountChanged(self, *args, **kwargs): if self.inLobby(): playerCount = self.getPlayerCount() playersString = '1 player' if playerCount == 1 else '%d players' % ( playerCount, ) self.transmitEvent('message("%s in lobby.");' % (playersString, )) def levelChanged(self, *args, **kwargs): self.transmitEvent(self.getInitialEvents()) def getInitialEvents(self): if self.gameServer is None: return 'hideScoreboard();message("No running games on server.");\n' if self.inLobby(): playerCount = self.getPlayerCount() return 'hideScoreboard();message("%d players in lobby.");\n' % ( playerCount, ) return 'hideMessage();%s\n' % (self.getScoreMessage(), ) def teamScoreChanged(self, *args, **kwargs): self.transmitEvent(self.getScoreMessage()) def getScoreMessage(self): world = self.gameServer.game.world blueTeam, redTeam = world.teams[:2] blueZones = blueTeam.numZonesOwned redZones = redTeam.numZonesOwned totalZones = len(world.zones) neutralZones = totalZones - blueZones - redZones return 'score(%d,%d,%d,%d,%d);' % ( blueTeam.orbScore, redTeam.orbScore, blueZones, neutralZones, redZones, ) def waitForEvent(self): d = defer.Deferred() self.nextEventListeners.append(d) return d def transmitEvent(self, jsCommand): listeners = self.nextEventListeners self.nextEventListeners = [] for d in listeners: d.callback(jsCommand + '\n') def keepEventPipeAlive(self): ''' To make sure that a reverse proxy doesn't close connections due to inactivity. ''' self.transmitEvent('')
class LocalGame(Game): def __init__(self, layoutDatabase, halfMapWidth=None, mapHeight=None, blockRatio=0.5, duration=0, maxPerTeam=100, maxTotal=500, serverInterface=None, onceOnly=False, saveReplay=False, gamePrefix=None, replayPath=None, level=None, gameType=None, lobbySettings=None, *args, **kwargs): self._serverCommandStack = [] self._waitingForEmptyCommandQueue = [] self.maxPerTeam = maxPerTeam self.maxTotalPlayers = min(2 * maxPerTeam, maxTotal) self.serverInterface = serverInterface self.lobbySettings = lobbySettings self.agents = set() self.agentInfos = {} self.layoutDatabase = layoutDatabase if level is None: level = StandardRandomLevel(halfMapWidth, mapHeight, blockRatio=blockRatio, duration=duration) world = ServerUniverse(self, layoutDatabase, onceOnly=onceOnly, level=level, gameType=gameType) self.idManager = world.idManager self.gameRecorder = GameRecorder(self, world, saveReplay=saveReplay, gamePrefix=gamePrefix, replayPath=replayPath) super(LocalGame, self).__init__(world, *args, **kwargs) world.onServerTickComplete.addListener(self.worldTickDone) self.gameRecorder.start() self.updateDelayLoop = WeakLoopingCall(self, 'updateDelays') self.updateDelayLoop.start(10, False) self.achievementManager = AchievementManager(self) self.achievementManager.start() if VERIFY_PLAYER_CONSISTENCY: self.playerConsistencyVerifier = PlayerConsistencyVerifier(self) def updateDelays(self): for info in self.agentInfos.values(): info.updateDelays() def addAgent(self, agent, user=None, authTag=0): agent.user = user info = AgentInfo(self, agent) self.agentInfos[agent] = info self.agents.add(agent) def detachAgent(self, agent): if agent not in self.agents: return self.agents.remove(agent) if agent.player: self.kickPlayer(agent.player.id) self.agentInfos[agent].takePlayer() del self.agentInfos[agent] agent.detached() def kickPlayer(self, playerId): ''' Removes the player with the specified ID from the game. ''' self.sendServerCommand(RemovePlayerMsg(playerId)) def agentRequest(self, agent, msg): ''' Some messages need to be delayed until the correct time comes. ''' msg.tracePoint(self, 'agentRequest') if agent not in self.agents: # Probably just because it's a delayed call log.debug('LocalGame got message %s from unconnected agent', msg) return info = self.agentInfos[agent] info.requestFromAgent(msg) def dispatchDelayedRequest(self, agent, msg): ''' Called by an AgentInfo when the correct time comes for a request to be dispatched. ''' msg.tracePoint(self, 'dispatchDelayedRequest') msg.serverApply(self, agent) def setPlayerLimits(self, maxPerTeam, maxTotal=40): ''' Changes the player limits in the current game. Note that this does not affect players who are already in the game. @param maxPerTeam: Maximum number of players per team at once @param maxTotal: Maximum number of players in the game at once ''' self.maxPerTeam = maxPerTeam self.maxTotalPlayers = min(2 * maxPerTeam, maxTotal) def stop(self): super(LocalGame, self).stop() self.gameRecorder.stop() self.achievementManager.stop() self.updateDelayLoop.stop() self.idManager.stop() def worldTickDone(self): ''' Called when the universe has ticked. ''' self.checkCollisionsWithCollectables() for info in self.agentInfos.values(): info.tick() def checkCollisionsWithCollectables(self): ''' When a player runs into a collectable unit (e.g. a coin or the trosball), we pay attention not to where we think the collectable units are, but where the player's client thinks they are. To do this, we need to project the collectable units back in time based on the player's current delay. ''' greatestDelay = 0 for info in self.agentInfos.values(): if not info.player or info.player.dead: continue greatestDelay = max(greatestDelay, info.currentDelay) for unit in self.world.getCollectableUnits(): if unit.checkCollision(info.player, info.currentDelay): unit.collidedWithPlayer(info.player) for unit in self.world.getCollectableUnits(): unit.clearOldHistory(greatestDelay) def joinSuccessful(self, agent, playerId): super(LocalGame, self).joinSuccessful(agent, playerId) if agent in self.agentInfos: self.agentInfos[agent].givePlayer(self.world.getPlayer(playerId)) def playerRemoved(self, player, playerId): super(LocalGame, self).playerRemoved(player, playerId) info = self.agentInfos.get(player.agent) if info: info.takePlayer() def sendServerCommand(self, msg): ''' Sends a command to the universe and all attached agents. Typically called by message classes in serverApply(). ''' self._serverCommandStack.append(msg) if len(self._serverCommandStack) > 1: # Sometimes one of the calls below (e.g. self.world.consumeMsg()) # triggers another sendServerCommand() call, so to make sure # that all the messages arrive at the clients in the same order # as the server, we queue them and release them as soon as the # first message is completely sent. msg.tracePoint(self, '(sendServerCommand: deferred)') return while self._serverCommandStack: cmd = self._serverCommandStack[0] cmd.tracePoint(self, 'sendServerCommand') if VERIFY_PLAYER_CONSISTENCY: self.playerConsistencyVerifier.preMessage(cmd) self.world.consumeMsg(cmd) self.gameRecorder.consumeMsg(cmd) if VERIFY_PLAYER_CONSISTENCY: self.playerConsistencyVerifier.postMessage(cmd) self.onServerCommand(cmd) cmd.tracePoint(self, 'done sendServerCommand') self._serverCommandStack.pop(0) # Release things that are waiting for the command stack to empty while not self._serverCommandStack and self._waitingForEmptyCommandQueue: d = self._waitingForEmptyCommandQueue.pop(0) try: d.callback(None) except Exception: log.exception('Error in queued callback') def waitForEmptyCommandQueue(self): ''' :return: a Deferred which will callback when the queue of server commands to send is empty. This is useful to be sure that the world is in a consistent state (all messages have been processed). ''' if not self._serverCommandStack: return defer.succeed(None) d = defer.Deferred() self._waitingForEmptyCommandQueue.append(d) return d def sendResync(self, playerId, reason='Your computer was out of sync with the server!'): ''' Resyncs the position of the player with the given id. Typically called by message classes in serverApply(). ''' player = self.world.getPlayer(playerId) player.sendResync(reason)
class ReplayPlayer(Hub): ''' Emulates a normal server by outputting the same messages that a server once did. ''' def __init__(self, filename, *args, **kwargs): super(ReplayPlayer, self).__init__(*args, **kwargs) self.tickPeriod = TICK_PERIOD self.file = open(filename, 'rb') self.finished = False self.loop = WeakLoopingCall(self, 'tick') self.agentIds = [] self.nextAgentId = 0 try: length = struct.unpack('!I', self.file.read(4))[0] except struct.error: raise ReplayFileError('invalid replay format') data = self.file.read(length) self.settings = unrepr(data) def popSettings(self): result, self.settings = self.settings, None return result def start(self): self.loop.start(self.tickPeriod) def stop(self): if self.loop.running: self.loop.stop() def tick(self): while True: data = self.file.read(4) if not data: self.finished = True if self.node: while self.agentIds: agentId = self.agentIds.pop(0) # Give 2 seconds before ending the replay reactor.callLater( 2, self.node.agentDisconnected, agentId) self.loop.stop() break length = struct.unpack('!I', data)[0] data = self.file.read(length) try: msg = clientMsgs.buildMessage(data) except MessageTypeError: log.warning('WARNING: UNKNOWN MESSAGE: %r' % (data,)) continue self.node.gotServerCommand(msg) if isinstance(msg, TickMsg): break def connectNewAgent(self): agentId = self.nextAgentId self.nextAgentId += 1 self.agentIds.append(agentId) return agentId def disconnectAgent(self, agentId): self.agentIds.remove(agentId) def sendRequestToGame(self, agentId, msg): pass