changeset 475:9d4d52793eca

Experimental netplay! Yay!
author Thibaut Girka <thib@sitedethib.com>
date Wed, 28 Dec 2011 18:45:09 +0100
parents 8f4cd1c01d22
children 44c5e7d4b615
files eosd pytouhou/game/game.py pytouhou/network.py pytouhou/ui/gamerunner.py
diffstat 4 files changed, 98 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/eosd
+++ b/eosd
@@ -27,7 +27,9 @@ from pytouhou.game.player import PlayerS
 from pytouhou.formats.t6rp import T6RP
 
 
-def main(path, stage_num, rank, character, replay, data):
+def main(path, stage_num, rank, character, replay, data, port, remote):
+    players = [PlayerState(character=character)]
+
     if replay:
         with open(replay, 'rb') as file:
             replay = T6RP.read(file)
@@ -40,10 +42,23 @@ def main(path, stage_num, rank, characte
     else:
         prng = None
 
+    if port != 0:
+        from pytouhou.network import Network
+        if remote:
+            remote_addr, remote_port = remote.split(':')
+            addr = remote_addr, int(remote_port)
+        else:
+            addr = None
+
+        players.append(PlayerState(character=character))
+        con = Network(port, addr)
+    else:
+        con = None
+
     resource_loader = Loader()
     resource_loader.scan_archives(os.path.join(path, name)
                                     for name in data)
-    game = EoSDGame(resource_loader, [PlayerState(character=character)], stage_num, rank, 16,
+    game = EoSDGame(resource_loader, players, stage_num, rank, 16,
                     prng=prng)
 
     # Load stage data
@@ -53,7 +68,7 @@ def main(path, stage_num, rank, characte
     background = Background(stage, background_anm_wrapper)
 
     # Main loop
-    runner = GameRunner(resource_loader, game, background, replay=replay)
+    runner = GameRunner(resource_loader, game, background, replay=replay, con=con)
     runner.start()
 
 
@@ -65,7 +80,9 @@ parser.add_argument('-s', '--stage', met
 parser.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
 parser.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
 parser.add_argument('--replay', metavar='REPLAY', help='Select a replay')
+parser.add_argument('--port', metavar='PORT', type=int, default=0, help='Port to use for netplay')
+parser.add_argument('--remote', metavar='REMOTE', default=None, help='Remote address')
 
 args = parser.parse_args()
 
-main(args.path, args.stage, args.rank, args.character, args.replay, tuple(args.data))
+main(args.path, args.stage, args.rank, args.character, args.replay, tuple(args.data), args.port, args.remote)
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -131,7 +131,7 @@ class Game(object):
         return enemy
 
 
-    def run_iter(self, keystate):
+    def run_iter(self, keystates):
         # 1. VMs.
         self.ecl_runner.run_iter()
         if self.frame % (32*60) == (32*60): #TODO: check if that is really that frame.
@@ -151,7 +151,7 @@ class Game(object):
 
         # Pri 6 is background
         self.update_effect() #TODO: Pri unknown
-        self.update_players(keystate) # Pri 7
+        self.update_players(keystates) # Pri 7
         self.update_enemies() # Pri 9
         self.update_effects() # Pri 10
         self.update_bullets() # Pri 11
@@ -173,8 +173,8 @@ class Game(object):
             enemy.update()
 
 
-    def update_players(self, keystate):
-        for player in self.players:
+    def update_players(self, keystates):
+        for player, keystate in zip(self.players, keystates):
             player.update(keystate) #TODO: differentiate keystates (multiplayer mode)
             if player.state.x < 8.:
                 player.state.x = 8.
new file mode 100644
--- /dev/null
+++ b/pytouhou/network.py
@@ -0,0 +1,66 @@
+import socket
+import struct
+from select import select
+
+MSG_STRUCT = struct.Struct('!IHHB')
+
+class Network(object):
+    def __init__(self, port=8080, dest=None):
+        self.frame = 0
+        self.keystate = 0
+        self.old_keystate = 0
+
+        self.remote_addr = dest
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+        self.sock.setblocking(0)
+        self.sock.bind(('', port))
+
+
+    def read_messages(self):
+        messages = []
+
+        rlist, wlist, xlist = select([self.sock], [], [], 0)
+        while rlist:
+            msg, addr = rlist[0].recvfrom(MSG_STRUCT.size)
+            # Check whether the message comes from the right address
+            if self.remote_addr is None or addr == self.remote_addr:
+                self.remote_addr = addr
+
+                frame, keystate, old_keystate, checksum = MSG_STRUCT.unpack(msg)
+
+                # Check for well-formedness
+                if checksum == sum(ord(c) for c in msg[:-1]) & 0xFF:
+                    messages.append((frame, keystate, old_keystate, checksum))
+            else:
+                print('Mismatch', self.remote_addr, addr)
+
+            rlist, wlist, xlist = select(rlist, [], [], 0)
+
+        return messages
+
+
+    def send_message(self):
+        frame, keystate, old_keystate = self.frame, self.keystate, self.old_keystate
+        if self.remote_addr is not None:
+            checksum = frame + (frame >> 8) + (frame >> 16) + (frame >> 24)
+            checksum += keystate + (keystate >> 8)
+            checksum += old_keystate + (old_keystate >> 8)
+            checksum &= 0xFF
+            self.sock.sendto(MSG_STRUCT.pack(frame, keystate, old_keystate, checksum), self.remote_addr)
+
+
+    def run_iter(self, game, keystate):
+        if self.frame < game.frame:
+            self.old_keystate, self.keystate = self.keystate, keystate
+            self.frame = game.frame
+
+        for frame, keystate, old_keystate, checksum in self.read_messages():
+            if frame == game.frame:
+                game.run_iter([self.keystate, keystate])
+            elif frame == game.frame + 1:
+                print('Skipped')
+                game.run_iter([self.keystate, old_keystate])
+                game.run_iter([self.keystate, keystate])
+
+        self.send_message()
+
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.py
@@ -32,13 +32,15 @@ logger = get_logger(__name__)
 
 
 class GameRunner(pyglet.window.Window, GameRenderer):
-    def __init__(self, resource_loader, game=None, background=None, replay=None):
+    def __init__(self, resource_loader, game=None, background=None, replay=None,
+                 con=None):
         GameRenderer.__init__(self, resource_loader, game, background)
 
         width, height = (game.width, game.height) if game else (None, None)
         pyglet.window.Window.__init__(self, width=width, height=height,
                                       caption='PyTouhou', resizable=False)
 
+        self.con = con
         self.replay_level = None
         if not replay or not replay.levels[game.stage-1]:
             self.keys = pyglet.window.key.KeyStateHandler()
@@ -125,7 +127,6 @@ class GameRunner(pyglet.window.Window, G
                     keystate |= 128
                 if self.keys[pyglet.window.key.LCTRL]:
                     keystate |= 256
-                self.game.run_iter(keystate)
             else:
                 keystate = 0
                 for frame, _keystate, unknown in self.replay_level.keys:
@@ -134,7 +135,10 @@ class GameRunner(pyglet.window.Window, G
                     else:
                         keystate = _keystate
 
-                self.game.run_iter(keystate)
+            if self.con:
+                self.con.run_iter(self.game, keystate)
+            else:
+                self.game.run_iter([keystate])
 
 
     def on_draw(self):