shiftgears/pong.py

217 lines
7.3 KiB
Python
Raw Permalink Normal View History

2020-04-16 08:58:21 -04:00
# The original server didn't even know about the pong multiplayer
# protocol, it just relayed "Forward" messages between clients and let
# them sort it all out. In practice this worked but if anybody with
# a sort of twisted Joker mind ever read the ShiftOS source then this
# could have led to mischief and malice.
# This implementation mediates the protocol to make sure messages are
# only going where they should. That makes it more complicated but
# on the plus side it's hopefully also more reliable even when
# everyone's playing by the rules.
import datetime
from messagehandler import forwardhandler, handler
class PongMatchmaking:
def __init__(self):
self.players = {}
self.current_heartbeat = 0
def join(self, newcomer):
for opponent in self.players.values():
opponent.connection.send_message("pong_handshake_matchmake", newcomer.name)
newcomer.connection.send_message("pong_handshake_matchmake", opponent.name)
self.players[newcomer.name] = newcomer
def leave(self, player):
try:
del self.players[player.name]
except KeyError:
return
for opponent in self.players.values():
opponent.connection.send_message("pong_handshake_left", player.name)
def heartbeat(self):
for player in list(self.players.values()): # copy to be able to delete
if self.current_heartbeat - player.last_heartbeat >= 3:
player.connection.infobox("Your connection to Pong has timed out.", "Timed out")
player.leave()
else:
player.send_heartbeat()
self.current_heartbeat += 1
def handshake(self, leader, follower_name):
try:
follower = self.players[follower_name]
except KeyError:
return False
if follower.opponent is not None:
return False
leader.opponent = follower
leader.is_leader = True
follower.opponent = leader
follower.is_leader = False
follower.handshake_complete = False
# If the opponent GUID is null-or-whitespace then the client
# won't send pong_mp_left. Other than that, the value doesn't
# matter.
follower.connection.send_message("pong_handshake_chosen", "_")
return True
class PongPlayer:
def __init__(self, connection):
self.connection = connection
# We have to store this, because it's used as the key sent to
# the clients to identify each other but it can change at any
# time.
self.name = self.connection.authsession.User.DisplayName
# The heartbeat 'freezes' while the server is waiting for the
# client to finish the handshake. This means that a
# mischievous client can't leave someone else hanging by
# responding to the heartbeats and deliberately ignoring the
# handshake. As a bonus it clears up the connection a little.
self.frozen = False
self.heartbeat()
self.y = 0
self.connected = True
self.connection.factory.pong.join(self)
self.opponent = None
def leave(self):
self.connected = False
self.connection.factory.pong.leave(self)
if self.opponent is not None:
self.opponent.opponent = None
# In the special situation that the follower leaves before
# the handshake is complete, the leader is still fit to
# choose another follower.
if not self.is_leader and not self.handshake_complete:
self.opponent.connection.infobox(f"{self.name} is no longer available.", "Matchmaking failed")
# in all other circumstances, including leader leaving
# part-way through handshake, the other player's state has
# changed and can't be recovered, so Pong has to quit.
else:
self.opponent.connection.send_message("pong_mp_left")
def send_heartbeat(self):
if not self.frozen:
self.connection.send_message("pong_handshake_resendid")
def heartbeat(self):
if not self.frozen:
self.last_heartbeat = self.connection.factory.pong.current_heartbeat
def freeze(self):
self.frozen = True
def unfreeze(self):
self.frozen = False
self.heartbeat()
def complete_handshake(self):
if self.is_leader:
raise ValueError("The handshake can only be completed by a follower")
self.handshake_complete = True
self.opponent.connection.send_message("pong_handshake_complete", "_")
class PongState:
def __init__(self, connection):
self.connection = connection
self.player = None
def busy(self):
return self.player is not None and self.player.connected
def join(self):
if self.busy():
self.connection.infobox("You can't play multiple simultaneous games of online Pong...nobody is that good")
return
self.player = PongPlayer(self.connection)
def leave(self):
if self.busy():
self.player.leave()
def heartbeat(self):
if self.busy():
self.player.heartbeat()
# Note that this is a normal handler. This is sent as a normal
# message only once at the start of matchmaking; after that, it's resent
# as a forward.
@handler("pong_handshake_matchmake")
def pong_handshake_matchmake(connection, contents):
connection.pong.join()
# We send out a pong_handshake_resendid to every matchmaking player
# every 5 seconds as a heartbeat, and a working client will respond with
# a pong_handshake_matchmake immediately.
@forwardhandler("pong_handshake_matchmake")
def pong_handshake_matchmake_forward(connection, contents, name):
connection.pong.heartbeat()
@forwardhandler("pong_handshake_chosen")
def pong_handshake_chosen(connection, contents, name):
if not connection.factory.pong.handshake(connection.pong.player, name):
connection.infobox(f"{name} is no longer available.", "Matchmaking failed")
@forwardhandler("pong_handshake_complete")
def pong_handshake_complete(connection, contents, name):
connection.pong.player.complete_handshake()
@forwardhandler("pong_handshake_left")
def pong_handshake_left(connection, contents, name):
connection.factory.pong.leave(connection.pong.player)
@forwardhandler("pong_mp_setopponenty", int)
def pong_mp_setopponenty(connection, y, name):
try:
assert -2147483648 <= y <= 2147483647
except:
connection.error("Y co-ordinate out of range")
connection.pong.player.y = y
@forwardhandler("pong_mp_setballpos", str)
def pong_mp_setballpos(connection, point, name):
if not connection.pong.player.is_leader:
connection.error("That message can only be used by the leader")
return
try:
x, y = point.split(",")
x = int(x)
y = int(y)
assert -2147483648 <= x <= 2147483647
assert -2147483648 <= y <= 2147483647
except:
connection.error("Invalid Point") # har har
connection.pong.player.opponent.connection.send_message("pong_mp_setballpos", f'"{x},{y}"')
connection.pong.player.opponent.connection.send_message("pong_mp_setopponenty", connection.pong.player.y)
connection.pong.player.connection.send_message("pong_mp_setopponenty", connection.pong.player.opponent.y)
@forwardhandler("pong_mp_left")
def pong_mp_left(connection, contents, name):
connection.pong.leave()
@forwardhandler("pong_mp_youwin")
def pong_mp_youwin(connection, contents, name):
connection.pong.player.opponent.connection.send_message("pong_mp_youwin")
@forwardhandler("pong_mp_youlose")
def pong_mp_youlose(connection, contents, name):
connection.pong.player.opponent.connection.send_message("pong_mp_youlose")
@forwardhandler("pong_mp_cashedout")
def pong_mp_cashedout(connection, contents, name):
# The client is already out of here immediately after sending the
# message.
opponent = connection.pong.player.opponent
if opponent is not None:
opponent.connection.send_message("pong_mp_cashedout")
opponent.opponent = None
connection.pong.player.opponent = None