2009-08-25 18:04:42 -06:00
|
|
|
#! /usr/bin/env python3
|
|
|
|
|
|
|
|
import json
|
|
|
|
import asyncore
|
|
|
|
import asynchat
|
|
|
|
import socket
|
|
|
|
import traceback
|
2009-08-26 15:14:09 -06:00
|
|
|
import time
|
|
|
|
from errno import EPIPE
|
2009-10-05 13:33:20 -06:00
|
|
|
from . import teams
|
|
|
|
from . import Flagger
|
2009-08-26 15:14:09 -06:00
|
|
|
|
|
|
|
|
2009-08-26 17:46:06 -06:00
|
|
|
# Heartbeat frequency (in seconds)
|
|
|
|
pulse = 2.0
|
|
|
|
|
|
|
|
##
|
|
|
|
## Network stuff
|
|
|
|
##
|
|
|
|
|
2009-08-25 18:04:42 -06:00
|
|
|
class Listener(asyncore.dispatcher):
|
|
|
|
def __init__(self, addr, player_factory, manager):
|
|
|
|
asyncore.dispatcher.__init__(self)
|
|
|
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
self.set_reuse_addr()
|
|
|
|
self.bind(addr)
|
|
|
|
self.listen(4)
|
|
|
|
self.player_factory = player_factory
|
|
|
|
self.manager = manager
|
|
|
|
|
|
|
|
def handle_accept(self):
|
|
|
|
conn, addr = self.accept()
|
|
|
|
player = self.player_factory(conn, self.manager)
|
|
|
|
# We don't need to keep the player, asyncore.socket_map already
|
|
|
|
# has a reference to it for as long as it's open.
|
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
def readable(self):
|
2009-09-02 10:15:54 -06:00
|
|
|
self.manager.heartbeat(time.time())
|
2009-09-01 11:23:40 -06:00
|
|
|
return True
|
|
|
|
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
class Manager:
|
|
|
|
"""Contest manager.
|
|
|
|
|
|
|
|
When a player connects and registers, they enter the lobby. As soon
|
|
|
|
as there are enough players in the lobby to run a game, everyone in
|
|
|
|
the lobby becomes a contestant. Contestants are assigned to games.
|
|
|
|
When the game declares a winner, the winner is added back to the list
|
|
|
|
of contestants, and other players are sent back to the lobby. When
|
|
|
|
a winner is declared by the last running game, that winner gets the
|
|
|
|
flag.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
def __init__(self, game_factory, flagger, minplayers, maxplayers=None):
|
2009-08-25 18:04:42 -06:00
|
|
|
self.game_factory = game_factory
|
|
|
|
self.flagger = flagger
|
2009-08-28 15:52:56 -06:00
|
|
|
self.minplayers = minplayers
|
|
|
|
self.maxplayers = maxplayers or minplayers
|
|
|
|
self.games = set()
|
2009-08-26 15:14:09 -06:00
|
|
|
self.lobby = set()
|
2009-08-25 18:04:42 -06:00
|
|
|
self.contestants = []
|
2009-09-01 11:23:40 -06:00
|
|
|
self.last_beat = 0
|
2009-09-02 10:15:54 -06:00
|
|
|
self.timers = set()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
def heartbeat(self, now):
|
2009-09-02 10:15:54 -06:00
|
|
|
"""Called by listener to beat heart."""
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
if now > self.last_beat + pulse:
|
|
|
|
for game in list(self.games):
|
|
|
|
game.heartbeat(now)
|
2009-09-25 13:27:37 -06:00
|
|
|
self.last_beat = now
|
2009-09-02 10:15:54 -06:00
|
|
|
for event in self.timers:
|
|
|
|
when, cb = event
|
|
|
|
if now >= when:
|
|
|
|
self.timers.remove(event)
|
|
|
|
cb()
|
|
|
|
|
|
|
|
def add_timer(self, when, cb):
|
|
|
|
"""Add a timed callback."""
|
|
|
|
|
|
|
|
self.timers.add((when, cb))
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def enter_lobby(self, player):
|
2009-08-26 15:14:09 -06:00
|
|
|
self.lobby.add(player)
|
|
|
|
self.run_contest()
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def add_contestant(self, player):
|
|
|
|
self.contestants.append(player)
|
|
|
|
self.run_contest()
|
|
|
|
|
2009-08-26 17:46:06 -06:00
|
|
|
def disconnect(self, player):
|
|
|
|
"""Player has disconnected."""
|
2009-08-26 15:14:09 -06:00
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
def set_flag(self, player):
|
2009-08-26 17:46:06 -06:00
|
|
|
"""Player has the flag."""
|
2009-08-26 15:14:09 -06:00
|
|
|
|
|
|
|
self.flagger.set_flag(player.name)
|
|
|
|
|
|
|
|
def start_contest(self):
|
2009-08-28 15:52:56 -06:00
|
|
|
"""Start a new contest."""
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
self.contestants = list(self.lobby)
|
2009-09-01 11:23:40 -06:00
|
|
|
print('new playoff:', [c.name for c in self.contestants])
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-25 18:04:42 -06:00
|
|
|
def run_contest(self):
|
2009-08-26 15:14:09 -06:00
|
|
|
# Purge any disconnected players
|
|
|
|
self.contestants = [p for p in self.contestants if p.connected]
|
|
|
|
self.lobby = set([p for p in self.lobby if p.connected])
|
|
|
|
|
|
|
|
llen = len(self.lobby)
|
|
|
|
clen = len(self.contestants)
|
|
|
|
glen = len(self.games)
|
2009-08-28 15:52:56 -06:00
|
|
|
if llen == 1:
|
2009-08-26 15:14:09 -06:00
|
|
|
# Give the flag to the only team connected
|
|
|
|
self.set_flag(list(self.lobby)[0])
|
2009-08-28 15:52:56 -06:00
|
|
|
elif llen < self.minplayers:
|
|
|
|
# More than one connected team, but still not enough to play
|
|
|
|
self.set_flag(None)
|
|
|
|
elif (clen == 1) and (glen == 0):
|
2009-08-26 15:14:09 -06:00
|
|
|
# Give the flag to the last team standing, and start a new contest
|
|
|
|
self.set_flag(self.contestants.pop())
|
|
|
|
self.start_contest()
|
2009-08-28 15:52:56 -06:00
|
|
|
elif (llen >= self.minplayers) and (clen == 0) and (glen == 0):
|
|
|
|
# There are enough in the lobby to begin a contest now
|
2009-08-26 15:14:09 -06:00
|
|
|
self.start_contest()
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
while len(self.contestants) >= self.minplayers:
|
|
|
|
players = self.contestants[:self.maxplayers]
|
|
|
|
del self.contestants[:self.maxplayers]
|
|
|
|
game = self.game_factory(self, set(players))
|
|
|
|
self.games.add(game)
|
2009-08-25 18:04:42 -06:00
|
|
|
for player in players:
|
|
|
|
player.attach_game(game)
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
def declare_winner(self, game, winner=None):
|
2009-09-01 11:23:40 -06:00
|
|
|
print('Winner:', winner and winner.name)
|
2009-08-28 15:52:56 -06:00
|
|
|
self.games.remove(game)
|
2009-08-26 15:14:09 -06:00
|
|
|
|
|
|
|
# Winner stays in the contest
|
2009-08-28 15:52:56 -06:00
|
|
|
if winner:
|
|
|
|
self.add_contestant(winner)
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def player_cmd(self, args):
|
|
|
|
cmd = args[0].lower()
|
|
|
|
if cmd == 'lobby':
|
|
|
|
return [p.name for p in self.lobby]
|
|
|
|
elif cmd == 'games':
|
2009-08-28 15:52:56 -06:00
|
|
|
return len(self.games)
|
2009-08-25 18:04:42 -06:00
|
|
|
elif cmd == 'flag':
|
|
|
|
return self.flagger.flag
|
|
|
|
else:
|
|
|
|
raise ValueError('Unrecognized manager command')
|
|
|
|
|
|
|
|
|
|
|
|
class Player(asynchat.async_chat):
|
2009-08-28 15:52:56 -06:00
|
|
|
# How long can a connection not send anything at all (unless blocked)?
|
|
|
|
timeout = 10.0
|
|
|
|
|
2009-08-25 18:04:42 -06:00
|
|
|
def __init__(self, sock, manager):
|
|
|
|
asynchat.async_chat.__init__(self, sock=sock)
|
|
|
|
self.manager = manager
|
|
|
|
self.game = None
|
|
|
|
self.set_terminator(b'\n')
|
|
|
|
self.inbuf = []
|
|
|
|
self.blocked = None
|
|
|
|
self.name = None
|
|
|
|
self.pending = None
|
2009-09-01 11:23:40 -06:00
|
|
|
self.last_activity = time.time()
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def readable(self):
|
2009-08-26 15:14:09 -06:00
|
|
|
ret = (not self.blocked) and asynchat.async_chat.readable(self)
|
|
|
|
if ret:
|
2009-09-01 11:23:40 -06:00
|
|
|
if time.time() - self.last_activity > self.timeout:
|
2009-08-26 15:14:09 -06:00
|
|
|
# They waited too long.
|
|
|
|
self.err('idle timeout')
|
|
|
|
self.close()
|
|
|
|
return False
|
|
|
|
return ret
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def block(self):
|
|
|
|
"""Block reads"""
|
|
|
|
self.blocked = True
|
|
|
|
|
|
|
|
def unblock(self):
|
|
|
|
"""Unblock reads"""
|
|
|
|
self.blocked = False
|
2009-09-01 11:23:40 -06:00
|
|
|
self.last_activity = time.time()
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def attach_game(self, game):
|
|
|
|
self.game = game
|
|
|
|
if self.pending:
|
|
|
|
self.unblock()
|
|
|
|
self.game.handle(self, *self.pending)
|
2009-08-26 15:14:09 -06:00
|
|
|
self.pending = None
|
|
|
|
|
|
|
|
def detach_game(self):
|
|
|
|
self.game = None
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def _write_val(self, val):
|
|
|
|
s = json.dumps(val) + '\n'
|
|
|
|
self.push(s.encode('utf-8'))
|
|
|
|
|
|
|
|
def write(self, val):
|
|
|
|
self._write_val(['OK', val])
|
|
|
|
|
|
|
|
def err(self, msg):
|
|
|
|
self._write_val(['ERR', msg])
|
|
|
|
|
2009-08-26 16:33:07 -06:00
|
|
|
def win(self):
|
2009-08-26 17:46:06 -06:00
|
|
|
self.detach_game()
|
2009-08-26 16:33:07 -06:00
|
|
|
self._write_val(['WIN'])
|
2009-08-25 18:04:42 -06:00
|
|
|
self.unblock()
|
|
|
|
|
|
|
|
def lose(self):
|
2009-08-26 17:46:06 -06:00
|
|
|
self.detach_game()
|
2009-08-25 18:04:42 -06:00
|
|
|
self._write_val(['LOSE'])
|
|
|
|
self.unblock()
|
|
|
|
|
|
|
|
def collect_incoming_data(self, data):
|
|
|
|
self.inbuf.append(data)
|
2009-08-26 16:33:07 -06:00
|
|
|
if len(self.inbuf) > 10:
|
|
|
|
self.err('Too much data, punk.')
|
|
|
|
self.close()
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
def found_terminator(self):
|
2009-08-26 15:14:09 -06:00
|
|
|
self.last_activity = time.time()
|
2009-08-25 18:04:42 -06:00
|
|
|
try:
|
|
|
|
data = b''.join(self.inbuf)
|
|
|
|
self.inbuf = []
|
|
|
|
val = json.loads(data.decode('utf-8'))
|
|
|
|
cmd, args = val[0].lower(), val[1:]
|
|
|
|
|
|
|
|
if cmd == 'login':
|
2009-09-02 10:15:54 -06:00
|
|
|
if self.name:
|
|
|
|
self.err('Already logged in.')
|
|
|
|
elif teams.chkpasswd(args[0], args[1]):
|
2009-08-25 18:04:42 -06:00
|
|
|
self.name = args[0]
|
|
|
|
self.write('Welcome to the fray, %s.' % self.name)
|
|
|
|
self.manager.enter_lobby(self)
|
|
|
|
else:
|
2009-09-02 10:15:54 -06:00
|
|
|
self.err('Invalid password.')
|
2009-08-25 18:04:42 -06:00
|
|
|
elif cmd == '^':
|
|
|
|
# Send to manager
|
|
|
|
ret = self.manager.player_cmd(args)
|
|
|
|
self.write(ret)
|
|
|
|
elif not self.name:
|
|
|
|
self.err('Log in first.')
|
|
|
|
else:
|
|
|
|
# Send to game
|
|
|
|
if not self.game:
|
|
|
|
self.pending = (cmd, args)
|
|
|
|
self.block()
|
|
|
|
else:
|
|
|
|
self.game.handle(self, cmd, args)
|
|
|
|
except Exception as err:
|
|
|
|
traceback.print_exc()
|
|
|
|
self.err(str(err))
|
|
|
|
|
2009-08-26 15:14:09 -06:00
|
|
|
def close(self):
|
2009-08-28 15:52:56 -06:00
|
|
|
self.unblock()
|
2009-08-26 15:14:09 -06:00
|
|
|
if self.game:
|
2009-08-28 15:52:56 -06:00
|
|
|
self.game.player_died(self)
|
2009-08-26 17:46:06 -06:00
|
|
|
self.manager.disconnect(self)
|
2009-08-26 15:14:09 -06:00
|
|
|
asynchat.async_chat.close(self)
|
|
|
|
|
|
|
|
def send(self, data):
|
|
|
|
try:
|
|
|
|
return asynchat.async_chat.send(self, data)
|
|
|
|
except socket.error as why:
|
|
|
|
if why.args[0] == EPIPE:
|
|
|
|
# Broken pipe, shut down.
|
|
|
|
self.close()
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
2009-08-25 18:04:42 -06:00
|
|
|
|
|
|
|
class Game:
|
|
|
|
def __init__(self, manager, players):
|
|
|
|
self.manager = manager
|
|
|
|
self.players = players
|
|
|
|
self.setup()
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
def setup(self):
|
|
|
|
pass
|
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
def heartbeat(self, now):
|
2009-08-26 17:46:06 -06:00
|
|
|
pass
|
|
|
|
|
|
|
|
def declare_winner(self, winner):
|
|
|
|
self.manager.declare_winner(self, winner)
|
|
|
|
|
|
|
|
# Congratulate winner
|
2009-08-28 15:52:56 -06:00
|
|
|
if winner:
|
|
|
|
winner.win()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
|
|
|
# Inform losers of their loss
|
|
|
|
losers = [p for p in players if p != winner]
|
|
|
|
for p in losers:
|
|
|
|
p.lose()
|
|
|
|
|
2009-08-26 15:14:09 -06:00
|
|
|
def handle(self, player, cmd, args):
|
|
|
|
"""Handle a command from player.
|
|
|
|
|
|
|
|
This just dispatches to 'self.do_[cmd]'.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
method_name = 'do_%s' % cmd
|
|
|
|
try:
|
|
|
|
method = getattr(self, method_name)
|
|
|
|
method(player, args)
|
|
|
|
except AttributeError:
|
|
|
|
raise ValueError('Invalid command: %s' % cmd)
|
|
|
|
|
2009-08-26 17:46:06 -06:00
|
|
|
def forfeit(self, player):
|
2009-08-28 15:52:56 -06:00
|
|
|
"""Player forfeits the game."""
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
self.remove(player)
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
def remove(self, player):
|
|
|
|
"""Remove the player from the game."""
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
self.players.remove(player)
|
|
|
|
player.detach_game()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
def player_died(self, player):
|
2009-08-26 17:46:06 -06:00
|
|
|
self.forfeit(player)
|
2009-08-26 15:14:09 -06:00
|
|
|
|
|
|
|
|
|
|
|
class TurnBasedGame(Game):
|
2009-08-26 17:46:06 -06:00
|
|
|
# How long you get to make a move (in seconds)
|
|
|
|
move_timeout = 2.0
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
# How long you get to complete the game (in seconds)
|
|
|
|
game_timeout = 6.0
|
|
|
|
|
2009-08-26 15:14:09 -06:00
|
|
|
def __init__(self, manager, players):
|
2009-09-01 11:23:40 -06:00
|
|
|
now = time.time()
|
2009-08-26 15:14:09 -06:00
|
|
|
self.ended_turn = set()
|
2009-08-28 15:52:56 -06:00
|
|
|
self.running = True
|
2009-08-26 17:46:06 -06:00
|
|
|
self.winner = None
|
|
|
|
self.lastmoved = dict([(p, now) for p in players])
|
2009-08-28 15:52:56 -06:00
|
|
|
self.began = now
|
2009-08-26 15:14:09 -06:00
|
|
|
Game.__init__(self, manager, players)
|
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
def heartbeat(self, now=None):
|
2009-09-25 13:27:37 -06:00
|
|
|
print('heart', self)
|
2009-09-01 11:23:40 -06:00
|
|
|
if now and (now - self.began > self.game_timeout):
|
2009-08-28 15:52:56 -06:00
|
|
|
self.running = False
|
|
|
|
|
|
|
|
# Idle players forfeit. They're also booted, so we don't have
|
|
|
|
# to worry about the synchronous illusion.
|
|
|
|
for player in list(self.players):
|
|
|
|
if not player.connected:
|
|
|
|
self.remove(player)
|
|
|
|
continue
|
|
|
|
when = self.lastmoved[player]
|
2009-08-26 17:46:06 -06:00
|
|
|
if now - when > self.move_timeout:
|
2009-08-28 15:52:56 -06:00
|
|
|
player.err('Timeout waiting for a move')
|
|
|
|
player.close()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
# If everyone left, nobody wins.
|
|
|
|
if not self.players:
|
|
|
|
self.manager.declare_winner(self, None)
|
|
|
|
|
|
|
|
def player_died(self, player):
|
|
|
|
Game.player_died(self, player)
|
|
|
|
if player in self.players:
|
|
|
|
# Update stuff
|
|
|
|
self.heartbeat()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
|
|
|
def declare_winner(self, winner):
|
|
|
|
"""Declare winner.
|
|
|
|
|
|
|
|
In a turn-based game, you can't tell anyone that the game has
|
|
|
|
ended until they make a move. Otherwise, you ruin the illusion
|
|
|
|
of the game being synchronous. This only sets the winner variable,
|
|
|
|
which is checked in self.end_turn().
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2009-08-28 15:52:56 -06:00
|
|
|
self.running = False
|
2009-08-26 17:46:06 -06:00
|
|
|
self.winner = winner
|
|
|
|
|
2009-08-26 15:14:09 -06:00
|
|
|
def calculate_moves(self):
|
2009-08-26 17:46:06 -06:00
|
|
|
"""Calculate all moves at the end of a turn.
|
|
|
|
|
|
|
|
Override this to define what to do when every player has ended
|
|
|
|
their turn.
|
|
|
|
|
|
|
|
"""
|
2009-08-26 15:14:09 -06:00
|
|
|
pass
|
|
|
|
|
|
|
|
def end_turn(self, player):
|
2009-08-26 17:46:06 -06:00
|
|
|
"""End player's turn."""
|
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
now = time.time()
|
2009-08-26 17:46:06 -06:00
|
|
|
|
2009-08-26 15:14:09 -06:00
|
|
|
self.ended_turn.add(player)
|
2009-08-26 17:46:06 -06:00
|
|
|
self.lastmoved[player] = now
|
2009-08-28 15:52:56 -06:00
|
|
|
if not self.players:
|
|
|
|
self.manager.declare_winner(self, None)
|
|
|
|
elif len(self.players) == 1:
|
|
|
|
winners = list(self.players)
|
|
|
|
self.declare_winner(winners[0])
|
|
|
|
elif len(self.ended_turn) >= len(self.players):
|
2009-08-26 15:14:09 -06:00
|
|
|
self.calculate_moves()
|
2009-08-28 15:52:56 -06:00
|
|
|
if self.running:
|
|
|
|
for p in self.players:
|
|
|
|
p.unblock()
|
|
|
|
else:
|
|
|
|
# Game has ended, tell everyone how they did
|
|
|
|
for p in list(self.players):
|
|
|
|
if self.winner == p:
|
|
|
|
p.win()
|
|
|
|
else:
|
|
|
|
p.lose()
|
|
|
|
self.manager.declare_winner(self, self.winner)
|
2009-08-26 15:14:09 -06:00
|
|
|
self.ended_turn = set()
|
2009-08-28 15:52:56 -06:00
|
|
|
elif self.running:
|
|
|
|
player.block()
|
|
|
|
else:
|
|
|
|
# The game has ended, tell the player, now that they've made
|
|
|
|
# a move.
|
|
|
|
if self.winner == player:
|
|
|
|
player.win()
|
|
|
|
self.manager.declare_winner(self, self.winner)
|
|
|
|
else:
|
|
|
|
player.lose()
|
|
|
|
self.remove(player)
|
2009-08-26 15:14:09 -06:00
|
|
|
|
2009-08-26 17:46:06 -06:00
|
|
|
|
|
|
|
##
|
|
|
|
## Running a game
|
|
|
|
##
|
|
|
|
|
2009-09-01 11:23:40 -06:00
|
|
|
def start(game_factory, port, auth, minplayers, maxplayers=None):
|
2009-08-25 18:04:42 -06:00
|
|
|
flagger = Flagger(('localhost', 6668), auth)
|
2009-08-28 15:52:56 -06:00
|
|
|
manager = Manager(game_factory, flagger, minplayers, maxplayers)
|
2009-08-25 18:04:42 -06:00
|
|
|
listener = Listener(('', port), Player, manager)
|
2009-09-01 11:23:40 -06:00
|
|
|
return (flagger, manager, listener)
|
|
|
|
|
|
|
|
def run(game_factory, port, auth, minplayers, maxplayers=None):
|
|
|
|
start(game_factory, port, auth, minplayers, maxplayers)
|
|
|
|
asyncore.loop(use_poll=True)
|
2009-08-25 18:04:42 -06:00
|
|
|
|