mirror of
https://github.com/DeclanHoare/shiftgears.git
synced 2025-01-22 00:01:55 -05:00
216 lines
7.3 KiB
Python
216 lines
7.3 KiB
Python
# 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
|
|
|
|
|