changed the packet code to be simpler and more flexible

This commit is contained in:
Faraphel 2023-02-28 14:07:00 +01:00
parent 58287a0649
commit 37350926b2
13 changed files with 136 additions and 76 deletions

View file

@ -1,12 +1,11 @@
A faire : A faire :
- Limite des bateaux, taille des pseudo, ...
- Nom dans les options
- Faire marcher le tchat - Faire marcher le tchat
- Sauvegarde / Quitter - Sauvegarde / Quitter
- Historique / Replay
- Police d'écriture - Police d'écriture
- Documenter - Documenter
- Voir si les event listener intégré à pyglet sont plus pratique que l'event propagation (?) - 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" (?) - Faire une scène incluant par défaut les boutons "Retour" (?)

View file

@ -296,14 +296,18 @@ class RoomCreate(Scene):
port = int(self.input_port.text) port = int(self.input_port.text)
settings = PacketSettings( settings = PacketSettings(
username=self.input_username.text,
grid_width=int(self.input_width.text), grid_width=int(self.input_width.text),
grid_height=int(self.input_height.text), grid_height=int(self.input_height.text),
host_start=self.checkbox_host_start.state, host_start=self.checkbox_host_start.state,
boats_length=[size for size, quantity in self.boat_size_amount.items() for _ in range(quantity)] 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): def on_draw(self):
self.batch_input_background.draw() self.batch_input_background.draw()

View file

@ -14,9 +14,10 @@ if TYPE_CHECKING:
class RoomHost(Scene): 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) super().__init__(window, **kwargs)
self.username: str = username
self.ip_address: str = "127.0.0.1" self.ip_address: str = "127.0.0.1"
self.port: int = port self.port: int = port
@ -59,7 +60,13 @@ class RoomHost(Scene):
batch=self.batch_label 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.thread_network.start()
self._refresh_ip_text() self._refresh_ip_text()
@ -70,10 +77,10 @@ class RoomHost(Scene):
def _refresh_ip(self): def _refresh_ip(self):
while True: while True:
try: try:
response = requests.get('https://api.ipify.org') response = requests.get('https://api.ipify.org', timeout=10)
response.raise_for_status() response.raise_for_status()
break break
except requests.HTTPError: except (requests.HTTPError, requests.Timeout):
if self.thread_ip.stopped: return if self.thread_ip.stopped: return
self.ip_address: str = response.content.decode('utf8') self.ip_address: str = response.content.decode('utf8')

View file

