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 :
- 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" (?)

View file

@ -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()

View file

@ -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')

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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""

View file

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

View file

@ -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):

View file

@ -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"))

View file

@ -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)
)

View file

@ -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"))

View file

@ -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))