aboutsummaryrefslogtreecommitdiff
path: root/pong.py
diff options
context:
space:
mode:
Diffstat (limited to 'pong.py')
-rw-r--r--pong.py216
1 files changed, 216 insertions, 0 deletions
diff --git a/pong.py b/pong.py
new file mode 100644
index 0000000..0a8307c
--- /dev/null
+++ b/pong.py
@@ -0,0 +1,216 @@
+# 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
+
+