@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any
from source.gui import scene from source.gui import scene
from source.network import game_network 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.network.packet.abc import Packet
from source.utils import StoppableThread from source.utils import StoppableThread
from source.utils.thread import in_pyglet_context from source.utils.thread import in_pyglet_context
@ -34,8 +34,9 @@ class Client(StoppableThread):
print(f"[Client] Connecté avec {connection}") print(f"[Client] Connecté avec {connection}")
settings: Any = Packet.from_connection(connection) settings: Any = PacketSettings.from_connection(connection)
PacketUsername(username=self.username).send_connection(connection) PacketUsername(username=self.username).instance_send_connection(connection)
packet_username = PacketUsername.from_connection(connection)
game_scene = in_pyglet_context( game_scene = in_pyglet_context(
self.window.set_scene, self.window.set_scene,
@ -45,7 +46,7 @@ class Client(StoppableThread):
boats_length=settings.boats_length, boats_length=settings.boats_length,
name_ally=self.username, name_ally=self.username,
name_enemy=settings.username, name_enemy=packet_username.username,
grid_width=settings.grid_width, grid_width=settings.grid_width,
grid_height=settings.grid_height, grid_height=settings.grid_height,
my_turn=not settings.host_start my_turn=not settings.host_start

View file

@ -1,11 +1,11 @@
import socket import socket
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
from source.gui import scene from source.gui import scene
from source.network import game_network from source.network import game_network
from source.network.packet.abc import Packet
from source.utils import StoppableThread from source.utils import StoppableThread
from source.utils.thread import in_pyglet_context from source.utils.thread import in_pyglet_context
from source.network.packet import PacketUsername
if TYPE_CHECKING: if TYPE_CHECKING:
from source.gui.window import Window from source.gui.window import Window
@ -17,10 +17,11 @@ class Host(StoppableThread):
The thread executed on the person who create a room. 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) super().__init__(**kw)
self.window = window self.window = window
self.username = username
self.settings = settings self.settings = settings
self.port = port self.port = port
@ -43,8 +44,9 @@ class Host(StoppableThread):
print(f"[Serveur] Connecté avec {address}") print(f"[Serveur] Connecté avec {address}")
self.settings.send_connection(connection) self.settings.instance_send_connection(connection)
packet_username: Any = Packet.from_connection(connection) packet_username = PacketUsername.from_connection(connection)
PacketUsername(username=self.username).instance_send_connection(connection)
game_scene = in_pyglet_context( game_scene = in_pyglet_context(
self.window.set_scene, self.window.set_scene,
@ -53,7 +55,7 @@ class Host(StoppableThread):
connection=connection, connection=connection,
boats_length=self.settings.boats_length, boats_length=self.settings.boats_length,
name_ally=self.settings.username, name_ally=self.username,
name_enemy=packet_username.username, name_enemy=packet_username.username,
grid_width=self.settings.grid_width, grid_width=self.settings.grid_width,
grid_height=self.settings.grid_height, grid_height=self.settings.grid_height,

View file

@ -1,5 +1,5 @@
import socket import socket
from typing import Any from typing import Type, Callable
from source.gui.scene import Game from source.gui.scene import Game
from source.network.packet.abc import Packet from source.network.packet.abc import Packet
@ -21,7 +21,7 @@ def game_network(
:param connection: the connection with the other player :param connection: the connection with the other player
""" """
game_methods = { game_methods: dict[Type["Packet"], Callable] = {
packet.PacketChat: game_scene.network_on_chat, packet.PacketChat: game_scene.network_on_chat,
packet.PacketBoatPlaced: game_scene.network_on_boat_placed, packet.PacketBoatPlaced: game_scene.network_on_boat_placed,
packet.PacketBombPlaced: game_scene.network_on_bomb_placed, packet.PacketBombPlaced: game_scene.network_on_bomb_placed,
@ -29,13 +29,15 @@ def game_network(
} }
while True: 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 if thread.stopped: return # vérifie si le thread n'est pas censé s'arrêter
continue continue
data = data_type.from_connection(connection)
if in_pyglet_context( 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 connection, data
): return # Appelle la méthode. Si elle renvoie True, arrête le thread ): return # Appelle la méthode. Si elle renvoie True, arrête le thread

View file

@ -9,8 +9,6 @@ class PacketBoatPlaced(Packet):
A packet that signal that all the boat of the player have been placed A packet that signal that all the boat of the player have been placed
""" """
packet_size: int = 0
def to_bytes(self): def to_bytes(self):
return b"" return b""

View file

@ -14,7 +14,6 @@ class PacketBombPlaced(Packet):
position: Point2D = field() position: Point2D = field()
packet_size: int = 2
packet_format: str = ">BB" packet_format: str = ">BB"
def to_bytes(self): def to_bytes(self):

View file

@ -15,7 +15,6 @@ class PacketBombState(Packet):
position: Point2D = field() position: Point2D = field()
bomb_state: BombState = field() bomb_state: BombState = field()
packet_size: int = 3
packet_format: str = ">BBb" packet_format: str = ">BBb"
def to_bytes(self): def to_bytes(self):

View file

@ -1,4 +1,7 @@
import socket
import struct
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from source.network.packet.abc import Packet from source.network.packet.abc import Packet
@ -11,11 +14,27 @@ class PacketChat(Packet):
message: str = field() message: str = field()
packet_size: int = 256 packet_format = ">I"
def to_bytes(self) -> bytes: 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 @classmethod
def from_bytes(cls, data: bytes): def from_connection(cls, connection: socket.socket) -> "Packet":
return cls(message=data.decode("utf-8")) 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"))

View file

@ -1,3 +1,4 @@
import socket
import struct import struct
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -6,37 +7,45 @@ from source.network.packet.abc import Packet
@dataclass @dataclass
class PacketSettings(Packet): class PacketSettings(Packet):
username: str = field()
grid_width: int = field() grid_width: int = field()
grid_height: int = field() grid_height: int = field()
host_start: bool = field() host_start: bool = field()
boats_length: list = field() boats_length: list = field()
packet_size: int = 51 packet_format: str = ">BB?I"
packet_format: str = ">16sBB?32B"
def to_bytes(self): 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( return struct.pack(
self.packet_format, f"{self.packet_format}{boats_len}B",
self.username.encode("utf-8"),
self.grid_width, self.grid_width,
self.grid_height, self.grid_height,
self.host_start, self.host_start,
*boat_size boats_len,
*self.boats_length
) )
@classmethod @classmethod
def from_bytes(cls, data: bytes): def from_connection(cls, connection: socket.socket) -> "Packet":
username, grid_width, grid_height, host_start, *boats_length = struct.unpack(cls.packet_format, data) 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( return cls(
username=username.replace(b"\x00", b"").decode("utf-8"),
grid_width=grid_width, grid_width=grid_width,
grid_height=grid_height, grid_height=grid_height,
host_start=host_start, host_start=host_start,
boats_length=list(filter(lambda value: value != 0, boats_length)) boats_length=list(boats_length)
) )

View file

@ -1,3 +1,5 @@
import socket
import struct
from dataclasses import dataclass, field from dataclasses import dataclass, field
from source.network.packet.abc import Packet from source.network.packet.abc import Packet
@ -7,11 +9,26 @@ from source.network.packet.abc import Packet
class PacketUsername(Packet): class PacketUsername(Packet):
username: str = field() username: str = field()
packet_size: int = 16 packet_format: str = ">I"
def to_bytes(self): 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 @classmethod
def from_bytes(cls, data: bytes): def from_connection(cls, connection: socket.socket) -> "PacketUsername":
return cls(username=data.decode("utf-8")) 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"))

View file

@ -1,14 +1,13 @@
import socket import socket
import struct
from abc import abstractmethod, ABC from abc import abstractmethod, ABC
from typing import Type, Optional from typing import Type, Optional
# TODO: struct.calcsize() au lieu de packet_size
class Packet(ABC): class Packet(ABC):
packed_header: bytes packed_header: bytes
packet_size: int
packet_id: int = 0 packet_id: int = 0
packet_format: str = ">"
def __init_subclass__(cls, **kwargs): def __init_subclass__(cls, **kwargs):
cls.packet_header = Packet.packet_id.to_bytes(1, "big") # give a header to the newly created subclass 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 pass
@classmethod def send_connection(self, connection: socket.socket):
@abstractmethod
def from_bytes(cls, data: bytes) -> "Packet":
""" """
Convert a bytes object into a packet. Send the packet directly into a socket.
:param data: the data to convert into a packet. Should be "packet_size" long. :param connection: the socket where to send the packet to.
:return: a packet corresponding to the bytes.
""" """
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 @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. Get a subclass from its packet header.
:param packet_header: the header to find the corresponding subclass :param packet_header: the header to find the corresponding subclass
@ -44,31 +47,32 @@ class Packet(ABC):
cls.__subclasses__() cls.__subclasses__()
)) ))
def send_connection(self, connection: socket.socket): @classmethod
def from_bytes(cls, data: bytes) -> "Packet":
""" """
Send the packet directly into a socket. Convert a bytes object into a packet.
:param connection: the socket where to send the packet to. :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 @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. Receive a packet from a socket.
:param connection: the socket where to get the data from :param connection: the socket where to get the data from
:return: the packet, or None if there was nothing in the socket to receive. :return: the packet, or None if there was nothing in the socket to receive.
""" """
# get the packet type packet_size: int = struct.calcsize(cls.packet_format)
return cls.from_bytes(connection.recv(packet_size))
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))