From a25335c4afeee6516ad2908e57fced335038bfba Mon Sep 17 00:00:00 2001 From: Julian Lemmerich <41118534+JM-Lemmi@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:21:10 +0100 Subject: [PATCH 1/4] feat(server): advertise Lobby on mDNS --- Server/websocket_server.py | 15 ++++++++++++--- requirements.txt | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Server/websocket_server.py b/Server/websocket_server.py index b4b3cec..9f1f25c 100644 --- a/Server/websocket_server.py +++ b/Server/websocket_server.py @@ -8,7 +8,8 @@ import logging import json from jsonschema import validate, ValidationError -from uuid import UUID +import uuid +from zeroconf import ServiceInfo, Zeroconf logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -26,6 +27,13 @@ def __init__(self, admin:Player, port: int = 8765) -> None: with open("./json_schema/client_to_server.json", "r") as f: self._json_schema = json.load(f) + # MDNS + #https://stackoverflow.com/a/74633230 + self._mdns = Zeroconf() + wsInfo = ServiceInfo(type_ = '_tictactoe._tcp.local.', name = 'tttk-'+str(uuid.uuid4())+'._tictactoe._tcp.local.', port = self._port) + self._mdns.register_service(wsInfo) + + async def handler(self, websocket): self._connections.add(websocket) @@ -50,7 +58,7 @@ async def handler(self, websocket): await websocket.send("Game in progress, cannot join") # TODO jsonify break - self._players[message_json["profile"]["uuid"]] = Player(uuid=UUID(message_json["profile"]["uuid"]), display_name=message_json["profile"]["display_name"], color=message_json["profile"]["color"]) + self._players[message_json["profile"]["uuid"]] = Player(uuid=uuid.UUID(message_json["profile"]["uuid"]), display_name=message_json["profile"]["display_name"], color=message_json["profile"]["color"]) # send new lobby status websockets.broadcast(self._connections, json.dumps({ @@ -91,6 +99,7 @@ async def handler(self, websocket): self._game = Game(player1 = list(self._players.values())[0], player2 = list(self._players.values())[1], rule_base = rulebase) self._inprogress = True + self._mdns.unregister_all_services() websockets.broadcast(self._connections, json.dumps({ "message_type": "game/start", @@ -204,5 +213,5 @@ def run(self): asyncio.run(self.start_server()) if __name__ == "__main__": - lobby = Lobby(port = 8765, admin = Player(uuid=UUID("c4f0eccd-a6a4-4662-999c-17669bc23d5e"), display_name="admin", color=0xffffff, ready=True)) + lobby = Lobby(port = 8765, admin = Player(uuid=uuid.UUID("c4f0eccd-a6a4-4662-999c-17669bc23d5e"), display_name="admin", color=0xffffff, ready=True)) lobby.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4f5214a..e2f1a80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ websockets==12.0 jsonschema==4.21.1 emoji==2.10.1 +zeroconf From c87f27ad528f147eb6cfe96bcee43afb96e28d87 Mon Sep 17 00:00:00 2001 From: Julian Lemmerich <41118534+JM-Lemmi@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:01:16 +0100 Subject: [PATCH 2/4] feat(server): send IP in announcement discover_games function in UI to list all games Co-Authored-By: Bengt Wegner <87223648+Petzys@users.noreply.github.com> --- Server/websocket_server.py | 4 +++- UI/autodiscovery.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 UI/autodiscovery.py diff --git a/Server/websocket_server.py b/Server/websocket_server.py index 9f1f25c..b086c09 100644 --- a/Server/websocket_server.py +++ b/Server/websocket_server.py @@ -10,6 +10,7 @@ from jsonschema import validate, ValidationError import uuid from zeroconf import ServiceInfo, Zeroconf +import socket logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -30,7 +31,8 @@ def __init__(self, admin:Player, port: int = 8765) -> None: # MDNS #https://stackoverflow.com/a/74633230 self._mdns = Zeroconf() - wsInfo = ServiceInfo(type_ = '_tictactoe._tcp.local.', name = 'tttk-'+str(uuid.uuid4())+'._tictactoe._tcp.local.', port = self._port) + ip = socket.inet_aton(socket.gethostbyname(socket.gethostname())) # this is a dirty hack and also doesnt work dual stack :'( + wsInfo = ServiceInfo(type_ = '_tictactoe._tcp.local.', name = admin.display_name+'s-Server'+'._tictactoe._tcp.local.', port = self._port, addresses = [ip]) self._mdns.register_service(wsInfo) diff --git a/UI/autodiscovery.py b/UI/autodiscovery.py new file mode 100644 index 0000000..f0b2d72 --- /dev/null +++ b/UI/autodiscovery.py @@ -0,0 +1,42 @@ +from zeroconf import ServiceBrowser, ServiceListener, Zeroconf +import logging +import time + +logger = logging.getLogger(__name__) + +def discover_games(timeout: int = 5) -> dict: + """ + Returns a dictionary of games that are available to play. + The key is the name of the game and the value is the IP address of the server as string. + + param: timeout. The time in seconds to wait for responses. Default is 5 seconds. + + return: dict. A dictionary of games on the network that are available to play. Key is the name, Value is the IP as string. + """ + + class MyListener(ServiceListener): + def __init__(self): + self.results = {} + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + info = zc.get_service_info(type_, name) + if info: + logger.info(f"Service {name} found, service address: {info.parsed_addresses()[0]}") # TODO only gets first address, so dual stack is not possible + self.results.update({name.removesuffix('._tictactoe._tcp.local.'): info.parsed_addresses()[0]}) + else: + logger.error(f"Service {name} found, no info") + + + zeroconf = Zeroconf() + listener = MyListener() + ServiceBrowser(zeroconf, "_tictactoe._tcp.local.", listener) + + time.sleep(timeout) + + zeroconf.close() + return listener.results + +if __name__ == "__main__": + while True: + print(discover_games()) + time.sleep(5) From 2e91da9b06aefc3c05823e54c0d4475e0fbb17dc Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 18:11:13 +0100 Subject: [PATCH 3/4] feat(UI-UX): included automatic game discovery into gui [TTTK-53] --- UI/multi.py | 70 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/UI/multi.py b/UI/multi.py index 9783a50..024d41f 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -1,6 +1,8 @@ #import tkinter as tk from .lib import tttk_tk as tk -from uuid import UUID +from threading import Thread +from queue import Queue +from sys import exit from .base_frame import base_frame from .field_frame import Field @@ -9,7 +11,8 @@ from .field_frame import player_type from Server.main import server_thread from AI.ai_context import AIContext -from AI.ai_strategy import AIStrategy, WeakAIStrategy, AdvancedAIStrategy +from AI.ai_strategy import WeakAIStrategy, AdvancedAIStrategy +from .autodiscovery import discover_games class Join(base_frame): def __init__(self, master, *args, opponent=player_type.unknown, **kwargs): @@ -81,41 +84,84 @@ def _menu(self): self.master.out_queue.put({'message_type': 'server/terminate', 'args': {}}) self.master.show_menu() + +def reload(tkinter_obj: tk.Widget, queue: Queue): + print('hiu') + for i in range(10): + servers = discover_games(i+1) + queue.put(servers) + tkinter_obj.event_generate('<>') + print(servers) + tkinter_obj.event_generate('<>') + print('dead') + exit() + class Lobby_Overview(tk.Container): def __init__(self, master): super().__init__(master) self._create_widgets() self._display_widgets() + self.queue = Queue() + self.thread = False + self.servers = {} + self.bind('<>', lambda *args: self._finish_reload()) + self.bind('<>', lambda *args: self._thread_reset()) + self._start_reload() def _create_widgets(self): self.frame = tk.Frame(self) self.innerframe = self.frame.widget self.lblHeading = tk.Label(self.innerframe, text="Join public lobbies", font=self.master.master.title_font) - - self.btnManual = tk.Button(self.innerframe, text="Join by address", command=lambda *args: self.manually()) + self.btnReload = tk.Button(self.innerframe, text="\u21BB", command=lambda *args: self._start_reload(), border=0) + self.wrapper = tk.Container(self.innerframe) + self.btnManual = tk.Button(self.innerframe, text="Join by address", command=lambda *args: self._manually()) self.etrAddress = tk.Entry(self.innerframe) - self.btnConnect = tk.Button(self.innerframe, text="Connect", command=lambda *args: self._connect()) + self.btnConnect = tk.Button(self.innerframe, text="Connect", command=lambda ip=self.etrAddress.get(), *args: self._connect(ip)) def _display_widgets(self): self.frame.pack(expand=True, fill=tk.BOTH) - self.innerframe.columnconfigure([0, 2, 4], weight=1) + self.innerframe.columnconfigure([0, 2, 4, 5], weight=1) self.innerframe.columnconfigure([1, 3], weight=5) self.innerframe.rowconfigure([0, 4], weight=2) self.innerframe.rowconfigure([1, 3], weight=1) self.innerframe.rowconfigure([2], weight=40) self.lblHeading.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=0, columnspan=3) - self.btnManual.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=4, columnspan=3) + self.btnReload.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=0) + self.btnManual.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=4, columnspan=4) - def manually(self): + def _manually(self): self.btnManual.grid_forget() self.etrAddress.grid(column=1, row=10, sticky=tk.E+tk.W+tk.N+tk.S) - self.btnConnect.grid(column=3, row=10, sticky=tk.E+tk.W+tk.N+tk.S) + self.btnConnect.grid(column=3, row=10, columnspan=2, sticky=tk.E+tk.W+tk.N+tk.S) - def _connect(self): + def _connect(self, ip): root = self.master.master - root.network_client = client_thread(root, in_queue=root.out_queue, out_queue=root.in_queue, player=root.player, ip=self.etrAddress.get()) + root.network_client = client_thread(root, in_queue=root.out_queue, out_queue=root.in_queue, player=root.player, ip=ip) root.show(Join) + def _start_reload(self): + if(self.thread): + return + Thread(target=reload, args=(self, self.queue), daemon=True).start() + self.thread = True + + def _finish_reload(self): + servers = self.queue.get() + if(servers == self.servers): + return + self.servers = servers + self.wrapper.destroy() + self.wrapper = tk.Container(self.innerframe) + self.wrapper.columnconfigure([0], weight=1) + self.wrapper.columnconfigure([1], weight=0) + for i, (server, ip) in enumerate(self.servers.items()): + tk.Label(self.wrapper, text=server).grid(column=0, row=i, sticky=tk.W+tk.N+tk.S) + tk.Button(self.wrapper, text="Join", command=lambda ip=ip, *args: self._connect(ip)).grid(column=1, row=i, sticky=tk.E+tk.W+tk.N+tk.S) + self.wrapper.grid(column=1, row=2, columnspan=4, sticky=tk.E+tk.W+tk.N+tk.S) + + def _thread_reset(self): + self.thread = False + class Multiplayer(base_frame): def __new__(cls, master, *args, **kwargs): if(master.player == None): @@ -126,7 +172,7 @@ def __init__(self, master, *args): super().__init__(master) self._create_widgets() self._display_widgets() - self.address_toogle = False + def _create_widgets(self): self.lblTitle = tk.Label(self, text='Multiplayer', font=self.master.title_font) From 0c12d4e115f9e7f746b3969d6b2e1b200bf7ed2f Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:15:33 +0100 Subject: [PATCH 4/4] Fix(autodiscovery): Added missing zeroconf remove service method, fixed UIClient arguments in UI Co-authored-by: Hauke Platte --- UI/autodiscovery.py | 3 +++ UI/multi.py | 17 ++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/UI/autodiscovery.py b/UI/autodiscovery.py index f0b2d72..9d7e6e5 100644 --- a/UI/autodiscovery.py +++ b/UI/autodiscovery.py @@ -26,6 +26,9 @@ def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: else: logger.error(f"Service {name} found, no info") + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + logger.info(f"Service {name} removed") + zeroconf = Zeroconf() listener = MyListener() diff --git a/UI/multi.py b/UI/multi.py index 0ee700a..1c08e9b 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -196,14 +196,17 @@ def _start_game(self): def reload(tkinter_obj: tk.Widget, queue: Queue): - print('hiu') for i in range(10): servers = discover_games(i+1) queue.put(servers) - tkinter_obj.event_generate('<>') - print(servers) - tkinter_obj.event_generate('<>') - print('dead') + try: + tkinter_obj.event_generate('<>') + except tk.TclError: + pass + try: + tkinter_obj.event_generate('<>') + except tk.TclError: + pass exit() class Lobby_Overview(tk.Container): @@ -217,7 +220,7 @@ def __init__(self, master): self.queue = Queue() self.thread = False self.servers = {} - self.master.master.network_events['lobby/connect'] = self._connect + self.master.master.network_events['lobby/connect'] = self._lobby_connect self.master.master.network_events['lobby/connect_error'] = self._connect_error self.bind('<>', lambda *args: self._finish_reload()) self.bind('<>', lambda *args: self._thread_reset()) @@ -259,7 +262,7 @@ def _enter(self): def _connect(self, ip): root = self.master.master root.out_queue = {root.players[root.player].uuid: Queue()} - root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) + root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=ip) self.etrAddress.config(state=tk.DISABLED) self.btnConnect.config(text="Connecting...", state=tk.DISABLED)