diff --git a/NOTE.md b/NOTE.md index e4d1abb..93978c5 100644 --- a/NOTE.md +++ b/NOTE.md @@ -1,12 +1,11 @@ A faire : -- Limite des bateaux, taille des pseudo, ... - -- Nom dans les options - Faire marcher le tchat - Sauvegarde / Quitter +- Historique / Replay - Police d'écriture - Documenter + - Voir si les event listener intégré à pyglet sont plus pratique que l'event propagation (?) - Faire une scène incluant par défaut les boutons "Retour" (?) diff --git a/source/gui/scene/RoomCreate.py b/source/gui/scene/RoomCreate.py index 1f72668..ff4d867 100644 --- a/source/gui/scene/RoomCreate.py +++ b/source/gui/scene/RoomCreate.py @@ -296,14 +296,18 @@ class RoomCreate(Scene): port = int(self.input_port.text) settings = PacketSettings( - username=self.input_username.text, grid_width=int(self.input_width.text), grid_height=int(self.input_height.text), host_start=self.checkbox_host_start.state, boats_length=[size for size, quantity in self.boat_size_amount.items() for _ in range(quantity)] ) - self.window.set_scene(RoomHost, port=port, settings=settings) + self.window.set_scene( + RoomHost, + port=port, + username=self.input_username.text, + settings=settings + ) def on_draw(self): self.batch_input_background.draw() diff --git a/source/gui/scene/RoomHost.py b/source/gui/scene/RoomHost.py index 18b9e1e..39f88f6 100644 --- a/source/gui/scene/RoomHost.py +++ b/source/gui/scene/RoomHost.py @@ -14,9 +14,10 @@ if TYPE_CHECKING: class RoomHost(Scene): - def __init__(self, window: "Window", port: int, settings: "PacketSettings", **kwargs): + def __init__(self, window: "Window", port: int, username: str, settings: "PacketSettings", **kwargs): super().__init__(window, **kwargs) + self.username: str = username self.ip_address: str = "127.0.0.1" self.port: int = port @@ -59,7 +60,13 @@ class RoomHost(Scene): batch=self.batch_label ) - self.thread_network = network.Host(window=self.window, daemon=True, port=self.port, settings=settings) + self.thread_network = network.Host( + window=self.window, + daemon=True, + port=self.port, + username=self.username, + settings=settings + ) self.thread_network.start() self._refresh_ip_text() @@ -70,10 +77,10 @@ class RoomHost(Scene): def _refresh_ip(self): while True: try: - response = requests.get('https://api.ipify.org') + response = requests.get('https://api.ipify.org', timeout=10) response.raise_for_status() break - except requests.HTTPError: + except (requests.HTTPError, requests.Timeout): if self.thread_ip.stopped: return self.ip_address: str = response.content.decode('utf8') diff --git a/source/network/Client.py b/source/network/Client.py index af1e2c5..3671d45 100644 --- a/source/network/Client.py +++ b/source/network/Client.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any from source.gui import scene from source.network import game_network -from source.network.packet import PacketUsername +from source.network.packet import PacketUsername, PacketSettings from source.network.packet.abc import Packet from source.utils import StoppableThread from source.utils.thread import in_pyglet_context @@ -34,8 +34,9 @@ class Client(StoppableThread): print(f"[Client] Connecté avec {connection}") - settings: Any = Packet.from_connection(connection) - PacketUsername(username=self.username).send_connection(connection) + settings: Any = PacketSettings.from_connection(connection) + PacketUsername(username=self.username).instance_send_connection(connection) + packet_username = PacketUsername.from_connection(connection) game_scene = in_pyglet_context( self.window.set_scene, @@ -45,7 +46,7 @@ class Client(StoppableThread): boats_length=settings.boats_length, name_ally=self.username, - name_enemy=settings.username, + name_enemy=packet_username.username, grid_width=settings.grid_width, grid_height=settings.grid_height, my_turn=not settings.host_start diff --git a/source/network/Host.py b/source/network/Host.py index 79982ea..5b0cb2a 100644 --- a/source/network/Host.py +++ b/source/network/Host.py @@ -1,11 +1,11 @@ import socket -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from source.gui import scene from source.network import game_network -from source.network.packet.abc import Packet from source.utils import StoppableThread from source.utils.thread import in_pyglet_context +from source.network.packet import PacketUsername if TYPE_CHECKING: from source.gui.window import Window @@ -17,10 +17,11 @@ class Host(StoppableThread): The thread executed on the person who create a room. """ - def __init__(self, window: "Window", port: int, settings: "PacketSettings", **kw): + def __init__(self, window: "Window", port: int, username: str, settings: "PacketSettings", **kw): super().__init__(**kw) self.window = window + self.username = username self.settings = settings self.port = port @@ -43,8 +44,9 @@ class Host(StoppableThread): print(f"[Serveur] Connecté avec {address}") - self.settings.send_connection(connection) - packet_username: Any = Packet.from_connection(connection) + self.settings.instance_send_connection(connection) + packet_username = PacketUsername.from_connection(connection) + PacketUsername(username=self.username).instance_send_connection(connection) game_scene = in_pyglet_context( self.window.set_scene, @@ -53,7 +55,7 @@ class Host(StoppableThread): connection=connection, boats_length=self.settings.boats_length, - name_ally=self.settings.username, + name_ally=self.username, name_enemy=packet_username.username, grid_width=self.settings.grid_width, grid_height=self.settings.grid_height, diff --git a/source/network/game_network.py b/source/network/game_network.py index c2a2998..e2445c0 100644 --- a/source/network/game_network.py +++ b/source/network/game_network.py @@ -1,5 +1,5 @@ import socket -from typing import Any +from typing import Type, Callable from source.gui.scene import Game from source.network.packet.abc import Packet @@ -21,7 +21,7 @@ def game_network( :param connection: the connection with the other player """ - game_methods = { + game_methods: dict[Type["Packet"], Callable] = { packet.PacketChat: game_scene.network_on_chat, packet.PacketBoatPlaced: game_scene.network_on_boat_placed, packet.PacketBombPlaced: game_scene.network_on_bomb_placed, @@ -29,13 +29,15 @@ def game_network( } while True: - data: Any = Packet.from_connection(connection) + data_type: Type["Packet"] = Packet.type_from_connection(connection) - if data is None: + if data_type is None: if thread.stopped: return # vérifie si le thread n'est pas censé s'arrêter continue + data = data_type.from_connection(connection) + if in_pyglet_context( - game_methods[type(data)], # récupère la methode relié ce type de donnée + game_methods[data_type], # récupère la methode relié ce type de donnée connection, data ): return # Appelle la méthode. Si elle renvoie True, arrête le thread diff --git a/source/network/packet/PacketBoatPlaced.py b/source/network/packet/PacketBoatPlaced.py index c1743c2..3af2f82 100644 --- a/source/network/packet/PacketBoatPlaced.py +++ b/source/network/packet/PacketBoatPlaced.py @@ -9,8 +9,6 @@ class PacketBoatPlaced(Packet): A packet that signal that all the boat of the player have been placed """ - packet_size: int = 0 - def to_bytes(self): return b"" diff --git a/source/network/packet/PacketBombPlaced.py b/source/network/packet/PacketBombPlaced.py index cf23dc3..7e30149 100644 --- a/source/network/packet/PacketBombPlaced.py +++ b/source/network/packet/PacketBombPlaced.py @@ -14,7 +14,6 @@ class PacketBombPlaced(Packet): position: Point2D = field() - packet_size: int = 2 packet_format: str = ">BB" def to_bytes(self): diff --git a/source/network/packet/PacketBombState.py b/source/network/packet/PacketBombState.py index 59135aa..cb1fc79 100644 --- a/source/network/packet/PacketBombState.py +++ b/source/network/packet/PacketBombState.py @@ -15,7 +15,6 @@ class PacketBombState(Packet): position: Point2D = field() bomb_state: BombState = field() - packet_size: int = 3 packet_format: str = ">BBb" def to_bytes(self): diff --git a/source/network/packet/PacketChat.py b/source/network/packet/PacketChat.py index 6ed7b05..014b573 100644 --- a/source/network/packet/PacketChat.py +++ b/source/network/packet/PacketChat.py @@ -1,4 +1,7 @@ +import socket +import struct from dataclasses import dataclass, field +from typing import Optional from source.network.packet.abc import Packet @@ -11,11 +14,27 @@ class PacketChat(Packet): message: str = field() - packet_size: int = 256 + packet_format = ">I" def to_bytes(self) -> bytes: - return self.message.encode("utf-8") + message: bytes = self.message.encode("utf-8") + message_len: int = len(message) + + # envoie la taille du message, suivi des données du message + return struct.pack(f"{self.packet_format}{message_len}s", message_len, message) @classmethod - def from_bytes(cls, data: bytes): - return cls(message=data.decode("utf-8")) + def from_connection(cls, connection: socket.socket) -> "Packet": + message_len, *_ = struct.unpack( + cls.packet_format, + connection.recv(struct.calcsize(cls.packet_format)) + ) + + format_: str = f">{message_len}s" + + message, *_ = struct.unpack( + format_, + connection.recv(struct.calcsize(format_)) + ) + + return cls(message=message.decode("utf-8")) diff --git a/source/network/packet/PacketSettings.py b/source/network/packet/PacketSettings.py index 4572f13..c0d75fa 100644 --- a/source/network/packet/PacketSettings.py +++ b/source/network/packet/PacketSettings.py @@ -1,3 +1,4 @@ +import socket import struct from dataclasses import dataclass, field @@ -6,37 +7,45 @@ from source.network.packet.abc import Packet @dataclass class PacketSettings(Packet): - username: str = field() grid_width: int = field() grid_height: int = field() host_start: bool = field() boats_length: list = field() - packet_size: int = 51 - packet_format: str = ">16sBB?32B" + packet_format: str = ">BB?I" def to_bytes(self): - boat_size = self.boats_length + ([0] * (32 - len(self.boats_length))) + boats_len: int = len(self.boats_length) return struct.pack( - self.packet_format, + f"{self.packet_format}{boats_len}B", - self.username.encode("utf-8"), self.grid_width, self.grid_height, self.host_start, - *boat_size + boats_len, + + *self.boats_length ) @classmethod - def from_bytes(cls, data: bytes): - username, grid_width, grid_height, host_start, *boats_length = struct.unpack(cls.packet_format, data) + def from_connection(cls, connection: socket.socket) -> "Packet": + grid_width, grid_height, host_start, boats_len = struct.unpack( + cls.packet_format, + connection.recv(struct.calcsize(cls.packet_format)) + ) + + format_ = f">{boats_len}B" + + boats_length = struct.unpack( + format_, + connection.recv(struct.calcsize(format_)) + ) return cls( - username=username.replace(b"\x00", b"").decode("utf-8"), grid_width=grid_width, grid_height=grid_height, host_start=host_start, - boats_length=list(filter(lambda value: value != 0, boats_length)) + boats_length=list(boats_length) ) diff --git a/source/network/packet/PacketUsername.py b/source/network/packet/PacketUsername.py index d4ae651..93767dc 100644 --- a/source/network/packet/PacketUsername.py +++ b/source/network/packet/PacketUsername.py @@ -1,3 +1,5 @@ +import socket +import struct from dataclasses import dataclass, field from source.network.packet.abc import Packet @@ -7,11 +9,26 @@ from source.network.packet.abc import Packet class PacketUsername(Packet): username: str = field() - packet_size: int = 16 + packet_format: str = ">I" def to_bytes(self): - return self.username.encode("utf-8") + username = self.username.encode() + username_len = len(username) + + return struct.pack(f"{self.packet_format}{username_len}s", username_len, username) @classmethod - def from_bytes(cls, data: bytes): - return cls(username=data.decode("utf-8")) + def from_connection(cls, connection: socket.socket) -> "PacketUsername": + username_len, *_ = struct.unpack( + cls.packet_format, + connection.recv(struct.calcsize(cls.packet_format)) + ) + + format_: str = f">{username_len}s" + + username, *_ = struct.unpack( + format_, + connection.recv(struct.calcsize(format_)) + ) + + return cls(username=username.decode("utf-8")) diff --git a/source/network/packet/abc/Packet.py b/source/network/packet/abc/Packet.py index 9b4e345..f622bc3 100644 --- a/source/network/packet/abc/Packet.py +++ b/source/network/packet/abc/Packet.py @@ -1,14 +1,13 @@ import socket +import struct from abc import abstractmethod, ABC from typing import Type, Optional -# TODO: struct.calcsize() au lieu de packet_size - class Packet(ABC): packed_header: bytes - packet_size: int packet_id: int = 0 + packet_format: str = ">" def __init_subclass__(cls, **kwargs): cls.packet_header = Packet.packet_id.to_bytes(1, "big") # give a header to the newly created subclass @@ -22,18 +21,22 @@ class Packet(ABC): """ pass - @classmethod - @abstractmethod - def from_bytes(cls, data: bytes) -> "Packet": + def send_connection(self, connection: socket.socket): """ - Convert a bytes object into a packet. - :param data: the data to convert into a packet. Should be "packet_size" long. - :return: a packet corresponding to the bytes. + Send the packet directly into a socket. + :param connection: the socket where to send the packet to. """ - pass + connection.send(self.packet_header + self.to_bytes()) + + def instance_send_connection(self, connection: socket.socket): + """ + Send the packet directly into a socket. + :param connection: the socket where to send the packet to. + """ + connection.send(self.to_bytes()) @classmethod - def cls_from_header(cls, packet_header: bytes) -> Type["Packet"]: + def type_from_header(cls, packet_header: bytes) -> Type["Packet"]: """ Get a subclass from its packet header. :param packet_header: the header to find the corresponding subclass @@ -44,31 +47,32 @@ class Packet(ABC): cls.__subclasses__() )) - def send_connection(self, connection: socket.socket): + @classmethod + def from_bytes(cls, data: bytes) -> "Packet": """ - Send the packet directly into a socket. - :param connection: the socket where to send the packet to. + Convert a bytes object into a packet. + :param data: the data to convert into a packet. Should be "packet_size" long. + :return: a packet corresponding to the bytes. """ - connection.send(self.packet_header + self.to_bytes()) + pass @classmethod - def from_connection(cls, connection: socket.socket) -> Optional["Packet"]: + def type_from_connection(cls, connection: socket.socket) -> Optional[Type["Packet"]]: + try: + packet_header = connection.recv(1) + except socket.timeout: + return None + + if not packet_header: return None # ignore si le header est invalide + return cls.type_from_header(packet_header) + + @classmethod + def from_connection(cls, connection: socket.socket) -> "Packet": """ Receive a packet from a socket. :param connection: the socket where to get the data from :return: the packet, or None if there was nothing in the socket to receive. """ - # get the packet type - - packet_header: Optional[bytes] = None - try: - packet_header = connection.recv(1) - except socket.timeout: - pass - - if not packet_header: return None # ignore si le header est invalide - packet_type = cls.cls_from_header(packet_header) - - # renvoie les données instanciées - return packet_type.from_bytes(connection.recv(packet_type.packet_size)) + packet_size: int = struct.calcsize(cls.packet_format) + return cls.from_bytes(connection.recv(packet_size))