# HG changeset patch # User Emmanuel Gil Peyrot # Date 1380301307 -7200 # Node ID 2f53be1b2f60e0254a0c25668eeec6b47dd5075f # Parent ca22df9e70bc31ae830d975db5f3a279bed71e43# Parent c099802e24354c6384e50188011bee4ebf6f558e Merge netplay branch. diff --git a/eosd b/eosd --- a/eosd +++ b/eosd @@ -46,6 +46,8 @@ parser.add_argument('--no-music', action parser.add_argument('--hints', metavar='HINTS', default=None, help='Hints file, to display text while playing.') parser.add_argument('--verbosity', metavar='VERBOSITY', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Select the wanted logging level.') parser.add_argument('--game', metavar='GAME', choices=['EoSD'], default='EoSD', help='Select the game engine to use.') +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() @@ -62,6 +64,7 @@ from pytouhou.formats.t6rp import T6RP, from pytouhou.utils.random import Random from pytouhou.vm.msgrunner import NextStage from pytouhou.formats.hint import Hint +from pytouhou.network import Network if args.game == 'EoSD': @@ -98,7 +101,7 @@ class GameBossRush(Game): def main(window, path, data, stage_num, rank, character, replay, save_filename, skip_replay, boss_rush, debug, enable_background, enable_particles, - enable_music, hints, verbosity): + enable_music, hints, verbosity, port, remote): resource_loader = Loader(path) @@ -136,6 +139,22 @@ def main(window, path, data, stage_num, save_replay.rank = rank save_replay.character = character + if port != 0: + players = [PlayerState(character=0), PlayerState(character=2)] + + if remote: + remote_addr, remote_port = remote.split(':') + addr = remote_addr, int(remote_port) + selected_player = 0 + else: + addr = None + selected_player = 1 + + prng = Random(0) + con = Network(port, addr, selected_player) + else: + con = None + if hints: with open(hints, 'rb') as file: hints = Hint.read(file) @@ -147,7 +166,7 @@ def main(window, path, data, stage_num, game_class = GameBossRush if boss_rush else Game common = Common(resource_loader) - runner = GameRunner(window, resource_loader, skip=skip_replay) + runner = GameRunner(window, resource_loader, skip=skip_replay, con=con) while True: if replay: level = replay.levels[stage_num - 1] @@ -167,7 +186,7 @@ def main(window, path, data, stage_num, states[0].lives = level.lives states[0].bombs = level.bombs difficulty = level.difficulty - else: + elif port == 0: prng = Random() if save_filename: @@ -231,7 +250,7 @@ with SDL(): main(window, args.path, tuple(args.data), args.stage, args.rank, args.character, args.replay, args.save_replay, args.skip_replay, args.boss_rush, args.debug, args.no_background, args.no_particles, - args.no_music, args.hints, args.verbosity) + args.no_music, args.hints, args.verbosity, args.port, args.remote) import gc gc.collect() diff --git a/pytouhou/game/enemy.pyx b/pytouhou/game/enemy.pyx --- a/pytouhou/game/enemy.pyx +++ b/pytouhou/game/enemy.pyx @@ -213,7 +213,7 @@ cdef class Enemy(Element): cpdef Player select_player(self, list players=None): if players is None: players = self._game.players - return players[0] #TODO + return min(players, key=self.select_player_key) cpdef double get_player_angle(self, tuple pos=None, Player player=None): @@ -540,3 +540,6 @@ cdef class Enemy(Element): self.frame += 1 + + def select_player_key(self, p): + return ((p.x - self.x) ** 2 + (p.y - self.y) ** 2, p.state.character) diff --git a/pytouhou/game/game.pxd b/pytouhou/game/game.pxd --- a/pytouhou/game/game.pxd +++ b/pytouhou/game/game.pxd @@ -34,11 +34,11 @@ cdef class Game: cpdef NativeText new_native_text(self, tuple pos, unicode text, align=*) cpdef Text new_hint(self, hint) cpdef new_face(self, side, effect) - cpdef run_iter(self, long keystate) + cpdef run_iter(self, list keystates) cdef void update_background(self) except * cdef void update_enemies(self) except * cdef void update_msg(self, long keystate) except * - cdef void update_players(self, long keystate) except * + cdef void update_players(self, list keystates) except * cdef void update_effects(self) except * cdef void update_hints(self) except * cdef void update_faces(self) except * diff --git a/pytouhou/game/game.pyx b/pytouhou/game/game.pyx --- a/pytouhou/game/game.pyx +++ b/pytouhou/game/game.pyx @@ -164,7 +164,7 @@ cdef class Game: cdef Bullet bullet cdef Laser laser - player = self.players[0] #TODO + player = min(self.players, key=select_player_key) item_type = self.item_types[6] self.items.extend([Item((bullet.x, bullet.y), 6, item_type, self, player=player) for bullet in self.bullets]) @@ -259,8 +259,9 @@ cdef class Game: return face - cpdef run_iter(self, long keystate): + cpdef run_iter(self, list keystates): cdef Laser laser + # 1. VMs. for runner in self.ecl_runners: runner.run_iter() @@ -283,9 +284,10 @@ cdef class Game: # Pri 6 is background self.update_background() #TODO: Pri unknown if self.msg_runner is not None: - self.update_msg(keystate) # Pri ? - keystate &= ~3 # Remove the ability to attack (keystates 1 and 2). - self.update_players(keystate) # Pri 7 + self.update_msg(keystates[0]) # Pri ? + for i in xrange(len(keystates)): + keystates[i] &= ~3 # Remove the ability to attack (keystates 1 and 2). + self.update_players(keystates) # Pri 7 self.update_enemies() # Pri 9 self.update_effects() # Pri 10 self.update_bullets() # Pri 11 @@ -337,9 +339,10 @@ cdef class Game: self.msg_runner.run_iteration() - cdef void update_players(self, long keystate): + cdef void update_players(self, list keystates): cdef Bullet bullet cdef Player player + cdef long keystate if self.time_stop: return @@ -347,13 +350,13 @@ cdef class Game: for bullet in self.players_bullets: bullet.update() - for player in self.players: + for player, keystate in zip(self.players, keystates): player.update(keystate) #TODO: differentiate keystates (multiplayer mode) - #XXX: Why 78910? Is it really the right value? - player.state.effective_score = min(player.state.effective_score + 78910, - player.state.score) - #TODO: give extra lives to the player + #XXX: Why 78910? Is it really the right value? + player.state.effective_score = min(player.state.effective_score + 78910, + player.state.score) + #TODO: give extra lives to the player cdef void update_effects(self): @@ -540,6 +543,7 @@ cdef class Game: if self.boss and self.boss._enemy.removed: self.boss = None + cdef list filter_removed(list elements): cdef Element element @@ -548,3 +552,7 @@ cdef list filter_removed(list elements): if not element.removed: filtered.append(element) return filtered + + +def select_player_key(player): + return (player.state.score, player.state.character) diff --git a/pytouhou/network.py b/pytouhou/network.py new file mode 100644 --- /dev/null +++ b/pytouhou/network.py @@ -0,0 +1,90 @@ +import socket +import struct +from select import select +import time + +MSG_STRUCT = struct.Struct('!HHH') + +class Network(object): + def __init__(self, port=8080, dest=None, selected_player=0): + self.frame = 0 + self.keystate = 0 + self.old_keystate = 0 + self.remote_keystate = 0 + + self.selected_player = selected_player + + self.remote_addr = dest + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + self.sock.bind(('', port)) + + + def read_message(self): + message = None + + start_time = time.time() + delta = 1./60. + + rlist, wlist, xlist = select([self.sock], [], [], delta) + while rlist: + msg, addr = rlist[0].recvfrom(MSG_STRUCT.size) + # Check whether the message comes from the right address + if self.frame == 0 or addr == self.remote_addr: + self.remote_addr = addr + + frame, keystate, old_keystate = MSG_STRUCT.unpack(msg) + + # Check for well-formedness + if frame in (self.frame, self.frame + 1): + message = (frame, keystate, old_keystate) + else: + print('Mismatch', self.remote_addr, addr) + + # If no valid message has been read, wait for one as long as possible + # else, read as much as we can without blocking. + delta = 0 if message else max(0, 1./60. - (time.time() - start_time)) + rlist, wlist, xlist = select(rlist, [], [], delta) + + return message + + + def send_message(self): + frame, keystate, old_keystate = self.frame, self.keystate, self.old_keystate + if self.remote_addr is not None: + self.sock.sendto(MSG_STRUCT.pack(frame, keystate, old_keystate), self.remote_addr) + + + def run_game_iter(self, game, keystate, other_keystate): + keystates = [other_keystate, other_keystate] + keystates[self.selected_player] = keystate + game.run_iter(keystates) + + + def run_iter(self, game, keystate): + if game.frame % 3 == 0: + # Phase 1: Update game with old data + self.run_game_iter(game, self.keystate, self.remote_keystate) + elif game.frame % 3 == 1: + # Phase 2: Update data, send new data, update game with old data + self.old_keystate, self.keystate = self.keystate, keystate + self.frame = game.frame // 3 + self.send_message() + self.run_game_iter(game, self.old_keystate, self.remote_keystate) + elif game.frame % 3 == 2: + # Phase 3: Send new data, get remote data, update game with new data + self.send_message() + # Follow one valid update + message = self.read_message() + if message: + frame, keystate, old_keystate = message + if frame == self.frame: + self.remote_keystate = keystate + elif frame == self.frame + 1: + self.remote_keystate = old_keystate + else: + raise Exception #TODO + self.run_game_iter(game, self.keystate, self.remote_keystate) + elif game.frame > 2: + print('ARGH') + + diff --git a/pytouhou/ui/gamerunner.pyx b/pytouhou/ui/gamerunner.pyx --- a/pytouhou/ui/gamerunner.pyx +++ b/pytouhou/ui/gamerunner.pyx @@ -20,20 +20,20 @@ from .music import MusicPlayer, SFXPlaye cdef class GameRunner(Runner): - cdef object game, background + cdef object game, background, con cdef GameRenderer renderer cdef Window window cdef object replay_level, save_keystates - cdef long keystate cdef bint skip - def __init__(self, Window window, resource_loader, bint skip=False): + def __init__(self, Window window, resource_loader, bint skip=False, + con=None): self.renderer = GameRenderer(resource_loader, window.use_fixed_pipeline) self.window = window self.replay_level = None self.skip = skip - self.keystate = 0 + self.con = con self.width = window.width #XXX self.height = window.height #XXX @@ -134,7 +134,11 @@ cdef class GameRunner(Runner): if self.save_keystates is not None: self.save_keystates.append(keystate) - self.game.run_iter(keystate) + if self.con: + self.con.run_iter(self.game, keystate) + else: + self.game.run_iter([keystate]) + self.game.interface.labels['framerate'].set_text('%.2ffps' % self.window.get_fps()) if not self.skip: self.renderer.render(self.game, self.window)