class RadioButtonGroup(framework.CompoundElement): def __init__(self, app): super(RadioButtonGroup, self).__init__(app) self.elements = [] self.selectedIndex = -1 self.onSelectionChanged = Event() self.by_value = {} def add(self, radioButton): self.elements.append(radioButton) self.by_value[radioButton.value] = radioButton radioButton._joinGroup(self) def getSelectedValue(self): if self.selectedIndex == -1: return None else: return self.elements[self.selectedIndex].value def selected(self, radioButton): if radioButton in self.by_value: radioButton = self.by_value[radioButton] assert radioButton in self.elements, ("Cannot select a radiobutton " "that isn't in this group") newIndex = self.elements.index(radioButton) if newIndex == self.selectedIndex: return if self.selectedIndex != -1: self.elements[self.selectedIndex].deselected() self.selectedIndex = newIndex radioButton.selected() self.onSelectionChanged.execute(self.selectedIndex)
class TextBoxCell(Cell): def __init__(self, app, row, column): super(TextBoxCell, self).__init__(app, row, column) textAlign = self.styleGet('textAlign') self.inputBox = InputBox( self.app, Area( CellAttachedPoint((0, 0), self, textAlign), self._getUsableRect().size, textAlign), font=self.styleGet('font'), colour=self.styleGet('foreColour')) self.inputBox.onEdit.addListener(self._valueChanged) self.elements = [self.inputBox] self._oldText = '' self._readOnly = True self.setReadOnly(False) self.onValueChanged = Event() def _valueChanged(self, sender): self._oldText = self.inputBox.getValue() self.onValueChanged.execute(self) def setReadOnly(self, readOnly): if readOnly == self._readOnly: return self._readOnly = readOnly if readOnly: self.inputBox.onClick.removeListener(self._boxClicked) else: self.inputBox.onClick.addListener(self._boxClicked) self.setFocus(None) def lostFocus(self): self.setFocus(None) def setText(self, text): if text != self._oldText: self.inputBox.setValue(text) self._styleChanged = True self._oldText = text def getText(self): return self._oldText def setMaxLength(self, length): self.inputBox.setMaxLength(length) def setValidator(self, validator): self.inputBox.setValidator(validator) def _update(self): self.inputBox.setFont(self.styleGet('font')) self.inputBox.setFontColour(self.styleGet('foreColour')) bgColour = self.styleGet('backColour') if bgColour is not None: self.inputBox.setBackColour(bgColour) textAlign = self.styleGet('textAlign') self.inputBox.area.anchor = textAlign self.inputBox.area.point.attachedAt = textAlign
class Hotkey(Element): def __init__(self, app, key, modifiers): super(Hotkey, self).__init__(app) self.onTriggered = Event() self.key = key self.mods = modifiers def processEvent(self, event): if event.type == pygame.KEYDOWN: if self.testEquals(event.key, event.mod): self.onTriggered.execute() return return event def __str__(self): return keyboard.shortcutName(self.key, self.mods) def testEquals(self, key, modifiers, mapping=None): 'Checks if the key press matches this shortcut.' # First test: key equality. if key <> self.key: return False # Second test: modifier compatibility. for kmod, modName in keyboard.KMOD_NAMES: if self.mods & kmod: if not (modifiers & kmod): return False elif modifiers & kmod: return False # Testing modifiers as done above ignores left/right status. return True
class MenuManager(object): def __init__(self, defaultMenu=None): self.defaultMenu = defaultMenu self.menu = defaultMenu self.menuStack = [] self.onShowMenu = Event() # (menu) def setDefaultMenu(self, defaultMenu): default = len(self.menuStack) == 0 and self.menu is self.defaultMenu self.defaultMenu = defaultMenu if default or self.menu is None: self.menu = defaultMenu self.onShowMenu.execute(defaultMenu) def reset(self): self.menu = self.defaultMenu self.menuStack = [] self.onShowMenu.execute(self.defaultMenu) def cancel(self): if len(self.menuStack) > 0: self.menu = self.menuStack.pop() else: self.menu = self.defaultMenu self.onShowMenu.execute(self.menu) def showMenu(self, menu): self.menuStack.append(self.menu) self.menu = menu self.onShowMenu.execute(menu)
class ConnectionFailedDialog(DialogBox): def __init__(self, app, reason): width = app.screenManager.scaleFactor * absoluteWidth font = app.fonts.connectionFailedFont boundary = app.screenManager.scaleFactor * 20 lines = wrapWords(app, reason, font, width - 2 * boundary) x = boundary elements = [] colours = app.theme.colours for text in lines: elements.append( TextElement(app, text, font, Location( DialogBoxAttachedPoint(self, (0, x), 'midtop'), 'midtop'), colour=colours.errorColour)) x += font.getHeight(app) # Boundary * 3 for top, bottom, and middle height = boundary * 3 + font.getHeight(app) * (len(lines) + 1) size = ScaledSize(absoluteWidth, height) super(ConnectionFailedDialog, self).__init__(app, size, 'Connection Failed') self.elements = elements self.elements.append( TextButton(app, Location( DialogBoxAttachedPoint(self, (0, -boundary), 'midbottom'), 'midbottom'), 'OK', font, colours.dialogButtonColour, colours.radioMouseover, onClick=lambda sender: self.close())) self.onEnter = Event() self.onEnter.addListener(self.close) def processEvent(self, event): if event.type == pygame.KEYDOWN and event.key in (pygame.K_KP_ENTER, pygame.K_RETURN): self.onEnter.execute() return None else: return super(ConnectionFailedDialog, self).processEvent(event)
class MenuItem(object): ''' @param name the caption of the menu item @param action a callable for when the item is selected ''' def __init__(self, name, action=None, listener=None, enabled=True): self.name = name self.onClick = Event() # (MenuItem) self.onAction = Event() # (action) self.action = action self.enabled = enabled if listener is not None: self.onClick.addListener(listener) def execute(self): self.onClick.execute(self) if self.action is not None: self.onAction.execute(self.action)
class PractiseScreen(framework.CompoundElement): def __init__(self, app, onClose=None, onStart=None): super(PractiseScreen, self).__init__(app) self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) self.onStart = Event() if onStart is not None: self.onStart.addListener(onStart) if app.displaySettings.alphaOverlays: alpha = 192 else: alpha = None bg = SolidRect(app, app.theme.colours.playMenu, alpha, Region(centre=Canvas(512, 384), size=Canvas(924, 500))) #colour = app.theme.colours.mainMenuColour #font = app.screenManager.fonts.consoleFont font = app.screenManager.fonts.bigMenuFont cancel = TextButton(app, Location(Canvas(512, 624), 'midbottom'), 'Cancel', font, app.theme.colours.secondMenuColour, app.theme.colours.white, onClick=self.cancel) self.elements = [bg, cancel] def cancel(self, element): self.onClose.execute() def startGame(self): db = self.app.layoutDatabase game = LocalGame(db, SIZE[0], SIZE[1], onceOnly=True) for i in range(AICOUNT): game.addBot('ranger') self.onStart.execute(game)
class MappingHotkey(Element): def __init__(self, app, key, mapping=None): ''' key may be a physical pygame key or a string where mapping[pygameKey] = key ''' super(MappingHotkey, self).__init__(app) self.key = key self.onActivated = Event() if mapping is None: mapping = {} self.mapping = mapping def processEvent(self, event): if event.type == pygame.KEYDOWN: if self.mapping.get(event.key, None) == self.key: self.onActivated.execute() return None if event.key == self.key: self.onActivated.execute() return None return event def __repr__(self): if isinstance(self.key, int): key = self.key else: # Calculate the physical key. for key, value in self.mapping.iteritems(): if value == self.key: break else: return '??' return keyboard.shortcutName(key, 0)
class Hotkeys(Element): ''' Traps any keys which occur in the mapping and carries out some action on them. Note that this observes only, but does not stop the keypress events from passing through. ''' def __init__(self, app, mapping, onActivated=None): super(Hotkeys, self).__init__(app) self.mapping = mapping self.onActivated = Event() if onActivated is not None: self.onActivated.addListener(onActivated) def processEvent(self, event): if event.type == pygame.KEYDOWN: try: action = self.mapping[event.key] except KeyError: return event # Process this hotkey. self.onActivated.execute(action) return event def __repr__(self): if isinstance(self.key, int): key = self.key else: # Calculate the physical key. for key, value in self.mapping.iteritems(): if value == self.key: break else: return '??' return keyboard.shortcutName(key, 0)
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.authTag = authTag 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.consoleFont, ) 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).addCallback(self.addedAgent) self.setElements() 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. config = open(getPath(user, 'keymap'), 'rU').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.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, 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, authTag=self.authTag) 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 Jerakeen!' % (player.nick, ) 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 == ALREADY_TURRET_REASON: text = 'There is already a turret in this zone.' 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)): if upgradeClass == Turret: self.playSound('turret') else: self.playSound('buyUpgrade') self.defaultHandler(msg) @ChatFromServerMsg.handler def gotChatFromServer(self, msg): self.detailsInterface.newMessage(msg.text.decode(), 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: 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: message = '%s was killed by the Trosball' % (target.nick, ) elif deathType == BOMBER_DEATH: message = '%s head asplode' % (target.nick, ) thisPlayer = self.detailsInterface.player if thisPlayer and target.id == thisPlayer.id: self.detailsInterface.doAction('no 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 ListBox(Element): def __init__(self, app, area, items, font, colour, hlColour=None, showBtns=True): super(ListBox, self).__init__(app) self.area = area self.items = items self.font = font self.showBtns = showBtns self.onValueChanged = Event() self.setColour(colour, hlColour) self.offset = 0 self.index = -1 # Create up and down buttons. if showBtns: img1 = TextImage('up', font, colour) img2 = TextImage('up', font, self.hlColour) self.upBtn = HoverButton( self.app, Location(AttachedPoint((0, 0), self._getRect, 'topright'), 'topright'), img1, img2) self.upBtn.onClick.addListener(self.upClick) img1 = TextImage('down', font, colour) img2 = TextImage('down', font, self.hlColour) self.dnBtn = HoverButton( self.app, Location(AttachedPoint((0, 0), self._getRect, 'bottomright'), 'bottomright'), img1, img2) self.dnBtn.onClick.addListener(self.dnClick) def _getRect(self): return self.area.getRect(self.app) def _getMaxInView(self): return int((self._getSize()[1] - 1) / self._getItemHeight()) + 1 def _getItemHeight(self): return int(self.font.getLineSize(self.app)) def setColour(self, colour, hlColour=None): self.colour = colour if hlColour: self.hlColour = hlColour else: self.hlColour = tuple((255 + i) / 2 for i in colour) self.bgColour = uniqueColour([self.hlColour, self.colour]) def setItems(self, items): self.items = items if self.index >= len(self.items): self.index = -1 def getItem(self, index=None): if index == None: index = self.index if index >= 0 and index < len(self.items): return self.items[index] raise ValueError, "Listbox doesn't have that many items" def getNumItems(self): return len(self.items) def setIndex(self, index): if index >= self.getNumItems(): raise ValueError, "Listbox doesn't have that many items" self.index = index def getIndex(self): return self.index def _getSize(self): return self._getRect().size def _getPt(self): rect = self._getRect() return rect.topleft def draw(self, screen): rect = self._getRect() pos = rect.topleft self.offset = min(self.offset, max(0, len(self.items) - self._getMaxInView() + 1)) # Draw the items. y = pos[1] for i in xrange( self.offset, min(self.offset + self._getMaxInView(), len(self.items))): text = self.font.render(self.app, self.items[i], True, self.colour) self._blitTextToScreen(screen, text, (pos[0], y)) # Highlight selected one. if i == self.index: text = self.font.render(self.app, self.items[i], True, self.hlColour) hlOffset = int(self._getItemHeight() / 30. + .5) self._blitTextToScreen(screen, text, (pos[0] + hlOffset, y + hlOffset)) y = y + self._getItemHeight() if self.showBtns: self.upBtn.draw(screen) self.dnBtn.draw(screen) def _blitTextToScreen(self, screen, textImage, placement): rect = self._getRect() textRect = textImage.get_rect() textRect.topleft = placement clippedRect = textRect.clip(rect) clippedRect.topleft = (0, 0) screen.blit(textImage, placement, clippedRect) def processEvent(self, event): if self.showBtns: event = self.upBtn.processEvent(event) if not event: return event = self.dnBtn.processEvent(event) if not event: return # Click on element selects it. if (event.type == pygame.MOUSEBUTTONDOWN and self._getRect().collidepoint(event.pos)): # Left-click if event.button == 1: rect = self._getRect() pos = rect.topleft index = ((event.pos[1] - pos[1]) / self._getItemHeight() + self.offset) if index < len(self.items): self.index = index self.onValueChanged.execute(index) else: return event # Scroll up elif event.button == 4: self._changeSelection(max(self.index - 1, 0)) # Scroll down elif event.button == 5: self._changeSelection(min(self.index + 1, len(self.items) - 1)) else: return event elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: self._changeSelection(max(self.index - 1, 0)) elif event.key == pygame.K_DOWN: self._changeSelection(min(self.index + 1, len(self.items) - 1)) elif event.key == pygame.K_PAGEUP: self._changeSelection(max(self.index - self._getMaxInView(), 0)) elif event.key == pygame.K_PAGEDOWN: self._changeSelection( min(self.index + self._getMaxInView(), len(self.items) - 1)) elif event.key == pygame.K_HOME: self._changeSelection(0) elif event.key == pygame.K_END: self._changeSelection(len(self.items) - 1) else: return event else: return event return None def _changeSelection(self, newIndex): self.setIndex(newIndex) self._putSelectedInView() self.onValueChanged.execute(self.index) def _putSelectedInView(self): if self.offset + self._getMaxInView() <= self.index: self.offset = (min(self.index + 3, len(self.items)) - self._getMaxInView()) elif self.offset > self.index: self.offset = max(self.index - 2, 0) def tick(self, deltaT): if not self.showBtns: return self.upBtn.tick(deltaT) self.dnBtn.tick(deltaT) def upClick(self, btn): self.offset = max(self.offset - 1, 0) def dnClick(self, btn): self.offset = self.offset + 1
class TrosnothServerFactory(MsgServer): messages = serverMsgs def __init__( self, game, noAuth=False, agentCallback=None, *args, **kwargs): self.game = game self.noAuth = noAuth self.agentCallback = agentCallback self.connectedClients = set() self.onShutdown = Event() # () self.onConnectionEstablished = Event(['protocol']) self.onConnectionLost = Event(['protocol']) self.running = True self._alreadyShutdown = False def checkGreeting(self, greeting): return (greeting == b'Trosnoth18') def startListening(self, port=DEFAULT_GAME_PORT, interface=''): try: self.port = reactor.listenTCP(port, self, interface=interface) except CannotListenError: log.warning('WARNING: Could not listen on port %s', port) self.port = reactor.listenTCP(0, self, interface=interface) def getTCPPort(self): return self.port.getHost().port def stopListening(self): self.port.stopListening() def gotBadString(self, protocol, data): log.warning('Server: Unrecognised network data: %r' % (data,)) log.warning(' : Did you invent a new network message and forget') log.warning(' : to add it to ' 'trosnoth.network.server.serverMsgs?') def connectionEstablished(self, protocol): ''' Called by the network manager when a new incoming connection is completed. ''' # Remember that this connection's ready for transmission. self.connectedClients.add(protocol) hub = LocalHub( self.game, noAuth=self.noAuth, agentCallback=self.agentCallback) hub.connectNode(protocol) # Send the setting information. protocol.gotServerCommand(InitClientMsg(self._getClientSettings())) self.onConnectionEstablished(protocol) def _getClientSettings(self): '''Returns a byte string representing the settings which must be sent to clients that connect to this server.''' result = self.game.world.dumpEverything() result['serverVersion'] = serverVersion return repr(result).encode('utf-8') def connectionLost(self, protocol, reason): if protocol in self.connectedClients: protocol.hub.disconnectNode() self.connectedClients.remove(protocol) self.onConnectionLost(protocol) def shutdown(self): if self._alreadyShutdown: return self._alreadyShutdown = True # Kill server self.running = False self.game.stop() self.onShutdown.execute()
class CheckBox(Element): def __init__(self, app, pos, text, font, colour, initValue=False, hotkey=None, style='circle', fillColour=None): super(CheckBox, self).__init__(app) self.pos = pos self.font = font if hasattr(pos, 'apply'): self.text = TextElement( app, ' ' + text, font, Location( AttachedPoint(ScaledSize(self._getBoxSize()[0] / 5, 2), self._getBoxRect, 'midright'), 'midleft'), colour) else: self.text = TextElement( app, ' ' + text, font, Location((pos[0] + self._getBoxSize()[0], pos[1] - self._getBoxSize()[0] / 10), 'topleft'), colour) self.value = initValue self.colour = colour if fillColour is None: self.fillColour = tuple((256 * 3 + i) / 4 for i in colour) else: self.fillColour = fillColour self.hotkey = hotkey self.style = style self.onValueChanged = Event() def _getRect(self): return self._getBoxRect().union(self.text._getRect()) def _getBorderWidth(self): return max(1, self._getBoxSize()[0] / 8) def _getBoxSize(self): return (int(self.font.getHeight(self.app) / 1.5), int(self.font.getHeight(self.app) / 1.5)) def _getBoxRect(self): if hasattr(self.pos, 'apply'): box = pygame.rect.Rect((0, 0), self._getBoxSize()) self.pos.apply(self.app, box) else: box = pygame.rect.Rect(self.pos, self._getBoxSize()) return box def draw(self, surface): box = self._getBoxRect() if self.value: if self.style == 'fill': pygame.draw.rect(surface, self.fillColour, box, 0) elif self.style == 'cross': pygame.draw.line(surface, self.fillColour, box.topright, box.bottomleft, self._getBorderWidth()) pygame.draw.line(surface, self.fillColour, box.topleft, box.bottomright, self._getBorderWidth()) elif self.style == 'circle': pygame.draw.circle(surface, self.fillColour, box.center, box.width / 2 - 2) pygame.draw.rect(surface, self.colour, box, self._getBorderWidth()) self.text.draw(surface) def setValue(self, val): if val != self.value: self.value = val self.onValueChanged.execute(self) def getValue(self): return self.value def processEvent(self, event): box = self._getBoxRect() # Active events. if (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and box.collidepoint(event.pos)) or (event.type == pygame.KEYDOWN and event.key == self.hotkey): self.setValue(not self.value) self.onValueChanged.execute(self) else: return event return None
class HoverButton(Element): '''A button which changes image when the mouse hovers over it. @param pos should be a trosnoth.ui.common.Location instance ''' def __init__(self, app, pos, stdImage, hvrImage, hotkey=None, onClick=None): super(HoverButton, self).__init__(app) self.stdImage = stdImage self.hvrImage = hvrImage self.pos = pos self.onClick = Event() self.hotkey = hotkey self.mouseOver = False if onClick is not None: self.onClick.addListener(onClick) def _getSurfaceToBlit(self): img = self._getImageToUse() if hasattr(img, 'getImage'): return img.getImage(self.app) else: return img def _getImageToUse(self): if self.mouseOver: return self.hvrImage else: return self.stdImage def _getSize(self): img = self._getImageToUse() if hasattr(img, 'getSize'): return img.getSize(self.app) else: # pygame.surface.Surface return img.get_size() def _getPt(self): if hasattr(self.pos, 'apply'): rect = pygame.Rect((0, 0), self._getSize()) self.pos.apply(self.app, rect) return rect.topleft else: return self.pos def _getRect(self): if hasattr(self.pos, 'apply'): rect = pygame.Rect((0, 0), self._getSize()) self.pos.apply(self.app, rect) return rect return pygame.Rect(self.pos, self._getSize()) def processEvent(self, event): rect = self._getRect() # Passive events. if event.type == pygame.MOUSEMOTION: self.mouseOver = rect.collidepoint(event.pos) # Active events. if (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and rect.collidepoint(event.pos)): self.onClick.execute(self) elif event.type == pygame.KEYDOWN and event.key == self.hotkey: self.onClick.execute(self) else: return event return None def draw(self, screen): # Draw the button. screen.blit(self._getSurfaceToBlit(), self._getPt())
class ScrollingText(Element): def __init__(self, app, area, text, colour, fonts, bgColour=None, textAlign='left', loop=False, startOff=False, antiAlias=False): super(ScrollingText,self).__init__(app) self.area = area self.border = False self.shadowColour = None self.loop = loop self.startOff = startOff self.bgColour = bgColour self.speed = 50 self.offset = 0 self.colour = colour self.fonts = fonts self.text = text self.textAlign = textAlign self.antiAlias = antiAlias self.dirty = False # A test to see whether the size has changed self.lastSize = app.screenManager.size self._setImage() self.autoScroll = False # Create up and down buttons for if there's autoscroll. if 'buttons' in fonts: font = fonts['buttons'] else: font = app.fonts.scrollingButtonsFont pos = Location(AttachedPoint((0, 0), self._getRect, 'topright'), 'topleft') self.upBtn = TextButton(app, pos, ' up', font, colour, (255,255,255)) pos = Location(AttachedPoint((0,0), self._getRect, 'bottomright'), 'bottomleft') self.dnBtn = TextButton(app, pos, ' down', font, colour, (255,255,255)) self.onFinishedScrolling = Event() def setColour(self, colour): self.colour = colour self.dirty = True pos = Location(AttachedPoint((0,0), self._getRect, 'topright'), 'topleft') if 'buttons' in self.fonts: font = self.fonts['buttons'] else: font = self.app.fonts.scrollingButtonsFont self.upBtn = TextButton(self.app, pos, ' up', font, colour, (255,255,255)) self.dnBtn = TextButton(self.app, pos, ' down', font, colour, (255,255,255)) def setAutoscroll(self, hasAutoscroll): self.autoScroll = hasAutoscroll def setSpeed(self, speed): self.speed = speed def setBorder(self, hasBorder): self.border = hasBorder def setShadowColour(self, shadowColour): self.shadowColour = shadowColour self.dirty = True def _setImage(self): mainImage = self._setImageDetails(self.text, self.colour, self.fonts, self.textAlign) if self.shadowColour is None: self.image = mainImage else: shadowImage = self._setImageDetails(self.text, self.shadowColour, self.fonts, self.textAlign) shadowOffset = (1, 1) size = mainImage.get_size() self.image = pygame.Surface(tuple([size[i] + shadowOffset[i] for i in (0,1)])) if self.bgColour: self.image.fill(self.bgColour) else: key = uniqueColour((self.colour, self.shadowColour)) self.image.fill(key) self.image.set_colorkey(key) self.image.blit(shadowImage, shadowOffset) self.image.blit(mainImage, (0,0)) self.dirty = False def _setImageDetails(self, text, colour, fonts, align): margin = 50 lines = text.split('\n') rendered = [] heading1Font = fonts['h1'] heading2Font = fonts['h2'] bodyFont = fonts['body'] x = 0 width = self._getSize()[0] - 2 * margin while x < len(lines): line = lines[x] if line.startswith("<h1>") and line.endswith("</h1>"): style = "h1" line = line[4:len(line)-5] font = heading1Font elif line.startswith("<h2>") and line.endswith("</h2>"): style = "h2" line = line[4:len(line)-5] font = heading2Font else: style = "body" font = bodyFont newLines = wrapWords(self.app, line, font, width) line = newLines[0] del newLines[0] # Insert the new lines into the list of lines newLines.reverse() for l in newLines: if style == "h1": l = "<h1>" + l + "</h1>" elif style == "h2": l = "<h2>" + l + "</h2>" lines.insert(x + 1, l) if self.bgColour: img = font.render(self.app, line, self.antiAlias, colour, self.bgColour) else: img = font.render(self.app, line, self.antiAlias, colour) rendered.append(img) x += 1 height = 0 width = self._getSize()[0] for r in rendered: height += r.get_height() newImage = pygame.Surface((width, height)) if self.bgColour: newImage.fill(self.bgColour) else: key = uniqueColour((colour),) newImage.fill(key) newImage.set_colorkey(key) yPos = 0 for r in rendered: if align == 'left': xPos = margin elif align == 'right': xPos = width - r.get_width() - margin elif align == 'middle': xPos = (width - r.get_width()) / 2 else: raise ValueError("Not a valid alignment argument") newImage.blit(r, (xPos, yPos)) yPos += r.get_height() if self.startOff: if self.loop: size = (width, height + self._getSize()[1]) else: size = (width, height + 2 * self._getSize()[1]) else: size = (width, height) image = pygame.Surface(size) if not self.bgColour: image.fill(key) image.set_colorkey(key) if self.startOff: image.blit(newImage, (0, self._getSize()[1])) else: image.blit(newImage, (0, 0)) self.canScroll = image.get_height() > self._getSize()[1] self.reachedEnd = (not self.loop and self.offset == 0) or (self.loop and not self.canScroll) return image def returnToTop(self): self.offset = 0 def _getRect(self): return self.area.getRect(self.app) def _getSize(self): return self._getRect().size def _getPt(self): return self._getRect().topleft def draw(self, screen): if self.dirty or self.app.screenManager.size != self.lastSize: self.lastSize = self.app.screenManager.size self._setImage() if not self.canScroll: # Our image won't cover the whole of the scrollingText height = self._getSize()[1] - self.image.get_height() rect = pygame.Rect(self._getPt()[0], self._getPt()[1] + self.image.get_height(), self._getSize()[0], height) if self.bgColour: screen.fill(self.bgColour, rect) # Find the segment which will be drawn on screen rect = pygame.Rect((0, self.offset), self._getSize()) screen.blit(self.image, self._getPt(), rect) if (self.loop and self.canScroll and self.offset + self._getSize()[1] > self.image.get_height()): # It's doing the looping rect = pygame.Rect((0, self.offset - self.image.get_height()), self._getSize()) screen.blit(self.image, self._getPt(), rect) if self.border: rect.topleft = self._getPt() pygame.draw.rect(screen, (0,0,0), rect, 4) if not self.autoScroll: self.upBtn.draw(screen) self.dnBtn.draw(screen) def processEvent(self, event): if self.autoScroll: # We don't consume any events. return event else: event = self.upBtn.processEvent(event) if not event: return event = self.dnBtn.processEvent(event) if not event: return return event def scroll(self, offset): if not self.canScroll: return if self.reachedEnd: if ((offset > 0 and self.offset > 0) or (offset < 0 and self.offset == 0)): # They have already reached the end, and are trying to # go further that way - do nothing. return self.reachedEnd = False newOffset = offset + self.offset if not self.loop: if newOffset + self._getSize()[1] > self.image.get_height(): # They've hit the bottom newOffset = self.image.get_height() - self._getSize()[1] self.onFinishedScrolling.execute() self.reachedEnd = True elif newOffset <= 0: # Hit the top newOffset = 0 self.onFinishedScrolling.execute() self.reachedEnd = True self.offset = newOffset # For looping: make sure it loops around if self.loop: self.offset %= self.image.get_height() def _getSpeed(self): if hasattr(self.speed, 'getSpeed'): speed = self.speed.getSpeed(self.app) else: speed = self.speed return speed def tick(self, deltaT): if self.autoScroll: self.scroll(self._getSpeed() * deltaT) else: if self.upBtn.mouseOver: self.scroll(-self._getSpeed() * deltaT) elif self.dnBtn.mouseOver: self.scroll(self._getSpeed() * deltaT) self.upBtn.tick(deltaT) self.dnBtn.tick(deltaT)
class SavedGameTab(Tab): def __init__(self, app, tabContainer, onCancel=None, onReplay=None): super(SavedGameTab, self).__init__(app, 'Saved Games') self.app = app self.tabContainer = tabContainer self.onCancel = Event(listener=onCancel) self.onReplay = Event(listener=onReplay) font = self.app.screenManager.fonts.ampleMenuFont smallFont = self.app.screenManager.fonts.menuFont colours = app.theme.colours # Static text self.staticText = [ TextElement(self.app, 'server details:', font, ScaledLocation(960, 200, 'topright'), colours.headingColour), TextElement(self.app, 'date and time:', font, ScaledLocation(960, 370, 'topright'), colours.headingColour), TextElement(self.app, 'replay:', font, ScaledLocation(620, 550, 'topleft'), colours.headingColour), TextElement(self.app, 'stats:', font, ScaledLocation(620, 605, 'topleft'), colours.headingColour) ] # Dynamic text self.listHeaderText = TextElement(self.app, 'available game files:', font, ScaledLocation(65, 200), colours.headingColour) self.noFiles1Text = TextElement(self.app, '', font, ScaledLocation(65, 260), colours.noGamesColour) self.noFiles2Text = TextElement(self.app, '', font, ScaledLocation(65, 310), colours.noGamesColour) self.serverNameText = TextElement(self.app, '', smallFont, ScaledLocation(960, 255, 'topright'), colours.startButton) self.serverDetailsText = TextElement( self.app, '', smallFont, ScaledLocation(960, 295, 'topright'), colours.startButton) self.dateText = TextElement(self.app, '', smallFont, ScaledLocation(960, 425, 'topright'), colours.startButton) self.lengthText = TextElement(self.app, '', smallFont, ScaledLocation(960, 465, 'topright'), colours.startButton) self.noReplayText = TextElement(self.app, '', smallFont, ScaledLocation(960, 550, 'topright'), colours.noGamesColour) self.noStatsText = TextElement(self.app, '', smallFont, ScaledLocation(960, 605, 'topright'), colours.noGamesColour) self.dynamicText = [ self.listHeaderText, self.noFiles1Text, self.noFiles2Text, self.serverNameText, self.serverDetailsText, self.dateText, self.lengthText, self.noReplayText, self.noStatsText ] # Text buttons self.watchButton = TextButton(self.app, ScaledLocation(960, 550, 'topright'), '', font, colours.secondMenuColour, colours.white) self.watchButton.onClick.addListener(self.watchReplay) self.statsButton = TextButton(self.app, ScaledLocation(960, 605, 'topright'), '', font, colours.secondMenuColour, colours.white) self.statsButton.onClick.addListener(self.viewStats) self.refreshButton = TextButton(self.app, ScaledLocation(620, 665, 'topleft'), 'refresh', font, colours.secondMenuColour, colours.white) self.refreshButton.onClick.addListener(self.populateList) self.cancelButton = TextButton(self.app, ScaledLocation(960, 665, 'topright'), 'cancel', font, colours.secondMenuColour, colours.white) self.cancelButton.onClick.addListener(self._cancel) self.loadFileButton = TextButton( self.app, ScaledLocation(960, 190, 'bottomright'), 'load file...', font, colours.mainMenuColour, colours.mainMenuHighlight) self.loadFileButton.onClick.addListener(self.showOpenDialog) self.buttons = [ self.watchButton, self.statsButton, self.refreshButton, self.cancelButton, self.loadFileButton ] # Replay list self.gameList = ListBox(self.app, ScaledArea(65, 255, 500, 450), [], smallFont, colours.listboxButtons) self.gameList.onValueChanged.addListener(self.updateSidebar) # Combine the elements self.elementsFiles = (self.staticText + self.dynamicText + self.buttons + [self.gameList]) self.elementsNoFiles = self.dynamicText + self.buttons # Populate the list of replays self.populateList() def _cancel(self, sender): self.onCancel.execute() def showOpenDialog(self, sender): root = Tk() root.withdraw() tksupport.install(root) filename = askopenfilename( defaultextension='.trosrepl', filetypes=[ ('Trosnoth replay', '*.trosrepl'), ], initialdir=getPath(user, replayDir), title='Select replay', ) if filename: self.onReplay.execute(filename) def populateList(self, sender=None): # Clear out the sidebar for item in self.dynamicText: item.setText('') self.listHeaderText.setText('available game files:') self.gameList.index = -1 self.elements = self.elementsFiles[:] # Get a list of files with the name '*.tros' logDir = getPath(user, gameDir) makeDirs(logDir) fileList = [] for fname in os.listdir(logDir): if os.path.splitext(fname)[1] == gameExt: fileList.append(fname) # Assume all files are valid for now validFiles = fileList[:] self.gameInfo = {} oldFound = False for fname in fileList: try: game = RecordedGame(os.path.join(logDir, fname)) except RecordedGameException: validFiles.remove(fname) continue except: log.warning('invalid file: %s', fname) continue else: if game.recordedGameVersion != recordedGameVersion: validFiles.remove(fname) oldFound = True self.gameInfo[os.path.splitext(fname)[0]] = game # Sort the games with most recent first. items = [(v.unixTimestamp, n) for n, v in self.gameInfo.iteritems()] items.sort(reverse=True) items = [n for v, n in items] self.gameList.setItems(items) if len(self.gameInfo) == 0: self.elements = self.elementsNoFiles[:] self.listHeaderText.setText('0 available game files:') if oldFound: self.noFiles1Text.setText('Some games were found from') self.noFiles2Text.setText('previous Trosnoth versions') else: self.noFiles1Text.setText('You have not yet run any') self.noFiles2Text.setText('games on this computer') else: self.gameList.setIndex(0) self.updateSidebar(0) if len(self.gameInfo) == 1: self.listHeaderText.setText('1 available game file:') else: self.listHeaderText.setText('{} available game files:'.format( len(self.gameInfo))) def updateSidebar(self, listID): # Update the details on the sidebar displayName = self.gameList.getItem(listID) # Server title self.serverNameText.setText(self.gameInfo[displayName].alias) # Date and time of match datePython = map(int, self.gameInfo[displayName].dateTime.split(',')) dateString = time.strftime('%a %d/%m/%y, %H:%M', datePython) self.dateText.setText(dateString) # Length of match dateUnix = time.mktime(datePython) if self.gameInfo[displayName].wasFinished(): lastUnix = self.gameInfo[displayName].gameFinishedTimestamp lengthSeconds = int(lastUnix - dateUnix) lengthMinutes, lengthSeconds = divmod(lengthSeconds, 60) secPlural = ('s', '')[lengthSeconds == 1] minPlural = ('s', '')[lengthMinutes == 1] if lengthMinutes == 0: lengthString = '{} second{}'.format(lengthSeconds, secPlural) else: lengthString = '{} min{}, {} sec{}'.format( lengthMinutes, minPlural, lengthSeconds, secPlural) self.lengthText.setText(lengthString) else: self.lengthText.setText('') # Enable the replay button if (self.gameInfo[displayName].replayFilename is not None and os.path.exists(self.gameInfo[displayName].replayFilename)): self.watchButton.setText('watch') self.noReplayText.setText('') else: self.watchButton.setText('') self.noReplayText.setText('unavailable') # Enabled the stats button if (self.gameInfo[displayName].statsFilename is not None and os.path.exists(self.gameInfo[displayName].statsFilename)): self.statsButton.setText('view') self.noStatsText.setText('') else: self.statsButton.setText('') self.noStatsText.setText('unavailable') def watchReplay(self, sender): '''Watch replay button was clicked.''' # Try to create a replay server. self.onReplay.execute( self.gameInfo[self.gameList.getItem()].replayFilename) def viewStats(self, sender=None): '''View stats button was clicked.''' game = self.gameInfo[self.gameList.getItem()] self.htmlPath = game.generateHtmlFile() browser.openPage(self.app, self.htmlPath) def draw(self, surface): super(SavedGameTab, self).draw(surface) rect = self.tabContainer._getTabRect() verLineX = rect.left + (rect.width * 0.6) horLineY = rect.top + (rect.height * 0.68) colour = self.app.theme.colours.replayTabBorder pygame.draw.line(surface, colour, (verLineX, rect.top), (verLineX, rect.bottom), self.tabContainer._getBorderWidth()) pygame.draw.line(surface, colour, (verLineX, horLineY), (rect.right, horLineY), self.tabContainer._getBorderWidth())
class DisplaySettingsTab(Tab, framework.TabFriendlyCompoundElement): def __init__(self, app, onClose=None): super(DisplaySettingsTab, self).__init__(app, 'Display') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) font = self.app.screenManager.fonts.bigMenuFont smallNoteFont = self.app.screenManager.fonts.smallNoteFont colour = self.app.theme.colours.headingColour def mkText(text, x, y, textFont=font, anchor='topright'): return TextElement(self.app, text, textFont, ScaledLocation(x, y, anchor), colour) self.text = [ mkText('X', 640, 280), mkText('Screen resolution', 430, 280), mkText('Fullscreen mode', 430, 360), mkText('Graphics detail', 430, 440), mkText('low', 460, 475, textFont=smallNoteFont, anchor='midtop'), mkText('high', 845, 475, textFont=smallNoteFont, anchor='midtop'), mkText('Show timings', 430, 525), ] self.invalidInputText = TextElement(self.app, '', font, ScaledLocation(512, 230, 'midtop'), (192, 0, 0)) self.widthInput = prompt.InputBox(self.app, ScaledArea(460, 265, 150, 60), initValue=str( self.app.screenManager.size[0]), font=font, maxLength=4, validator=prompt.intValidator) self.widthInput.onEnter.addListener(lambda sender: self.saveSettings()) self.widthInput.onClick.addListener(self.setFocus) self.widthInput.onTab.addListener(self.tabNext) self.heightInput = prompt.InputBox(self.app, ScaledArea(652, 265, 150, 60), initValue=str( self.app.screenManager.size[1]), font=font, maxLength=4, validator=prompt.intValidator) self.heightInput.onEnter.addListener( lambda sender: self.saveSettings()) self.heightInput.onClick.addListener(self.setFocus) self.heightInput.onTab.addListener(self.tabNext) self.tabOrder = [self.widthInput, self.heightInput] self.fullscreenBox = CheckBox( self.app, ScaledLocation(460, 365), text='', font=font, colour=(192, 192, 192), initValue=self.app.screenManager.isFullScreen(), ) self.fullscreenBox.onValueChanged.addListener(self.fullscreenChanged) displaySettings = app.displaySettings self.detailSlider = Slider( self.app, ScaledArea(460, 430, 390, 40), bounds=(0, len(displaySettings.DETAIL_LEVELS) - 1), snap=True) self.detailSlider.setVal( displaySettings.DETAIL_LEVELS.index(displaySettings.detailLevel)) self.detailSlider.onValueChanged.addListener(self.detailChanged) self.showTimingsBox = CheckBox( self.app, ScaledLocation(460, 530), text='', font=font, colour=(192, 192, 192), initValue=displaySettings.showTimings, ) self.input = [ self.widthInput, self.heightInput, self.widthInput, self.fullscreenBox, self.detailSlider, self.showTimingsBox ] self.elements = self.text + self.input + [ self.invalidInputText, button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.cancelMenu, (100, -75), 'midbottom', secondColour=app.theme.colours.white), ] self.setFocus(self.widthInput) def detailChanged(self, newLevel): pass def cancelMenu(self): self.fullscreenBox.setValue(self.app.screenManager.isFullScreen()) self.showTimingsBox.setValue(self.app.displaySettings.showTimings) self.heightInput.setValue(str(self.app.screenManager.size[1])) self.widthInput.setValue(str(self.app.screenManager.size[0])) self.onClose.execute() def saveSettings(self): displaySettings = self.app.displaySettings height = self.getInt(self.heightInput.value) width = self.getInt(self.widthInput.value) fullScreen = self.fullscreenBox.value detailLevel = displaySettings.DETAIL_LEVELS[self.detailSlider.getVal()] showTimings = self.showTimingsBox.value # The resolutionList is used when fullScreen is true. resolutionList = pygame.display.list_modes() resolutionList.sort() minResolution = resolutionList[0] maxResolution = resolutionList[-1] if not fullScreen: minResolution = (320, 240) # These values are used when fullScreen is false. widthRange = (minResolution[0], maxResolution[0]) heightRange = (minResolution[1], maxResolution[1]) if not widthRange[0] <= width <= widthRange[1]: self.incorrectInput('Screen width must be between %d and %d' % (widthRange[0], widthRange[1])) width = None return if not heightRange[0] <= height <= heightRange[1]: self.incorrectInput('Screen height must be between %d and %d' % (heightRange[0], heightRange[1])) height = None return if fullScreen: selectedResolution = (width, height) if selectedResolution not in resolutionList: self.incorrectInput('Selected resolution is not valid for ' 'this display') height = width = None return self.incorrectInput('') # Save these values. displaySettings.fullScreen = fullScreen displaySettings.detailLevel = detailLevel displaySettings.showTimings = showTimings if fullScreen: displaySettings.fsSize = (width, height) else: displaySettings.size = (width, height) # Write to file and apply. displaySettings.save() displaySettings.apply() self.onClose.execute() def getInt(self, value): if value == '': return 0 return int(value) def incorrectInput(self, string): self.invalidInputText.setText(string) self.invalidInputText.setFont(self.app.screenManager.fonts.bigMenuFont) def fullscreenChanged(self, element): # If the resolution boxes haven't been touched, swap their values to # the appropriate resolution for the new mode. height = self.getInt(self.heightInput.value) width = self.getInt(self.widthInput.value) fullScreen = self.fullscreenBox.value if fullScreen: # Going to full screen mode. if (width, height) != self.app.displaySettings.size: return width, height = self.app.displaySettings.fsSize else: # Going from full screen mode. if (width, height) != self.app.displaySettings.fsSize: return width, height = self.app.displaySettings.size self.heightInput.setValue(str(height)) self.widthInput.setValue(str(width))
class KeymapTab(Tab): def __init__(self, app, onClose=None): super(KeymapTab, self).__init__(app, 'Controls') self.font = app.screenManager.fonts.bigMenuFont self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) # Break things up into categories movement = ['jump', 'down', 'left', 'right'] menus = ['menu', 'more actions'] actions = [ 'respawn', 'select upgrade', 'activate upgrade', 'change nickname', 'ready' ] misc = ['chat', 'follow'] upgrades = [ upgradeClass.action for upgradeClass in sorted( allUpgrades, key=lambda upgradeClass: upgradeClass.order) ] upgrades.append('no upgrade') display = ['leaderboard', 'toggle interface', 'toggle terminal'] actionNames = { 'select upgrade': 'Select upgrade', 'activate upgrade': 'Activate upgrade', 'change nickname': 'Change nickname', 'chat': 'Chat', 'down': 'Drop down', 'follow': 'Auto pan (replay)', 'jump': 'Jump', 'leaderboard': 'Show leaderboard', 'left': 'Move left', 'menu': 'Main menu', 'more actions': 'Advanced', 'no upgrade': 'Deselect upgrade', 'ready': 'Toggle ready', 'respawn': 'Respawn', 'right': 'Move right', 'status bar': 'Status bar', 'timer': 'Show timer', 'toggle interface': 'Toggle HUD', 'toggle terminal': 'Toggle terminal', 'zone progress': 'Show zone bar', } actionNames.update((upgradeClass.action, upgradeClass.name) for upgradeClass in allUpgrades) # Organise the categories by column self.layout = [ [movement, menus], [actions, display], [upgrades, misc], ] self.errorInfo = TextElement(self.app, '', self.font, ScaledLocation(512, 580, 'center')) self.text = [self.errorInfo] self.inputLookup = {} xPos = 190 # Lay everything out automatically. keymapFont = self.app.screenManager.fonts.keymapFont keymapInputFont = self.app.screenManager.fonts.keymapInputFont for column in self.layout: # Each column yPos = 200 for category in column: # Each category for action in category: # Each action # Draw action name (eg. Respawn) self.text.append( TextElement(self.app, actionNames[action], keymapFont, ScaledLocation(xPos, yPos + 6, 'topright'), self.app.theme.colours.headingColour)) # Create input box box = prompt.KeycodeBox(self.app, ScaledArea(xPos + 10, yPos, 100, 30), font=keymapInputFont) box.onClick.addListener(self.setFocus) box.onChange.addListener(self.inputChanged) box.__action = action self.inputLookup[action] = box yPos += 35 # Between items yPos += 35 # Between categories xPos += 320 # Between columns self.elements = self.text + self.inputLookup.values() + [ button(app, 'restore default controls', self.restoreDefaults, (0, -125), 'midbottom', secondColour=app.theme.colours.white), button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.cancel, (100, -75), 'midbottom', secondColour=app.theme.colours.white), ] self.populateInputs() def inputChanged(self, box): # Remove the old key. try: oldKey = self.keyMapping.getkey(box.__action) except KeyError: pass else: del self.keyMapping[oldKey] # Set the new key. self.keyMapping[box.value] = box.__action # Refresh the display. self.refreshInputs() def populateInputs(self): # Set up the keyboard mapping. self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) try: # Try to load keyboard mappings from the user's personal settings. config = open(getPath(user, 'keymap'), 'rU').read() self.keyMapping.load(config) except IOError: pass # Refresh the display. self.refreshInputs() def refreshInputs(self): for column in self.layout: for category in column: for action in category: # Get the current key and put it in the box. try: key = self.keyMapping.getkey(action) except KeyError: key = None self.inputLookup[action].value = key # Make the box white self.inputLookup[action].backColour = (255, 255, 255) def restoreDefaults(self): self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) self.refreshInputs() self.incorrectInput( "Default controls restored: press 'save' to " "confirm", (0, 128, 0)) def clearBackgrounds(self): for action in self.inputLookup: self.inputLookup[action].backColour = (255, 255, 255) self.setFocus(None) def saveSettings(self): # Perform the save. open(getPath(user, 'keymap'), 'w').write(self.keyMapping.save()) emptyBoxes = [] for box in self.inputLookup.itervalues(): if box.value is None: emptyBoxes.append(box) if len(emptyBoxes) > 0: self.populateInputs() for box in emptyBoxes: box.backColour = self.app.theme.colours.invalidDataColour self.incorrectInput('Warning: some actions have no key', (192, 0, 0)) else: self.mainMenu() def incorrectInput(self, string, colour): self.errorInfo.setColour(colour) self.errorInfo.setText(string) self.errorInfo.setFont(self.font) def cancel(self): self.populateInputs() self.mainMenu() def mainMenu(self): self.incorrectInput('', (0, 0, 0)) self.clearBackgrounds() self.onClose.execute()
class TabContainer(CompoundElement): def __init__(self, app, area, font, borderColour=(0, 0, 0)): super(TabContainer, self).__init__(app) self.onTabSelected = Event() # area is that of the entire space including tab headers self.area = area self.font = font self._borderWidth = ScaledScalar(3) self.borderColour = borderColour nextTab = lambda: self._tabSelected( (self.selectedIndex + 1) % len(self.tabs)) lastTab = lambda: self._tabSelected( (self.selectedIndex - 1) % len(self.tabs)) self.nextTabKey = Hotkey(app, pygame.K_TAB, pygame.KMOD_CTRL) self.nextTabKey.onTriggered.addListener(nextTab) self.lastTabKey = Hotkey(app, pygame.K_TAB, pygame.KMOD_CTRL | pygame.KMOD_SHIFT) self.lastTabKey.onTriggered.addListener(lastTab) self.tabs = [] self.tabHeaders = TabHeaderRow(app, self) self.selectedIndex = -1 self.tabHeaders._update() self.elements = [self.tabHeaders, self.lastTabKey, self.nextTabKey] self.tabHeaders._update() def _getBorderWidth(self): if hasattr(self._borderWidth, 'getVal'): return int(self._borderWidth.getVal(self.app)) else: return self._borderWidth def _getHeaderHeight(self): linesize = self.font.getLineSize(self.app) return int(linesize * 1.1) ## # Full rect (including headers) def _getRect(self): return self.area.getRect(self.app) def _getSize(self): return self._getRect().size def _getPt(self): return self._getRect().topleft ## # For the internal rect (not including headers) def _getTabPos(self): pos = self._getPt() return (pos[0], pos[1] + self._getHeaderHeight()) def _getTabSize(self): size = self._getSize() return (size[0], size[1] - self._getHeaderHeight()) def _getTabRect(self): return pygame.Rect(self._getTabPos(), self._getTabSize()) def _getTabInternalRect(self): ''' Returns the pygame.Rect of area which the tab should draw on. (When the tab draws it is relative to the top-left of the tab container.) ''' return pygame.Rect((0, self._getHeaderHeight()), self._getTabSize()) def selectTab(self, index): self._tabSelected(index) def _tabSelected(self, tab): if self.selectedIndex != -1: if not self.tabs[self.selectedIndex].beforeDeactivating(): return self.tabs[self.selectedIndex].deactivated() if isinstance(tab, Tab): assert tab in self.tabs, "This tab must be in our list" self.selectedIndex = self.tabs.index(tab) else: # By index self.selectedIndex = tab # Notify those who care: self.tabHeaders.tabSelected(self.selectedIndex) self.onTabSelected.execute(self.selectedIndex) self.tabs[self.selectedIndex].activated() def addTab(self, tab): self.tabs.append(tab) tab.container = self newHeader = TabHeader(self.app, tab, self, self.font) self.tabHeaders.newTabHeader(newHeader) self.tabHeaders._update() # If it's the only tab, select it if self.selectedIndex == -1: self._tabSelected(tab) def renameTab(self, newName, tab): tab.caption = newName self.tabHeaders._update() def removeTabAt(self, index): tab = self.tabs.pop(index) tab.container = None self.tabHeaders.delTabHeader(index) assert len(self.tabs) == len(self.tabHeaders) assert len(self.tabs) != 0 if index == self.selectedIndex: self.selectedIndex = -1 self._tabSelected(min(index, len(self.tabs) - 1)) self.tabHeaders._update() def removeTab(self, tab): tab.container = None index = self.tabs.index(tab) self.removeTabAt(index) def draw(self, screen): super(TabContainer, self).draw(screen) assert len(self.tabs) > 0, ('Need to have at least one tab before ' 'drawing a tabContainer') self.tabs[self.selectedIndex].draw(screen) # Draw a border pygame.draw.rect(screen, self.borderColour, self._getTabRect(), self._getBorderWidth()) def tick(self, deltaT): super(TabContainer, self).tick(deltaT) for i in range(0, len(self.tabs)): # tick all the tabs self.tabs[i].tick(deltaT) def processEvent(self, event): event = super(TabContainer, self).processEvent(event) if event: event = self.tabs[self.selectedIndex].processEvent(event) return event
class MoveableBox(CompoundElement): defaultBorderColour = (0, 0, 255) defaultTitleColour = (255, 255, 255) defaultBackgroundColour = (255, 255, 255) def __init__(self, app, size, caption, subCaption=None): CompoundElement.__init__(self, app) self.showing = False self.size = size self._edge = MoveableBoxEdge(self.app, self, caption, subCaption) self.setColours(self.defaultBorderColour, self.defaultTitleColour, self.defaultBackgroundColour) self.onClose = Event() def _giveFocus(self): self.app.screenManager.dialogFocus(self) def _getSize(self): if hasattr(self.size, 'getSize'): return self.size.getSize(self.app) else: return self.size def _setPos(self, pos): self._edge._setPos(pos) def setColours(self, borderColour=None, titleColour=None, backgroundColour=None): if borderColour: self._edge.borderColour = borderColour if titleColour: self._edge.titleColour = titleColour if backgroundColour: self.backgroundColour = backgroundColour def setCaption(self, caption=None, subCaption=None): if caption is not None: self._edge.caption = caption if subCaption is not None: if subCaption == False: self._edge.subCaption = None else: self._edge.subCaption = subCaption def _getPt(self): return self._edge._getInsideArea() def _getInsideRect(self): return pygame.Rect(self._edge._getInsideTopLeftPt(), self._getSize()) def draw(self, screen): self._edge.draw(screen) subSurface = pygame.Surface(self._getSize()) subSurface.fill(self.backgroundColour) CompoundElement.draw(self, subSurface) screen.blit(subSurface, self._edge._getInsideTopLeftPt()) def processEvent(self, event): event = self._edge.processEvent(event) if event: if hasattr(event, 'pos'): event2 = translateEvent(event, self._edge._getInsideTopLeftPt()) isPos = True else: event2 = event isPos = False event2 = CompoundElement.processEvent(self, event2) if event2 == None: return None elif isPos and self._edge._getEdgeRect().collidepoint(event.pos): return None else: return event def show(self): try: showDialog = self.app.screenManager.showDialog except AttributeError: raise MultiWindowException( 'Dialog Boxes cannot be used unless the Application is a ' 'MultiWindowApplication') showDialog(self) self.showing = True def close(self): try: closeDialog = self.app.screenManager.closeDialog except AttributeError: raise MultiWindowException( 'Dialog Boxes cannot be used unless the ' 'Application is a MultiWindowApplication') closeDialog(self) self.showing = False self.onClose.execute()
class PlayAuthScreen(framework.CompoundElement): passwordGUIFactory = PasswordGUI def __init__(self, app, onSucceed=None, onFail=None): super(PlayAuthScreen, self).__init__(app) self.onSucceed = Event(listener=onSucceed) self.onFail = Event(listener=onFail) self.lobby = None self.badServers = set() if app.displaySettings.alphaOverlays: alpha = 192 else: alpha = None bg = SolidRect(app, app.theme.colours.playMenu, alpha, Region(centre=Canvas(512, 384), size=Canvas(924, 500))) colour = app.theme.colours.mainMenuColour font = app.screenManager.fonts.consoleFont self.logBox = LogBox( app, Region(size=Canvas(900, 425), midtop=Canvas(512, 146)), colour, font) font = app.screenManager.fonts.bigMenuFont cancel = TextButton(app, Location(Canvas(512, 624), 'midbottom'), 'Cancel', font, app.theme.colours.secondMenuColour, app.theme.colours.white, onClick=self.cancel) self.cancelled = False self.elements = [bg, self.logBox, cancel] self._passwordGetter = None self._gameSelectionBox = None @property def passwordGetter(self): if self._passwordGetter is None: self._passwordGetter = self.passwordGUIFactory(self.app) return self._passwordGetter @defer.inlineCallbacks def begin(self, servers, canHost=True): self.cancelled = False self.passwordGetter.setCancelled(False) self.badServers = set() if self.lobby is None: self.lobby = Lobby(self.app) # Removes the third item (http) from the tuple since we don't care about # it. servers = [(server[:2] if isinstance(server, tuple) else server) for server in servers] for server in servers: if self.cancelled: break if server == JOIN_LOCAL_GAME: if self.app.server is not None: self.onSucceed.execute() self.app.interface.connectToLocalServer() return elif isinstance(server, tuple): if server in self.badServers: continue self.logBox.log('Requesting games from %s:%d...' % server) connected = yield self.attemptServerConnect( self.lobby.getGames, server) if connected: return elif server == JOIN_LAN_GAME: self.logBox.log('Asking local network for other games...') games = yield self.lobby.get_adhoc_lan_games() for game in games: joinSuccessful = yield self.attemptJoinGame(game) if joinSuccessful: return elif server == JOIN_AUTH_LAN_GAME: self.logBox.log('Searching for servers on local network...') server = yield get_multicast_server() if server: self.logBox.log( 'Server found. Requesting games from %s:%d...' % server) connected = yield self.attemptServerConnect( self.lobby.getGames, server) if connected: return if canHost: if not self.cancelled: result = yield HostGameQuery(self.app).run() if not result: self.onFail.execute() return self.app.startListenServer(2, 1) self.onSucceed.execute() self.app.interface.connectToLocalServer() else: if not self.cancelled: box = OkBox(self.app, ScaledSize(450, 150), 'Trosnoth', 'Connection unsuccessful.') box.onClose.addListener(self.onFail.execute) box.show() @defer.inlineCallbacks def attemptServerConnect(self, getGamesList, server): ''' Attempts to connect to a game on the server, returned by getGamesList(server). ''' try: games = yield getGamesList(server) except ConnectError: self.logBox.log('Unable to connect.') self.badServers.add(server) except amp.UnknownRemoteError: self.logBox.log('Error on remote server.') except amp.RemoteAmpError as e: self.logBox.log('Error on remote server.') log.error('Remote error getting games list: %s', e) except IOError as e: self.logBox.log('Error connecting to remote server.') log.error('Error connecting to auth server: %s', e) self.badServers.add(server) except Exception: self.logBox.log('Error retrieving games list.') log.exception('Error retrieving games list') else: if len(games) == 0: self.logBox.log('No running games.') elif len(games) == 1: joinSuccessful = yield self.attemptJoinGame(games[0]) defer.returnValue(joinSuccessful) else: game = yield self.selectGame(server, games) if game: joinSuccessful = yield self.attemptJoinGame(game) defer.returnValue(joinSuccessful) self.logBox.log('No game selected.') defer.returnValue(False) def selectGame(self, server, games): host, port = server if self._gameSelectionBox is None: self._gameSelectionBox = GameSelectionBox(self.app) return self._gameSelectionBox.selectGame(host, games) @defer.inlineCallbacks def attemptJoinGame(self, game): ''' Attempts to join the given game. ''' try: self.logBox.log('Found game: joining.') result = yield game.join(self.passwordGetter) except authcommands.GameDoesNotExist: pass except AuthenticationCancelled: pass except authcommands.NotAuthenticated: self.logBox.log('Authentication failure.') except amp.UnknownRemoteError: self.logBox.log('Error on remote server.') except amp.RemoteAmpError as e: self.logBox.log('Error on remote server.') log.error('Remote error joining game: %s', e) except ConnectionFailed: self.logBox.log('Could not connect.') except ConnectionRefusedError: self.logBox.log('Connection refused.') except IOError as e: self.logBox.log('Error connecting to remote server.') log.error('Error attempting to join game: %s', e) except Exception: self.logBox.log('Error connecting to remote server.') log.exception('Error joining game.') else: self._joined(result) defer.returnValue(True) return defer.returnValue(False) def _joined(self, result): self.onSucceed.execute() self.app.interface.connectedToGame(*result) def cancel(self, element): self.cancelled = True self.passwordGetter.setCancelled(True) self.onFail.execute() def processEvent(self, event): if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: self.onFail.execute() else: return super(PlayAuthScreen, self).processEvent(event)
class ServerSideBotProcessManager: def __init__(self, game, logPrefix): self.game = game self.logPrefix = logPrefix self.process = None self.starting = None self.token = None self.waitForAMPConnection = None self.waitForProcessStart = None self.amp = None self.server = None self.joiningAgents = [] def stop(self): if self.server: self.server.stopListening() self.server = None async def startBot(self, aiName, fromLevel=False, nick='', team=None): if self.server is None: self.server = TrosnothServerFactory( self.game, noAuth=True, agentCallback=self.agentConnected) self.server.startListening(port=0, interface=LOCALHOST) await self.ensureProcessHasStarted() authTag = len(self.joiningAgents) agentHasJoined = defer.Deferred() self.joiningAgents.append((agentHasJoined, fromLevel)) await self.amp.callRemote( StartBot, name=aiName, fromLevel=fromLevel, nick=nick, teamId=team.id if team else NEUTRAL_TEAM_ID, authTag=authTag, ) agent = await agentHasJoined return agent def agentConnected(self, agent, authTag): d, fromLevel = self.joiningAgents[authTag] self.joiningAgents[authTag] = None while self.joiningAgents and self.joiningAgents[-1] is None: self.joiningAgents.pop() agent.allowBotPlayer(fromLevel) d.callback(agent) async def ensureProcessHasStarted(self): if self.starting: await self.starting.waitOrRaise() return if self.process: return success = False self.starting = Event(['result']) try: await self._startProcess() success = True except Exception as e: self.starting.execute(e) raise else: self.starting.execute(None) finally: self.starting = None if not success: self.process = None self.amp = None async def _startProcess(self): assert self.process is None self.process = BotProcessProtocol(self) self.token = uuid.uuid4().hex.encode('ascii') self.waitForAMPConnection = defer.Deferred() ampFactory = Factory.forProtocol(ArenaSideAMPProtocol) ampFactory.manager = self ampEndpoint = TCP4ServerEndpoint(reactor, 0) listeningPort = await ampEndpoint.listen(ampFactory) try: ampPort = listeningPort.getHost().port cmd = self.getCommand() + [str(ampPort)] if self.logPrefix: cmd += [self.logPrefix] log.info('Running command %r', cmd) self.waitForProcessStart = defer.Deferred() transport = reactor.spawnProcess( self.process, cmd[0], cmd, env=None, childFDs=None if os.name == 'nt' else {0: 'w', 1: 1, 2: 2}) try: await self.waitForProcessStart finally: self.waitForProcessStart = None self.process.transport.write(self.token + b'\n') try: self.amp = await self.waitForAMPConnection finally: self.waitForAMPConnection = None await self.amp.callRemote( ConnectToServer, port=self.server.port.getHost().port) finally: listeningPort.stopListening() def getCommand(self): if getattr(sys, 'frozen', False): # Bundled by PyInstaller path = os.path.dirname(sys.executable) ext = '.exe' if os.name == 'nt' else '' return [os.path.join(path, 'support' + ext), 'bots'] return [sys.executable, __file__] def ampConnectionLost(self): if self.process: log.warning('Lost AMP connection to bot subprocess') self.process.killProcess()
class PlayConnector(object): ''' Connects to the first available game based on the configured server settings. passwordGetFunction must be a function that satisfies the PasswordGetter.getPassword interface from trosnoth.network.lobby. hostGameQueryFunction must be a function that returns a deferred which prompts the user to decide whether to host a game and returns the user's decision. ''' def __init__(self, app, onSucceed=None, onFail=None, onLogLine=None, passwordGetFunction=None, hostGameQueryFunction=None): self.app = app self.onSucceed = Event(listener=onSucceed) self.onFail = Event(listener=onFail) self.onLogLine = Event(listener=onLogLine) self.passwordGetFunction = passwordGetFunction self.hostGameQueryFunction = hostGameQueryFunction self.lobby = None self.badServers = set() self.cancelled = False self.running = False def getPassword(self, host, errorText=''): if self.passwordGetFunction is not None: return self.passwordGetFunction(host, errorText) return None @defer.inlineCallbacks def begin(self, servers, canHost=True): ''' Attempts to connect to the given servers in turn, and if canHost is given will prompt the user to host a game if no other games are available. Calling this function will always trigger either onSucceed or onFail to be executed unless the user selects cancel. ''' if self.running: raise RuntimeError('Already running') self.running = True self.cancelled = False self.badServers = set() if self.lobby is None: self.lobby = Lobby(self.app) for server in servers: if self.cancelled: break if server == JOIN_LOCAL_GAME: localGame = self.app.hoster.getGameObject() if localGame is not None: self.onSucceed.execute() self.app.connector.openGameObject(localGame) self.running = False return elif isinstance(server, tuple): host, port = server[:2] if (host, port) in self.badServers: continue self.onLogLine('Requesting games from {}:{}...'.format( host, port)) connected = yield self.attemptServerConnect( self.lobby.getGames, (host, port)) if connected: self.running = False return elif server == JOIN_LAN_GAME: self.onLogLine('Asking local network for other games...') games = yield self.lobby.get_adhoc_lan_games() for game in games: joinSuccessful = yield self.attemptJoinGame(game) if joinSuccessful: self.running = False return if canHost: if not self.cancelled: result = yield self.hostGameQueryFunction() if not result: self.onFail.execute() self.running = False return self.app.hoster.startServer(halfMapWidth=2, mapHeight=1) self.onSucceed.execute() localGame = self.app.hoster.getGameObject() self.app.connector.openGameObject(localGame) else: if not self.cancelled: self.onFail.execute() self.running = False @defer.inlineCallbacks def attemptServerConnect(self, getGamesList, server): ''' Attempts to connect to a game on the server, returned by getGamesList(server). ''' try: games = yield getGamesList(server) except ConnectError: self.onLogLine('Unable to connect.') self.badServers.add(server) except amp.UnknownRemoteError: self.onLogLine('Error on remote server.') except amp.RemoteAmpError as e: self.onLogLine('Error on remote server.') log.error('Remote error getting games list: %s', e) except IOError as e: self.onLogLine('Error connecting to remote server.') log.error('Error connecting to auth server: %s', e) self.badServers.add(server) except Exception: self.onLogLine('Error retrieving games list.') log.exception('Error retrieving games list') else: if len(games) == 0: self.onLogLine('No running games.') # TODO: display a dialog for user to select arena for game in games: joinSuccessful = yield self.attemptJoinGame(game) if joinSuccessful: defer.returnValue(True) return defer.returnValue(False) @defer.inlineCallbacks def attemptJoinGame(self, game): ''' Attempts to join the given game. ''' try: self.onLogLine('Found game: joining.') result = yield game.join(self) except authcommands.GameDoesNotExist: pass except AuthenticationCancelled: pass except authcommands.NotAuthenticated: self.onLogLine('Authentication failure.') except amp.UnknownRemoteError: self.onLogLine('Error on remote server.') except amp.RemoteAmpError as e: self.onLogLine('Error on remote server.') log.error('Remote error joining game: %s', e) except ConnectionFailed: self.onLogLine('Could not connect.') except ConnectionRefusedError: self.onLogLine('Connection refused.') except IOError as e: self.onLogLine('Error connecting to remote server.') log.error('Error attempting to join game: %s', e) except Exception: self.onLogLine('Error connecting to remote server.') log.exception('Error joining game.') else: self._joined(result) defer.returnValue(True) return defer.returnValue(False) def _joined(self, result): self.onSucceed.execute() self.app.connector.connectedToGame(*result) def cancel(self): self.cancelled = True
class SoundSettingsTab(Tab, framework.CompoundElement): def __init__(self, app, onClose=None): super(SoundSettingsTab, self).__init__(app, 'Sounds') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) font = self.app.screenManager.fonts.bigMenuFont colours = app.theme.colours text = [ TextElement(self.app, 'Music Volume', font, ScaledLocation(400, 285, 'topright'), colours.headingColour), TextElement(self.app, 'Enable Music', font, ScaledLocation(400, 355, 'topright'), colours.headingColour), TextElement(self.app, 'Sound Volume', font, ScaledLocation(400, 425, 'topright'), colours.headingColour), TextElement(self.app, 'Enable Sound', font, ScaledLocation(400, 495, 'topright'), colours.headingColour), ] initVolume = app.soundSettings.musicVolume musicVolumeLabel = TextElement(self.app, '%d' % (initVolume, ), font, ScaledLocation(870, 280, 'topleft'), colours.headingColour) self.musicVolumeSlider = Slider(self.app, ScaledArea(450, 280, 400, 40)) onSlide = lambda volume: musicVolumeLabel.setText('%d' % volume) self.musicVolumeSlider.onSlide.addListener(onSlide) self.musicVolumeSlider.onValueChanged.addListener(onSlide) self.musicVolumeSlider.setVal(initVolume) self.musicBox = CheckBox(self.app, ScaledLocation(450, 360), text='', font=font, colour=(192, 192, 192), initValue=app.soundSettings.musicEnabled) initSndVolume = app.soundSettings.soundVolume soundVolumeLabel = TextElement(self.app, '%d' % (initSndVolume, ), font, ScaledLocation(870, 420, 'topleft'), colours.headingColour) self.soundVolumeSlider = Slider(self.app, ScaledArea(450, 420, 400, 40)) onSlide = lambda volume: soundVolumeLabel.setText('%d' % volume) self.soundVolumeSlider.onSlide.addListener(onSlide) self.soundVolumeSlider.onValueChanged.addListener(onSlide) self.soundVolumeSlider.setVal(initSndVolume) self.soundBox = CheckBox(self.app, ScaledLocation(450, 500), text='', font=font, colour=(192, 192, 192), initValue=app.soundSettings.soundEnabled) self.buttons = [ button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.onClose.execute, (100, -75), 'midbottom', secondColour=app.theme.colours.white) ] self.elements = text + [ musicVolumeLabel, self.musicVolumeSlider, self.musicBox, soundVolumeLabel, self.soundVolumeSlider, self.soundBox ] + self.buttons def saveSettings(self, sender=None): playMusic, volume, playSound, sndVolume = self.getValues() ss = self.app.soundSettings ss.musicEnabled = playMusic ss.musicVolume = volume ss.soundEnabled = playSound ss.soundVolume = sndVolume ss.save() ss.apply() self.onClose.execute() def getValues(self): playMusic = self.musicBox.value volume = self.musicVolumeSlider.getVal() playSound = self.soundBox.value sndVolume = self.soundVolumeSlider.getVal() return [playMusic, volume, playSound, sndVolume]
class TrosnothServerFactory(MsgServer): messages = serverMsgs def __init__(self, game, authTagManager=None, *args, **kwargs): self.game = game self.authTagManager = authTagManager self.connectedClients = set() self.onShutdown = Event() # () self.running = True self._alreadyShutdown = False def checkGreeting(self, greeting): return (greeting == 'Trosnoth18') def startListening(self, port=6789, interface=''): try: self.port = reactor.listenTCP(port, self, interface=interface) except CannotListenError: log.warning('WARNING: Could not listen on port %s', port) self.port = reactor.listenTCP(0, self, interface=interface) def getTCPPort(self): return self.port.getHost().port def stopListening(self): self.port.stopListening() def gotBadString(self, protocol, data): log.warning('Server: Unrecognised network data: %r' % (data, )) log.warning(' : Did you invent a new network message and forget') log.warning(' : to add it to trosnoth.network.server.serverMsgs?') def connectionEstablished(self, protocol): ''' Called by the network manager when a new incoming connection is completed. ''' # Remember that this connection's ready for transmission. self.connectedClients.add(protocol) hub = LocalHub(self.game) hub.connectNode(protocol) # Send the setting information. protocol.gotServerCommand(InitClientMsg(self._getClientSettings())) def _getClientSettings(self): '''Returns a string representing the settings which must be sent to clients that connect to this server.''' result = self.game.world.dumpEverything() result['serverVersion'] = serverVersion return repr(result) def connectionLost(self, protocol, reason): if protocol in self.connectedClients: protocol.hub.disconnectNode() self.connectedClients.remove(protocol) # Check for game over and no connections left. if (len(self.connectedClients) == 0 and self.game.world.uiOptions.showReadyStates): # Don't shut down if local player is connected. for p in self.game.world.players: if not p.bot: break else: # Shut down the server. self.shutdown() def shutdown(self): if self._alreadyShutdown: return self._alreadyShutdown = True # Kill server self.running = False self.game.stop() self.onShutdown.execute()
class ThemeTab(Tab, framework.TabFriendlyCompoundElement): def __init__(self, app, onClose=None, onRestart=None): super(ThemeTab, self).__init__(app, 'Themes') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) self.onRestart = Event() if onRestart is not None: self.onRestart.addListener(onRestart) font = self.app.screenManager.fonts.menuFont colours = self.app.theme.colours self.inactiveTheme = False self.originalTheme = app.theme.name # Static text self.staticText = [ TextElement(self.app, 'theme information:', font, ScaledLocation(960, 250, 'topright'), colours.headingColour), TextElement(self.app, 'theme contents:', font, ScaledLocation(960, 390, 'topright'), colours.headingColour) ] # Dynamic text self.feedbackText = TextElement( self.app, 'Your current theme: %s' % (app.theme.name, ), font, ScaledLocation(512, 200, 'midtop'), colours.startButton) self.listHeaderText = TextElement(self.app, 'available themes:', font, ScaledLocation(70, 250), colours.headingColour) self.themeNameText = TextElement(self.app, 'Default Theme', font, ScaledLocation(960, 290, 'topright'), colours.startButton) self.themeAuthorText = TextElement( self.app, 'created by: Trosnoth Team', font, ScaledLocation(960, 330, 'topright'), colours.startButton) self.contents = [] numContents = 4 for yPos in range(430, 430 + numContents * 40 + 1, 40): self.contents.append( TextElement(self.app, '', font, ScaledLocation(960, yPos, 'topright'), colours.startButton)) self.dynamicText = [ self.feedbackText, self.listHeaderText, self.themeNameText, self.themeAuthorText ] + self.contents # Theme list self.themeList = ListBox(self.app, ScaledArea(70, 290, 400, 290), [], font, colours.listboxButtons) self.themeList.onValueChanged.addListener(self.updateSidebar) # Text buttons self.useThemeButton = button(app, 'use selected theme', self.applyTheme, (0, -125), 'midbottom') self.refreshButton = button(app, 'refresh', self.populateList, (-100, -75), 'midbottom') self.cancelButton = button(app, 'cancel', self.backToMain, (100, -75), 'midbottom') self.restartButton = button(app, 'restart Trosnoth', self.restart, (0, -125), 'midbottom') self.buttons = [ self.useThemeButton, self.refreshButton, self.cancelButton, self.restartButton ] # Combine the elements self.elements = self.staticText + self.dynamicText + self.buttons + [ self.themeList ] self.contentTypes = { "sprites": "sprite", "blocks": "map block", "fonts": "font", "startupMenu": "backdrop" } # Populate the list of replays self.populateList() def populateList(self): defaultTheme = { "name": "Default Theme", "filename": "default", "author": "Trosnoth Team", "content": None, "source": "internal" } # Clear out the sidebar self.themeNameText.setText('') self.themeAuthorText.setText('') for element in self.contents: element.setText('') self.useThemeButton.setText('') self.restartButton.setText('') self.listHeaderText.setText('available themes:') self.themeList.index = -1 userThemeDir = getPath(user, 'themes') internalThemeDir = getPath(themes) makeDirs(userThemeDir) themeList = [] # Get a list of internal themes for dirName in os.listdir(internalThemeDir): if os.path.isdir(os.path.join(internalThemeDir, dirName)): themeList.append("i/%s" % dirName) # Get a list of user-defined themes for dirName in os.listdir(userThemeDir): if os.path.isdir(os.path.join(userThemeDir, dirName)): # Internal themes overrule user-defined themes if "i/" + dirName not in themeList and dirName != "default": themeList.append("u/%s" % dirName) # Assume all themes are valid for now validThemes = themeList[:] self.themeInfo = {} for themeName in themeList: themeInfo = {"content": {}} if themeName.startswith("i/"): themeInfo['source'] = 'internal' directory = internalThemeDir else: themeInfo['source'] = 'user-defined' directory = userThemeDir themeNameList = themeName themeName = themeName[2:] themeInfo['filename'] = themeName[2:] anyContent = False for contentType in list(self.contentTypes.keys()): if themeInfo['source'] == 'internal': contentDir = os.path.join(directory, themeName, contentType) else: contentDir = os.path.join(directory, themeName, contentType) if not os.path.isdir(contentDir): continue else: fileCount = len([ f for f in os.listdir(contentDir) if os.path.isfile(os.path.join(contentDir, f)) ]) if fileCount > 0: anyContent = True themeInfo["content"][contentType] = fileCount if not anyContent: validThemes.remove(themeNameList) continue infoFile = os.path.join(directory, themeName, "info.txt") if os.path.isfile(infoFile): infoFile = open(infoFile) infoContents = infoFile.readlines() else: infoContents = [] if len(infoContents) >= 2: themeInfo["author"] = infoContents[1].strip() else: themeInfo["author"] = None if len(infoContents) >= 1: themeInfo["name"] = infoContents[0].strip() else: themeInfo["name"] = themeName self.themeInfo[themeName] = themeInfo self.themeInfo["default"] = defaultTheme # Sort the themes alphabetically items = [(v['filename'], n) for n, v in self.themeInfo.items()] items.sort() items = [n for v, n in items] self.themeList.setItems(items) if len(self.themeInfo) == 1: self.listHeaderText.setText("1 available theme:") self.themeList.index = 0 self.updateSidebar(0) else: self.listHeaderText.setText("%d available themes:" % len(self.themeInfo)) def updateSidebar(self, listID): # Update the details on the sidebar displayName = self.themeList.getItem(listID) themeInfo = self.themeInfo[displayName] if themeInfo['source'] == "internal": self.themeNameText.setText(themeInfo['name'] + " (built-in)") else: self.themeNameText.setText(themeInfo['name']) if themeInfo['author'] is not None: self.themeAuthorText.setText("by " + themeInfo['author']) else: self.themeAuthorText.setText("(creator unknown)") if themeInfo['content'] is None: for element in self.contents: element.setText('') self.contents[0].setText('N/A') else: count = 0 for k, v in themeInfo['content'].items(): suffix = "" if v != 1: suffix = "s" text = "%d %s%s" % (v, self.contentTypes[k], suffix) self.contents[count].setText(text) count += 1 self.restartButton.setText("") if self.app.displaySettings.theme != displayName: if displayName == self.originalTheme: self.useThemeButton.setText("revert back to this theme") else: self.useThemeButton.setText("use this theme") else: self.useThemeButton.setText("") if self.inactiveTheme: self.restartButton.setText('restart Trosnoth') def applyTheme(self): themeName = self.themeList.getItem(self.themeList.index) self.app.displaySettings.theme = themeName self.app.displaySettings.save() self.useThemeButton.setText("") if themeName != self.originalTheme: self.feedbackText.setText("You must restart Trosnoth for the " "'%s' theme to take effect." % themeName) self.restartButton.setText('restart Trosnoth') self.inactiveTheme = True else: self.feedbackText.setText('Your current theme: %s' % (self.app.theme.name, )) self.restartButton.setText("") self.inactiveTheme = False def restart(self): self.onRestart.execute() def backToMain(self): if self.inactiveTheme: self.feedbackText.setText("Your current theme: %s " "(restart required)" % self.app.displaySettings.theme) self.onClose.execute() def draw(self, surface): super(ThemeTab, self).draw(surface) width = max(int(3 * self.app.screenManager.scaleFactor), 1) scalePoint = self.app.screenManager.placePoint pygame.draw.line(surface, self.app.theme.colours.mainMenuColour, scalePoint((512, 260)), scalePoint((512, 580)), width)
class InputBox(framework.Element): pxFromLeft = 2 pxFromRight = 5 pxToSkipWhenCursorGoesToTheRight = 90 cursorFlashTime = 0.6 def __init__(self, app, area, initValue='', validator=None, font=None, colour=(255, 255, 255), maxLength=None, onClick=None, onEnter=None, onTab=None, onEsc=None, onEdit=None): super(InputBox, self).__init__(app) self.area = area self.onClick = Event() if onClick is not None: self.onClick.addListener(onClick) self.onEnter = Event() if onEnter is not None: self.onEnter.addListener(onEnter) self.onTab = Event() if onTab is not None: self.onTab.addListener(onTab) self.onEsc = Event() if onEsc is not None: self.onEsc.addListener(onEsc) self.onEdit = Event() if onEdit is not None: self.onEdit.addListener(onEdit) if validator: self.validator = validator else: self.validator = lambda x: True if font: self.font = font else: self.font = self.app.screenManager.fonts.defaultTextBoxFont self.value = initValue self.maxLength = maxLength self.fontColour = (0, 0, 0) self.backColour = colour self.cursorColour = (255, 0, 0) self._cursorVisible = True self._timeToFlash = self.cursorFlashTime # Calculate the white part of the input area self.cursorIndex = len(self.value) self.offset = 0 def clear(self): self.setValue('') def setValue(self, value): self.value = value self.offset = 0 self.cursorIndex = len(self.value) def getValue(self): return self.value def setFont(self, font): self.font = font def setValidator(self, validator): self.validator = validator def setMaxLength(self, length): self.maxLength = length def _getSize(self): return self._getRect().size def _getCursorImage(self): s = pygame.Surface((2, min(int(self.font.getHeight(self.app)), self._getSize()[1] * 0.8))) s.fill(self.cursorColour) return s def _getRect(self): return self.area.getRect(self.app) def _getPt(self): return self._getRect().topleft def setFontColour(self, fontColour): self.fontColour = fontColour def setBackColour(self, backColour): self.backColour = backColour def setCursorColour(self, cursorColour): self.cursorColour = cursorColour def gotFocus(self): self._cursorVisible = True self._timeToFlash = self.cursorFlashTime def _renderText(self): ''' Provided so that PasswordBox can override it. ''' return self.font.render(self.app, self.value, True, self.fontColour, self.backColour) def draw(self, surface): rect = self._getRect() size = rect.size pos = rect.topleft # Fill the input area with the specified colour surface.fill(self.backColour, rect) # Put what's currently inputted into the input area inputText = self._renderText() # Adjust offset cursorPos = self._getCursorPos() if cursorPos < self.offset: self.offset = max( 0, self.offset - self.pxToSkipWhenCursorGoesToTheRight) else: minOffset = cursorPos - size[0] + self.pxFromRight + self.pxFromLeft if self.offset < minOffset: self.offset = minOffset text_rect = inputText.get_rect() text_rect.centery = rect.centery diff = (text_rect.height - rect.height) / 2 # Find the currently-displaying text (based on where the cursor is): area = pygame.Rect(self.offset, diff, size[0] - self.pxFromRight, rect.height) # Put the text on the screen surface.blit(inputText, (pos[0] + self.pxFromLeft, rect.top), area) # Add the cursor where it is. if self.hasFocus and self._cursorVisible: cs = self._getCursorImage() cursor_rect = cs.get_rect() cursor_rect.centery = rect.centery surface.blit( cs, (pos[0] + self._getCursorPos() - self.offset, cursor_rect.top)) def _getCursorPos(self): return self._getFontSize(self.value[:self.cursorIndex])[0] + 1 def _getFontSize(self, text): ''' Provided so that PasswordBox can override it. ''' return self.font.size(self.app, text) # Get the index of the position clicked def __getCursorIndex(self, clickOffset): totalOffset = clickOffset + self.offset + self.pxFromLeft i = 1 fontOffset = 0 last = 0 while fontOffset < totalOffset and i <= len(self.value): last = fontOffset fontOffset = self._getFontSize(self.value[:i])[0] i += 1 if (fontOffset - totalOffset) <= (totalOffset - last): return i - 1 else: return i - 2 return i def processEvent(self, event): '''Processes the key press. If we use the event, return nothing. If we do not use it, return the event so something else can use it.''' if self.hasFocus and event.type == pygame.KEYDOWN: self._cursorVisible = True self._timeToFlash = self.cursorFlashTime if event.key in (pygame.K_RETURN, pygame.K_KP_ENTER): self.onEnter.execute(self) return None if event.key == pygame.K_ESCAPE: self.onEsc.execute(self) return None if event.key == pygame.K_TAB: self.onTab.execute(self) return None # Delete the letter behind the cursor if event.key == pygame.K_BACKSPACE: # Make sure there's something in the string behind the cursor # first, don't want to kill what's not there if self.cursorIndex > 0: self.value = (self.value[:self.cursorIndex - 1] + self.value[self.cursorIndex:]) self.cursorIndex -= 1 self.onEdit.execute(self) elif event.key == pygame.K_DELETE: # Make sure there's something in the string in front of the # cursor first, don't want to kill what's not there if self.cursorIndex < len(self.value): self.value = (self.value[:self.cursorIndex] + self.value[self.cursorIndex + 1:]) self.onEdit.execute(self) elif event.key == pygame.K_LEFT: if self.cursorIndex > 0: self.cursorIndex -= 1 elif event.key == pygame.K_RIGHT: if self.cursorIndex < len(self.value): self.cursorIndex += 1 elif event.key == pygame.K_END: self.cursorIndex = len(self.value) elif event.key == pygame.K_HOME: self.cursorIndex = 0 self.offset = 0 # Add the character to our string elif event.unicode == '': return event else: # Validate the input. if not self.validator(event.unicode): return event # Check the maxLength if self.maxLength is not None: if len(self.value) >= self.maxLength: return event # Add the typed letter to the string self.value = (self.value[:self.cursorIndex] + event.unicode + self.value[self.cursorIndex:]) self.cursorIndex += len(event.unicode) self.onEdit.execute(self) else: rect = self._getRect() if (event.type == pygame.MOUSEBUTTONDOWN and rect.collidepoint(event.pos) and event.button == 1): self.onClick.execute(self) xOffset = event.pos[0] - rect.left self.cursorIndex = self.__getCursorIndex(xOffset) self._timeToFlash = self.cursorFlashTime self._cursorVisible = True else: # It's not a keydown event. Pass it on. return event def tick(self, deltaT): self._timeToFlash -= deltaT while self._timeToFlash < 0: self._timeToFlash += self.cursorFlashTime self._cursorVisible = not self._cursorVisible
class Slider(framework.Element): def __init__(self, app, area, bounds=None, snap=False): super(Slider, self).__init__(app) self.app = app self.area = area self.beingDragged = False self.snap = snap if not bounds: bounds = 0, 100 self.lowBound, self.highBound = bounds self.unscaledSlider = 20 self.sliderRectWidth = int(self.unscaledSlider * app.screenManager.scaleFactor) self.lineColour = (0, 192, 0) self.sliderColour = (0, 255, 0) self.onValueChanged = Event() self.onSlide = Event() self.largeArea = ScaledArea( self.area.point.val[0] + self.unscaledSlider / 2, self.area.point.val[1], self.area.size.val[0] - self.unscaledSlider + 1, self.area.size.val[1]) self.area, self.largeArea = self.largeArea, self.area self.sliderVal = self.highBound def doSnap(self, value): if self.snap: return int(value + 0.5) return value def getVal(self): return self.sliderVal def setVal(self, val): old = self.sliderVal self.sliderVal = self.doSnap(val) if old != self.sliderVal: self.onValueChanged.execute(self.sliderVal) def setRange(self, low, high): self.lowBound = low self.highBound = high if self.sliderVal > self.highBound: self.sliderVal = self.highBound elif self.sliderVal < self.lowBound: self.sliderVal = self.lowBound def setSliderColour(self, colour): self.sliderColour = colour def setLineColour(self, colour): self.lineColour = colour def _getRange(self): return self.highBound - self.lowBound def _getRect(self): return self.area.getRect(self.app) def _getPt(self): return self._getRect().topleft def _getSize(self): return self._getRect().size def _valPerPixel(self): return self._getSize()[0] / (self._getRange() + 0.) def _pixelPerVal(self): return self._getRange() / (self._getSize()[0] + 0.) # Number of pixels from 0 to width def _relPx(self, x): return min(max(0, (x - self._getPt()[0])), self._getSize()[0]) def _mouseAt(self, x): old = self.sliderVal self.sliderVal = self.lowBound + self._pixelPerVal() * self._relPx(x) if old != self.sliderVal: self.onSlide.execute(self.sliderVal) def _getSliderRect(self): # Ignore pos for now sliderRect = pygame.Rect(0, 0, self._getSliderRectWidth(), self._getSize()[1]) pt = self._getPt() sliderRect.midtop = ( (self.sliderVal - self.lowBound) * self._valPerPixel() + pt[0], pt[1]) return sliderRect def _getLineWidth(self): return max(1, self._getSize()[1] / 8) def _getSliderRectWidth(self): return int(self.unscaledSlider * self.app.screenManager.scaleFactor) def processEvent(self, event): if (event.type == pygame.MOUSEBUTTONDOWN and self.largeArea.getRect(self.app).collidepoint(event.pos)): # Each click of the scroll wheel moves the slider by 5% percent = int((self.highBound - self.lowBound) * 0.05) if event.button == 1: self.beingDragged = True self._mouseAt(event.pos[0]) elif event.button == 4: self.setVal(min(self.highBound, self.sliderVal + percent)) elif event.button == 5: self.setVal(max(self.lowBound, self.sliderVal - percent)) else: return event elif event.type == pygame.MOUSEMOTION and self.beingDragged: self._mouseAt(event.pos[0]) elif (event.type == pygame.MOUSEBUTTONUP and event.button == 1 and self.beingDragged): self.beingDragged = False self.sliderVal = self.doSnap(self.sliderVal) self.onValueChanged.execute(self.sliderVal) else: return event return None def draw(self, surface): r = self.largeArea.getRect(self.app) lineWidth = self._getLineWidth() pygame.draw.line(surface, self.lineColour, r.midleft, r.midright, lineWidth) if self.snap: low = r.left high = r.right count = self.highBound - self.lowBound + 0. y0 = r.centery - 0.25 * r.height y1 = r.centery + 0.25 * r.height for i in xrange(self.lowBound, self.highBound + 1): x = (high * i + low * (count - i)) / count pygame.draw.line(surface, self.lineColour, (x, y0), (x, y1), lineWidth) surface.fill(self.sliderColour, self._getSliderRect())
class KeycodeBox(framework.Element): pxFromLeft = 2 pxFromRight = 5 def __init__(self, app, area, initValue=None, font=None, colour=(255, 255, 255), focusColour=(192, 192, 255), acceptMouse=False): super(KeycodeBox, self).__init__(app) self.area = area self.onClick = Event() self.onChange = Event() if font: self.font = font else: self.font = self.app.screenManager.fonts.defaultTextBoxFont self.value = initValue self.fontColour = (0, 0, 0) self.realBackColour = self._backColour = colour self.focusColour = focusColour self.acceptMouse = acceptMouse def _getBackColour(self): return self._backColour def _setBackColour(self, colour): if self.realBackColour == self._backColour: self.realBackColour = colour self._backColour = colour backColour = property(_getBackColour, _setBackColour) def draw(self, surface): rect = self._getRect() size = rect.size pos = rect.topleft # Fill the input area with the specified colour surface.fill(self.realBackColour, rect) # Put what's currently inputted into the input area if self.value is None: name = '' else: name = keyboard.shortcutName(self.value) inputText = self.font.render(self.app, name, True, self.fontColour, self.realBackColour) text_rect = inputText.get_rect() diff = (text_rect.height - rect.height) / 2 area = pygame.Rect(0, diff, size[0] - self.pxFromRight, rect.height) # Put the text on the screen text_rect.centery = rect.centery surface.blit(inputText, (pos[0] + self.pxFromLeft, rect.top), area) def processEvent(self, event): '''Processes the key press. If we use the event, return nothing. If we do not use it, return the event so something else can use it.''' if self.hasFocus: # Catch a single keystroke. if event.type == pygame.KEYDOWN: self.caughtKey(event.key) elif self.acceptMouse and event.type == pygame.MOUSEBUTTONDOWN: if event.button != 1: self.caughtKey(keyboard.mouseButton(event.button)) else: rect = self._getRect() if (event.type == pygame.MOUSEBUTTONDOWN and rect.collidepoint(event.pos)): self.onClick.execute(self) else: # It's not a keydown event. Pass it on. return event def caughtKey(self, key): self.value = key self.hasFocus = False self.lostFocus() self.onChange.execute(self) def _getSize(self): return self._getRect().size def _getRect(self): return self.area.getRect(self.app) def _getPt(self): return self._getRect().topleft def gotFocus(self): self.realBackColour = self.focusColour def lostFocus(self): self.realBackColour = self._backColour
class PlayAuthScreen(framework.CompoundElement): passwordGUIFactory = PasswordGUI def __init__(self, app, onSucceed=None, onFail=None): super(PlayAuthScreen, self).__init__(app) self.onSucceed = Event(listener=onSucceed) self.onFail = Event(listener=onFail) self.lobby = None self.badServers = set() if app.displaySettings.alphaOverlays: alpha = 192 else: alpha = None bg = SolidRect(app, app.theme.colours.playMenu, alpha, Region(centre=Canvas(512, 384), size=Canvas(924, 500))) colour = app.theme.colours.mainMenuColour font = app.screenManager.fonts.consoleFont self.logBox = LogBox( app, Region(size=Canvas(900, 425), midtop=Canvas(512, 146)), colour, font) font = app.screenManager.fonts.bigMenuFont cancel = TextButton(app, Location(Canvas(512, 624), 'midbottom'), 'Cancel', font, app.theme.colours.secondMenuColour, app.theme.colours.white, onClick=self.cancel) self.cancelled = False self.elements = [bg, self.logBox, cancel] self._passwordGetter = None @property def passwordGetter(self): if self._passwordGetter is None: self._passwordGetter = self.passwordGUIFactory(self.app) return self._passwordGetter @defer.inlineCallbacks def begin(self, servers, canHost=True): self.cancelled = False self.passwordGetter.cancelled = False self.badServers = set() if self.lobby is None: self.lobby = Lobby(self.app) # Removes the third item (http) from the tuple since we don't care about # it. servers = [(server[:2] if isinstance(server, tuple) else server) for server in servers] for server in servers: if self.cancelled: break if server == 'self': if self.app.server is not None: self.onSucceed.execute() self.app.interface.connectToLocalServer() return elif isinstance(server, tuple): if server in self.badServers: continue self.logBox.log('Requesting games from %s:%d...' % server) connected = yield self.attemptServerConnect( self.lobby.getGames, server) if connected: return elif server == 'others': for server in servers: if (server in self.badServers or not isinstance(server, tuple)): continue self.logBox.log('Asking %s:%d about other games...' % server) connected = yield self.attemptServerConnect( self.lobby.getOtherGames, server) if connected: return elif server == 'lan': self.logBox.log('Asking local network for other games...') games = yield self.lobby.getMulticastGames() for game in games: joinSuccessful = yield self.attemptJoinGame(game) if joinSuccessful: return elif server == 'create': for server in servers: if server in self.badServers or not isinstance( server, tuple): continue self.logBox.log('Asking to create game on %s...' % (server[0], )) joinSuccessful = yield self.attemptCreateGame(server) if joinSuccessful: return if canHost: if not self.cancelled: result = yield HostGameQuery(self.app).run() if not result: self.onFail.execute() return self.app.startServer(2, 1) # Notify remaining auth servers of this game. for server in servers: if server in self.badServers or not isinstance( server, tuple): continue self.logBox.log('Registering game with %s...' % (server[0], )) try: result = yield self.lobby.registerGame( server, self.app.server) except ConnectError: self.logBox.log('Unable to connect.') if not result: self.logBox.log('Registration failed.') self.onSucceed.execute() self.app.interface.connectToLocalServer() else: if not self.cancelled: box = OkBox(self.app, ScaledSize(450, 150), 'Trosnoth', 'Connection unsuccessful.') box.onClose.addListener(self.onFail.execute) box.show() @defer.inlineCallbacks def attemptServerConnect(self, getGamesList, server): ''' Attempts to connect to a game on the server, returned by getGamesList(server). ''' try: games = yield getGamesList(server) except ConnectError: self.logBox.log('Unable to connect.') self.badServers.add(server) except amp.UnknownRemoteError: self.logBox.log('Error on remote server.') except amp.RemoteAmpError, e: self.logBox.log('Error on remote server.') log.exception(str(e)) except IOError, e: self.logBox.log('Error connecting to remote server.') log.exception(str(e)) self.badServers.add(server)