diff options
| author | Declan Hoare <[email protected]> | 2020-04-16 22:58:21 +1000 |
|---|---|---|
| committer | Declan Hoare <[email protected]> | 2020-04-16 23:10:44 +1000 |
| commit | 7000fce72fbec34c6f4957a59d4146cc7148ee59 (patch) | |
| tree | 5affe93d68a7fbcc6cf85a4d9a3eedecc730d1f7 /pong.py | |
| download | shiftgears-7000fce72fbec34c6f4957a59d4146cc7148ee59.tar.gz shiftgears-7000fce72fbec34c6f4957a59d4146cc7148ee59.tar.bz2 shiftgears-7000fce72fbec34c6f4957a59d4146cc7148ee59.zip | |
Initial Release
Diffstat (limited to 'pong.py')
| -rw-r--r-- | pong.py | 216 |
1 files changed, 216 insertions, 0 deletions
@@ -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 + + |
