From 9442e3cdfbd17a6254aab72153c80bae425f265b Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Fri, 21 Mar 2025 01:41:49 +0100 Subject: [PATCH 01/11] First (non-functional) multiplayer test --- export_presets.cfg | 2 +- icon.svg.import | 2 +- scenes/MultiMorseBanner.tscn | 13 +++ scenes/MultiplayerConnect.tscn | 89 ++++++++++++++++++ scenes/main.gd | 8 ++ scenes/main.tscn | 6 ++ scenes/multi_morse_banner.gd | 48 ++++++++++ scenes/multi_morse_banner.gd.uid | 1 + scenes/multiplayer_connect.gd | 69 ++++++++++++++ scenes/multiplayer_connect.gd.uid | 1 + signalsrv/signalsrv.py | 146 ++++++++++++++++++++++++++++++ signalsrv/signalsrv2.py | 81 +++++++++++++++++ 12 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 scenes/MultiMorseBanner.tscn create mode 100644 scenes/MultiplayerConnect.tscn create mode 100644 scenes/multi_morse_banner.gd create mode 100644 scenes/multi_morse_banner.gd.uid create mode 100644 scenes/multiplayer_connect.gd create mode 100644 scenes/multiplayer_connect.gd.uid create mode 100644 signalsrv/signalsrv.py create mode 100644 signalsrv/signalsrv2.py diff --git a/export_presets.cfg b/export_presets.cfg index 2c029c6..5b7d0c8 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -129,7 +129,7 @@ permissions/install_location_provider=false permissions/install_packages=false permissions/install_shortcut=false permissions/internal_system_window=false -permissions/internet=false +permissions/internet=true permissions/kill_background_processes=false permissions/location_hardware=false permissions/manage_accounts=false diff --git a/icon.svg.import b/icon.svg.import index e3d1b7b..224ae46 100644 --- a/icon.svg.import +++ b/icon.svg.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://dvaugiwdmfmge" +uid="uid://bkoamufjn5wa1" path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" metadata={ "vram_texture": false diff --git a/scenes/MultiMorseBanner.tscn b/scenes/MultiMorseBanner.tscn new file mode 100644 index 0000000..a9a5267 --- /dev/null +++ b/scenes/MultiMorseBanner.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=2 format=3 uid="uid://ug3u6jf36dst"] + +[ext_resource type="Script" uid="uid://j1oei8suq5sj" path="res://scenes/morse_banner.gd" id="1_a1ve8"] + +[node name="MorseBanner" type="Control"] +custom_minimum_size = Vector2(200, 100) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_a1ve8") diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn new file mode 100644 index 0000000..341033f --- /dev/null +++ b/scenes/MultiplayerConnect.tscn @@ -0,0 +1,89 @@ +[gd_scene load_steps=2 format=3 uid="uid://dnxcrx04kl3xy"] + +[ext_resource type="Script" uid="uid://di8r70441xdms" path="res://scenes/multiplayer_connect.gd" id="1_uyd8l"] + +[node name="MultiplayerConnect" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_uyd8l") + +[node name="ConnectView" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="ConnectView"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="ConnectView/VBoxContainer"] +layout_mode = 2 + +[node name="FrequencyCreator" type="TextEdit" parent="ConnectView/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "430.200" + +[node name="Label" type="Label" parent="ConnectView/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "MHz" + +[node name="CreateButton" type="Button" parent="ConnectView/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_stretch_ratio = 2.41 +text = "Create Frequency" + +[node name="RefreshButton" type="Button" parent="ConnectView/VBoxContainer"] +layout_mode = 2 +text = "Refresh" + +[node name="FreqList" type="ItemList" parent="ConnectView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MorseView" type="Control" parent="."] +unique_name_in_owner = true +visible = false +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="MorseView"] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="HBoxContainer" type="HBoxContainer" parent="MorseView/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MorseView/VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Freq:" + +[node name="FreqLabel" type="Label" parent="MorseView/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="MorseButton" type="Button" parent="MorseView/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +size_flags_stretch_ratio = 2.0 +text = "MORSE" + +[connection signal="pressed" from="ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] +[connection signal="pressed" from="ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] +[connection signal="item_clicked" from="ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] diff --git a/scenes/main.gd b/scenes/main.gd index b80edb7..881ac4b 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -6,6 +6,7 @@ var phase = 0.0 var morse_state := false var playback: AudioStreamPlayback = null var vol_on := -30 +var multiplayer_enabled: bool = true # FIXME: maybe make this a tag? func _process(_delta): fill_buffer() @@ -24,6 +25,9 @@ func _ready(): $Player.play() playback = $Player.get_stream_playback() fill_buffer() + + if multiplayer_enabled: + $VBoxContainer/MultiplayerButton.visible = true OS.open_midi_inputs() print(OS.get_connected_midi_inputs()) @@ -144,3 +148,7 @@ func _on_wav_button_pressed() -> void: func _on_reset_button_pressed() -> void: MorseState.reset() + + +func _on_multiplayer_button_pressed() -> void: + get_tree().change_scene_to_file("res://scenes/MultiplayerConnect.tscn") diff --git a/scenes/main.tscn b/scenes/main.tscn index 61f554f..e3f2099 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -63,6 +63,11 @@ size_flags_vertical = 3 size_flags_stretch_ratio = 0.5 text = "Reset" +[node name="MultiplayerButton" type="Button" parent="VBoxContainer"] +visible = false +layout_mode = 2 +text = "Connect to Frequency" + [node name="Player" type="AudioStreamPlayer" parent="."] stream = SubResource("AudioStreamGenerator_kvn5v") volume_db = -80.0 @@ -74,3 +79,4 @@ script = ExtResource("3_sugp2") [connection signal="button_up" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_up"] [connection signal="pressed" from="VBoxContainer/WavButton" to="." method="_on_wav_button_pressed"] [connection signal="pressed" from="VBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"] +[connection signal="pressed" from="VBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"] diff --git a/scenes/multi_morse_banner.gd b/scenes/multi_morse_banner.gd new file mode 100644 index 0000000..974bf3f --- /dev/null +++ b/scenes/multi_morse_banner.gd @@ -0,0 +1,48 @@ +@tool +extends Control + +var color_on := Color(0, 128, 0) +var color_off := Color(0, 0, 0) +var last_delta := 0.0 +@export_range(0.1, 120.0) var display_sec := 5.0 +@export var stretch_display := false + +var morse_step_perc := 0.45 + + +func _draw_morse_rect(x: float, width: float, state: bool): + var rect := Rect2(max(x, 0.0), morse_step_perc * size.y, width, (0.5 - morse_step_perc) * size.y) + draw_rect(rect, color_on if state else color_off, true, -1.0, true) + +func _draw(): + # black background + draw_rect(Rect2(0.0, 0.0, size.x, size.y), Color.BLACK) + + # in editor we only want a black rectangle + if Engine.is_editor_hint(): + return + + var morse_on := MorseState.curr_state + var first_time := Time.get_ticks_msec() - MorseState.last_change + var curr_x := float(size.x) + + var px_per_s := 0.0 + if not stretch_display: + px_per_s = size.x / display_sec + else: + px_per_s = size.x / (Time.get_ticks_msec() - MorseState.start_time) * 1000.0 + + for n in [-1] + range(MorseState.states.size() - 1, -1, -1): + var duration := first_time if n == -1 else MorseState.states[n] + var rect_width: float = min(duration / 1000.0 * px_per_s, curr_x) + curr_x -= rect_width + if morse_on: + # at the moment we only draw the morse rects + _draw_morse_rect(curr_x, rect_width, morse_on) + morse_on = not morse_on + if curr_x <= 0.0: + break + +func _process(_delta): + last_delta += _delta + queue_redraw() diff --git a/scenes/multi_morse_banner.gd.uid b/scenes/multi_morse_banner.gd.uid new file mode 100644 index 0000000..b170160 --- /dev/null +++ b/scenes/multi_morse_banner.gd.uid @@ -0,0 +1 @@ +uid://b1k6j1jti114u diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd new file mode 100644 index 0000000..da121a1 --- /dev/null +++ b/scenes/multiplayer_connect.gd @@ -0,0 +1,69 @@ +extends Control + +var server := "localhost" +var port := "3784" +var ws := WebSocketPeer.new() + +var mb_scene := preload("res://scenes/MultiMorseBanner.tscn") +var available_freqs = [] + +func _ready() -> void: + print("ws://%s:%s" % [server, port]) + ws.connect_to_url("ws://%s:%s" % [server, port]) + +func refresh_list() -> void: + pass + + +func _process(_delta: float) -> void: + ws.poll() + var state := ws.get_ready_state() + # print("moin ", state) + while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count(): + _handle_packet() + +func _handle_packet() -> void: + var data := ws.get_packet().get_string_from_utf8() + print(data) + var parsed: Dictionary = JSON.parse_string(data) + if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("type"): + return + print("parsed ", parsed) + + match parsed["type"]: + "join": + _join_freq(parsed["freq"]) + + "freq-list": + %FreqList.clear() + for freq in parsed["freqs"]: + var text = "%s MHz (%d present)" % [freq["freq"], freq["players"]] + print("Adding ", text) + var idx: int = %FreqList.add_item(text) + %FreqList.set_item_metadata(idx, freq) + +func _join_freq(freq: String): + %FreqLabel.text = "%s MHz" % freq + %ConnectView.hide() + %MorseView.show() + +func _on_refresh_button_pressed() -> void: + var refresh_cmd := {"cmd": "list", "type": "cw-generator"} + var data := JSON.stringify(refresh_cmd) + "\n" + ws.send_text(data) + + +func _on_create_button_pressed() -> void: + var freq: String = "%.3f" % float(%FrequencyCreator.text) + var refresh_cmd := {"cmd": "create", "type": "cw-generator", "freq": freq} + var data := JSON.stringify(refresh_cmd) + "\n" + ws.send_text(data) + + +func _on_freq_list_join(index: int, at_position: Vector2, mouse_button_index: int) -> void: + var meta = %FreqList.get_item_metadata(index) + var freq: String = meta["freq"] + print("Yop ", index, " metadata ", freq) + var join_cmd := {"cmd": "join", "freq": freq, "type": "cw-generator"} + ws.send_text(JSON.stringify(join_cmd) + "\n") + diff --git a/scenes/multiplayer_connect.gd.uid b/scenes/multiplayer_connect.gd.uid new file mode 100644 index 0000000..a1328ac --- /dev/null +++ b/scenes/multiplayer_connect.gd.uid @@ -0,0 +1 @@ +uid://di8r70441xdms diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py new file mode 100644 index 0000000..0e2bbec --- /dev/null +++ b/signalsrv/signalsrv.py @@ -0,0 +1,146 @@ +import asyncio +import json +import re + +from websockets.asyncio.server import serve + +__VERSION__ = "0.0.1" + + +class Client: + freqs = {} + freq_re = re.compile(r"^\d+\.\d{3}$") + + def __init__(self, websocket): + self.websocket = websocket + self.curr_freq = None + + async def handle(self): + print(f" >>> New client {self.client} connected") + exc = None + try: + await self._handle_client() + except Exception as e: + exc = e + finally: + # FIXME: basically handle disconnect / leave from room + print(f" <<< Client {self.client} disconnected: {exc}") + + @property + def client(self): + ip, port, *_ = self.websocket.remote_address + if ':' in ip: + ip = f"[{ip}]" + return f"{ip}:{port}" + + @property + def id(self): + return str(self.websocket.id) + + async def _handle_client(self): + await self._send(type="hello", name="LobbySrv 3000", version=__VERSION__) + async for data in self.websocket: + print(f" <-- client {self.client} sent {repr(data)}") + try: + data = json.loads(data) + except json.JSONDecodeError: + self._send_error("Could not decode message, invalid json") + continue + + if not isinstance(data, dict) or "cmd" not in data: + self._send_error("Invalid format in json") + continue + + print(f"{self.client} wrote:", data) + + match data["cmd"]: + case "quit": + break + case "create": + await self._create_room(data) + case "join": + await self._join_room(data) + case "list": + freqs = [{"freq": freq, "players": len(players)} + for freq, players in self.freqs.items()] + await self._send(type="freq-list", freqs=freqs) + case "disconnect": + pass + case "morse-state": + # FIXME: send to all other clients + pass + case _: + await self._send_error("Unknown command") + + async def _create_room(self, data): + if self.curr_freq: + await self._send_error(f"Already on frequency {self.curr_freq}") + return + + if "freq" not in data: + await self._send_error("No frequency in create message") + return + freq = data["freq"] + + if not self.freq_re.match(freq): + await self._send_error("Invalid frequency") + return + + if freq in self.freqs: + await self._send_error("Frequency already in use") + return + + self.curr_freq = freq + self.freqs[freq] = [self] + await self._send(type="join", freq=self.curr_freq, others=[]) + + async def _join_room(self, data): + if self.curr_freq: + await self._send_error(f"Already on frequency {self.curr_freq}") + return + + if "freq" not in data: + await self._send_error("No frequency in join message") + return + freq = data["freq"] + + if freq not in self.freqs: + await self._send_error(f"Frequency {freq} not available") + return + + self.freqs[freq].append(self) + # FIXME: do we need locking here? + await self._send(type="join", freq=self.curr_freq, players=[c.id for c in self._others(freq)]) + for other in self._others(freq): + await self._send(type="player-joined", player=self.id) + + def _others(self, freq): + return [c for c in self.freqs[freq] if c != self.websocket] + + async def _send(self, **kwargs): + data = json.dumps(kwargs).encode() + print(f" --> sending out to {self.client}: {data}") + await self.websocket.send(json.dumps(kwargs).encode() + b"\n") + + async def _send_error(self, msg: str): + await self._send(type="error", message=msg) + + +async def new_client(websocket): + try: + client = Client(websocket) + await client.handle() + finally: + pass + # async for message in websocket: + # await websocket.send(message) + + +async def main(): + HOST, PORT = "localhost", 3784 + async with serve(new_client, HOST, PORT) as server: + await server.serve_forever() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/signalsrv/signalsrv2.py b/signalsrv/signalsrv2.py new file mode 100644 index 0000000..aad0300 --- /dev/null +++ b/signalsrv/signalsrv2.py @@ -0,0 +1,81 @@ +import json +import socketserver + +__VERSION__ = "0.0.1" + +# https://websockets.readthedocs.io/en/stable/reference/asyncio/server.html + + +class LobbyMgr: + __instance = None + + def __new__(cls): + if cls.__instance is None: + cls.__instance = LobbyMgr() + + return cls.__instance + + +class LobbyHandler(socketserver.StreamRequestHandler): + def handle(self): + print(f" >>> New client {self.client} connected") + exc = None + try: + self._handle_client() + except Exception as e: + exc = e + finally: + print(f" <<< Client {self.client} disconnected: {exc}") + + @property + def client(self): + return f"{':'.join(map(str, self.client_address))}" + + def _handle_client(self): + self._send(type="hello", name="LobbySrv 3000", version=__VERSION__) + while True: + data = self.rfile.readline(10000).rstrip() + print(f" <-- client {self.client} sent {repr(data)}") + + if not data.strip(): + continue + try: + data = json.loads(data) + except json.JSONDecodeError: + self._send_error("Could not decode message, invalid json") + continue + + if not isinstance(data, dict) or "cmd" not in data: + self._send_error("Invalid format in json") + continue + + print(f"{self.client_address[0]} wrote:", data) + + match data["cmd"]: + case "quit": + break + case _: + self._send_error("Unknown command") + + def _send(self, **kwargs): + data = json.dumps(kwargs).encode() + print(f" --> sending out to {self.client}: {data}") + self.wfile.write(json.dumps(kwargs).encode() + b"\n") + + def _send_error(self, msg: str): + self._send(type="error", message=msg) + + +class LobbyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + + +def main(): + HOST, PORT = "localhost", 3784 + + with LobbyServer((HOST, PORT), LobbyHandler) as server: + server.serve_forever() + + +if __name__ == "__main__": + main() From 0bd80b400bb111378186a48c2979944e93073ce6 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Wed, 9 Apr 2025 21:50:13 +0200 Subject: [PATCH 02/11] Further multiplayer progress (still broken) --- scenes/MultiMorseBanner.tscn | 12 +++++- scenes/MultiplayerConnect.tscn | 6 +++ scenes/multi_morse_banner.gd | 73 +++++++++++++++++++++++++++++++--- scenes/multiplayer_connect.gd | 58 ++++++++++++++++++++++++++- signalsrv/signalsrv.py | 7 +++- 5 files changed, 145 insertions(+), 11 deletions(-) diff --git a/scenes/MultiMorseBanner.tscn b/scenes/MultiMorseBanner.tscn index a9a5267..255334d 100644 --- a/scenes/MultiMorseBanner.tscn +++ b/scenes/MultiMorseBanner.tscn @@ -1,6 +1,9 @@ -[gd_scene load_steps=2 format=3 uid="uid://ug3u6jf36dst"] +[gd_scene load_steps=3 format=3 uid="uid://ug3u6jf36dst"] -[ext_resource type="Script" uid="uid://j1oei8suq5sj" path="res://scenes/morse_banner.gd" id="1_a1ve8"] +[ext_resource type="Script" uid="uid://b1k6j1jti114u" path="res://scenes/multi_morse_banner.gd" id="1_a1ve8"] + +[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_a1ve8"] +mix_rate = 22050.0 [node name="MorseBanner" type="Control"] custom_minimum_size = Vector2(200, 100) @@ -11,3 +14,8 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_a1ve8") + +[node name="Player" type="AudioStreamPlayer" parent="."] +stream = SubResource("AudioStreamGenerator_a1ve8") +volume_db = -100.0 +stream_paused = true diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn index 341033f..308c4c2 100644 --- a/scenes/MultiplayerConnect.tscn +++ b/scenes/MultiplayerConnect.tscn @@ -78,6 +78,10 @@ text = "Freq:" unique_name_in_owner = true layout_mode = 2 +[node name="PlayerContainer" type="VBoxContainer" parent="MorseView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + [node name="MorseButton" type="Button" parent="MorseView/VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 @@ -87,3 +91,5 @@ text = "MORSE" [connection signal="pressed" from="ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] [connection signal="pressed" from="ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] [connection signal="item_clicked" from="ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] +[connection signal="button_down" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] +[connection signal="button_up" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"] diff --git a/scenes/multi_morse_banner.gd b/scenes/multi_morse_banner.gd index 974bf3f..04c4811 100644 --- a/scenes/multi_morse_banner.gd +++ b/scenes/multi_morse_banner.gd @@ -1,14 +1,59 @@ @tool +class_name MultiMorseBanner extends Control +static var sample_hz := 22050.0 +static var vol_on := -30 +var phase := 0.0 + +var num: int +var tone_hz: int var color_on := Color(0, 128, 0) var color_off := Color(0, 0, 0) var last_delta := 0.0 @export_range(0.1, 120.0) var display_sec := 5.0 @export var stretch_display := false +var playback; + +func _ready() -> void: + $Player.stream.mix_rate = sample_hz + $Player.volume_db = -100 + $Player.play() + playback = $Player.get_stream_playback() + fill_buffer() var morse_step_perc := 0.45 +const MMB_SCENE := preload("res://scenes/MultiMorseBanner.tscn") +class LocalMorseState: + var states: Array[int] = [] + var curr_state := false + var last_change: int = 0 + var start_time := 0 + + func reset() -> void: + last_change = Time.get_ticks_msec() + start_time = last_change + states = [] + + func set_state(state: bool) -> void: + if state == curr_state: + return + curr_state = state + + var now := Time.get_ticks_msec() + states.push_back(now - last_change) + last_change = now + +var morse_state := LocalMorseState.new() + +static func new_banner(num: int, color: Color, tone: int): + var mmb = MMB_SCENE.instantiate() + mmb.num = num + mmb.color_on = color + mmb.tone_hz = tone + + return mmb func _draw_morse_rect(x: float, width: float, state: bool): var rect := Rect2(max(x, 0.0), morse_step_perc * size.y, width, (0.5 - morse_step_perc) * size.y) @@ -22,18 +67,18 @@ func _draw(): if Engine.is_editor_hint(): return - var morse_on := MorseState.curr_state - var first_time := Time.get_ticks_msec() - MorseState.last_change + var morse_on := morse_state.curr_state + var first_time := Time.get_ticks_msec() - morse_state.last_change var curr_x := float(size.x) var px_per_s := 0.0 if not stretch_display: px_per_s = size.x / display_sec else: - px_per_s = size.x / (Time.get_ticks_msec() - MorseState.start_time) * 1000.0 + px_per_s = size.x / (Time.get_ticks_msec() - morse_state.start_time) * 1000.0 - for n in [-1] + range(MorseState.states.size() - 1, -1, -1): - var duration := first_time if n == -1 else MorseState.states[n] + for n in [-1] + range(morse_state.states.size() - 1, -1, -1): + var duration := first_time if n == -1 else morse_state.states[n] var rect_width: float = min(duration / 1000.0 * px_per_s, curr_x) curr_x -= rect_width if morse_on: @@ -46,3 +91,21 @@ func _draw(): func _process(_delta): last_delta += _delta queue_redraw() + fill_buffer() + +func set_morse_state(state: bool) -> void: + morse_state.set_state(state) + # morse_state = state + if state: + $Player.volume_db = vol_on + else: + $Player.volume_db = -100 + +func fill_buffer(): + var increment = tone_hz / sample_hz + var frames_available = playback.get_frames_available() + + for i in range(frames_available): + playback.push_frame(Vector2.ONE * sin(phase * TAU)) + phase = fmod(phase + increment, 1.0) + diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index da121a1..ef425b6 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -4,10 +4,32 @@ var server := "localhost" var port := "3784" var ws := WebSocketPeer.new() -var mb_scene := preload("res://scenes/MultiMorseBanner.tscn") var available_freqs = [] +var mmb_self: MultiMorseBanner +var mmb_others: Dictionary[String, MultiMorseBanner] + +class PlayerData: + var num: int + var color: Color + var tone: int + + func _init(num, color, tone): + self.num = num + self.color = color + self.tone = tone + +var player_data := [ + PlayerData.new(0, Color( 0, 255, 0), 880), + PlayerData.new(1, Color(255, 0, 0), 880), + PlayerData.new(2, Color( 0, 0, 255), 880), + PlayerData.new(3, Color(255, 0, 255), 880), +] func _ready() -> void: + # FIXME: connection handling / reconnect + # FIXME: status / error messages + # FIXME: randomize default join frquency + # FIXME: automatic refresh print("ws://%s:%s" % [server, port]) ws.connect_to_url("ws://%s:%s" % [server, port]) @@ -31,6 +53,9 @@ func _handle_packet() -> void: print("parsed ", parsed) match parsed["type"]: + "hello": + # fetch frequency list on first join + _on_refresh_button_pressed() "join": _join_freq(parsed["freq"]) @@ -42,7 +67,14 @@ func _handle_packet() -> void: var idx: int = %FreqList.add_item(text) %FreqList.set_item_metadata(idx, freq) -func _join_freq(freq: String): +func _join_freq(freq: String, player_id: String, other_players: Array[String]): + for child in %PlayerContainer.get_children(): + %PlayerContainer.remove_child(child) + + mmb_self = make_player(0) + for n, player_id in enumeerate(other_players): + make_player(n + 1) + %FreqLabel.text = "%s MHz" % freq %ConnectView.hide() %MorseView.show() @@ -66,4 +98,26 @@ func _on_freq_list_join(index: int, at_position: Vector2, mouse_button_index: in print("Yop ", index, " metadata ", freq) var join_cmd := {"cmd": "join", "freq": freq, "type": "cw-generator"} ws.send_text(JSON.stringify(join_cmd) + "\n") + +func make_player(no: int): + var pd: PlayerData + if no < player_data.size(): + pd = player_data[no] + else: + pd = PlayerData.new(no, Color(randi_range(0, 255), randi_range(0, 255), randi_range(0, 255)), randi_range(440, 440 * 3)) + var mmb = MultiMorseBanner.new_banner(pd.num, pd.color, pd.tone) + %PlayerContainer.add_child(mmb) + return mmb + + +func _on_morse_button_button_down() -> void: + if not mmb_self: + return + mmb_self.set_morse_state(true) + + +func _on_morse_button_button_up() -> void: + if not mmb_self: + return + mmb_self.set_morse_state(false) diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index 0e2bbec..86edb8b 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -92,7 +92,7 @@ class Client: self.curr_freq = freq self.freqs[freq] = [self] - await self._send(type="join", freq=self.curr_freq, others=[]) + await self._send(type="join", freq=self.curr_freq, self_id=self.id, others=[]) async def _join_room(self, data): if self.curr_freq: @@ -108,9 +108,12 @@ class Client: await self._send_error(f"Frequency {freq} not available") return + self.curr_freq = freq self.freqs[freq].append(self) # FIXME: do we need locking here? - await self._send(type="join", freq=self.curr_freq, players=[c.id for c in self._others(freq)]) + print("FREQ", self.curr_freq, freq, self.freqs) + await self._send(type="join", freq=self.curr_freq, self_id=self.id, players=[c.id for c in self._others(freq) + if not c.id == self.id]) for other in self._others(freq): await self._send(type="player-joined", player=self.id) From a0ee2491dd5cd370fa4c54ca0ab7b629b5290fce Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Thu, 10 Apr 2025 01:16:05 +0200 Subject: [PATCH 03/11] External multiplayer server test --- scenes/multiplayer_connect.gd | 77 +++++++++++++++++++++++++++++------ signalsrv/signalsrv.py | 45 +++++++++++++++----- 2 files changed, 99 insertions(+), 23 deletions(-) diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index ef425b6..3ad34ef 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -1,6 +1,6 @@ extends Control -var server := "localhost" +var server := "seba-geek.de" var port := "3784" var ws := WebSocketPeer.new() @@ -20,9 +20,9 @@ class PlayerData: var player_data := [ PlayerData.new(0, Color( 0, 255, 0), 880), - PlayerData.new(1, Color(255, 0, 0), 880), - PlayerData.new(2, Color( 0, 0, 255), 880), - PlayerData.new(3, Color(255, 0, 255), 880), + PlayerData.new(1, Color(255, 0, 0), 1200), + PlayerData.new(2, Color( 0, 0, 255), 600), + PlayerData.new(3, Color(255, 0, 255), 500), ] func _ready() -> void: @@ -57,7 +57,7 @@ func _handle_packet() -> void: # fetch frequency list on first join _on_refresh_button_pressed() "join": - _join_freq(parsed["freq"]) + _join_freq(parsed["freq"], parsed["self_id"], parsed["other_players"]) "freq-list": %FreqList.clear() @@ -66,14 +66,53 @@ func _handle_packet() -> void: print("Adding ", text) var idx: int = %FreqList.add_item(text) %FreqList.set_item_metadata(idx, freq) + + "morse-state": + var from_player: String = parsed["from_player"] + if from_player not in mmb_others: + print("Error: Got morse state from unknown player ", from_player) + return + mmb_others[from_player].set_morse_state(parsed["state"]) -func _join_freq(freq: String, player_id: String, other_players: Array[String]): + "player-joined": + var new_player: String = parsed["player"] + if new_player in mmb_others: + print("Error: got player join message, but player ", new_player, " is already present") + return + + # find free player id + var player_n := 1 + while true: + var found := false + for other in mmb_others.values(): + if other.num == player_n: + print("New player, num ", player_n, " already taken") + found = true + break + if not found: + break + player_n += 1 + + make_player(player_n, new_player) + + "player-left": + var player: String = parsed["player"] + if player not in mmb_others: + print("Error: player not part of freq ", player) + return + remove_player(parsed["player"]) + + _: + print("Unhandled message: ", parsed["type"]) + + +func _join_freq(freq: String, player_id: String, other_players: Array): for child in %PlayerContainer.get_children(): %PlayerContainer.remove_child(child) - mmb_self = make_player(0) - for n, player_id in enumeerate(other_players): - make_player(n + 1) + mmb_self = make_player(0, player_id) + for n in range(other_players.size()): + make_player(n + 1, other_players[n]) %FreqLabel.text = "%s MHz" % freq %ConnectView.hide() @@ -99,7 +138,7 @@ func _on_freq_list_join(index: int, at_position: Vector2, mouse_button_index: in var join_cmd := {"cmd": "join", "freq": freq, "type": "cw-generator"} ws.send_text(JSON.stringify(join_cmd) + "\n") -func make_player(no: int): +func make_player(no: int, player_id: String): var pd: PlayerData if no < player_data.size(): pd = player_data[no] @@ -108,16 +147,28 @@ func make_player(no: int): var mmb = MultiMorseBanner.new_banner(pd.num, pd.color, pd.tone) %PlayerContainer.add_child(mmb) + mmb_others[player_id] = mmb return mmb +func remove_player(player_id: String): + mmb_others[player_id].set_morse_state(false) + %PlayerContainer.remove_child(mmb_others[player_id]) + mmb_others.erase(player_id) + +func set_morse_state(state: bool): + mmb_self.set_morse_state(state) + send_data({"cmd": "morse-state", "state": state}) + +func send_data(data: Dictionary): + var text := JSON.stringify(data) + "\n" + ws.send_text(text) func _on_morse_button_button_down() -> void: if not mmb_self: return - mmb_self.set_morse_state(true) - + set_morse_state(true) func _on_morse_button_button_up() -> void: if not mmb_self: return - mmb_self.set_morse_state(false) + set_morse_state(false) diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index 86edb8b..cdcecab 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -24,7 +24,9 @@ class Client: exc = e finally: # FIXME: basically handle disconnect / leave from room - print(f" <<< Client {self.client} disconnected: {exc}") + print(f" <<< Client {self.client} id {self.id} disconnected: {exc}") + if self.curr_freq: + await self._leave_room() @property def client(self): @@ -48,7 +50,7 @@ class Client: continue if not isinstance(data, dict) or "cmd" not in data: - self._send_error("Invalid format in json") + await self._send_error("Invalid format in json") continue print(f"{self.client} wrote:", data) @@ -67,8 +69,7 @@ class Client: case "disconnect": pass case "morse-state": - # FIXME: send to all other clients - pass + await self._handle_morse_state(data) case _: await self._send_error("Unknown command") @@ -92,7 +93,7 @@ class Client: self.curr_freq = freq self.freqs[freq] = [self] - await self._send(type="join", freq=self.curr_freq, self_id=self.id, others=[]) + await self._send(type="join", freq=self.curr_freq, self_id=self.id, other_players=[]) async def _join_room(self, data): if self.curr_freq: @@ -112,13 +113,36 @@ class Client: self.freqs[freq].append(self) # FIXME: do we need locking here? print("FREQ", self.curr_freq, freq, self.freqs) - await self._send(type="join", freq=self.curr_freq, self_id=self.id, players=[c.id for c in self._others(freq) - if not c.id == self.id]) + await self._send(type="join", freq=self.curr_freq, self_id=self.id, + other_players=[c.id for c in self._others(freq)]) for other in self._others(freq): - await self._send(type="player-joined", player=self.id) + await other._send(type="player-joined", player=self.id) + + async def _handle_morse_state(self, data): + if not self.curr_freq: + await self._send_error("No frequency selected") + return + + if "state" not in data or not isinstance(data["state"], bool): + await self._send_error("No state key with type bool in data") + return + + for other in self._others(self.curr_freq): + await other._send(type="morse-state", state=data["state"], from_player=self.id) + + async def _leave_room(self): + for other in self._others(self.curr_freq): + await other._send(type="player-left", player=self.id) + try: + self.freqs[self.curr_freq].remove(self) + except ValueError: + print(f"Warning: Player {self.id} was not in freq {self.curr_freq}") + if not self.freqs[self.curr_freq]: + del self.freqs[self.curr_freq] + self.curr_freq = None def _others(self, freq): - return [c for c in self.freqs[freq] if c != self.websocket] + return [c for c in self.freqs[freq] if c.id != self.id] async def _send(self, **kwargs): data = json.dumps(kwargs).encode() @@ -140,10 +164,11 @@ async def new_client(websocket): async def main(): - HOST, PORT = "localhost", 3784 + HOST, PORT = "0.0.0.0", 3784 async with serve(new_client, HOST, PORT) as server: await server.serve_forever() if __name__ == "__main__": + print("Starting server") asyncio.run(main()) From 1f6c534b1aa3ef91eca259c64f5ebb3c719e9d81 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 13 Apr 2025 00:20:10 +0200 Subject: [PATCH 04/11] Working Multiplayer ...now with a GUI Theme! Players can leave a room as well. Actually usable on mobile! --- scenes/GUITheme.tres | 4 ++++ scenes/MultiplayerConnect.tscn | 30 +++++++++++++++++++++++------- scenes/main.gd | 2 +- scenes/main.tscn | 20 +++++++++++++++----- scenes/multiplayer_connect.gd | 30 ++++++++++++++++++++++++++---- signalsrv/signalsrv.py | 11 +++++++++++ 6 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 scenes/GUITheme.tres diff --git a/scenes/GUITheme.tres b/scenes/GUITheme.tres new file mode 100644 index 0000000..b723596 --- /dev/null +++ b/scenes/GUITheme.tres @@ -0,0 +1,4 @@ +[gd_resource type="Theme" format=3 uid="uid://xxoc27tvaiut"] + +[resource] +default_font_size = 30 diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn index 308c4c2..6b8d360 100644 --- a/scenes/MultiplayerConnect.tscn +++ b/scenes/MultiplayerConnect.tscn @@ -1,5 +1,6 @@ -[gd_scene load_steps=2 format=3 uid="uid://dnxcrx04kl3xy"] +[gd_scene load_steps=3 format=3 uid="uid://dnxcrx04kl3xy"] +[ext_resource type="Theme" uid="uid://xxoc27tvaiut" path="res://scenes/GUITheme.tres" id="1_2wc0w"] [ext_resource type="Script" uid="uid://di8r70441xdms" path="res://scenes/multiplayer_connect.gd" id="1_uyd8l"] [node name="MultiplayerConnect" type="Control"] @@ -9,6 +10,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +theme = ExtResource("1_2wc0w") script = ExtResource("1_uyd8l") [node name="ConnectView" type="Control" parent="."] @@ -35,6 +37,7 @@ layout_mode = 2 unique_name_in_owner = true custom_minimum_size = Vector2(100, 0) layout_mode = 2 +theme_override_font_sizes/font_size = 20 text = "430.200" [node name="Label" type="Label" parent="ConnectView/VBoxContainer/HBoxContainer"] @@ -42,6 +45,7 @@ layout_mode = 2 text = "MHz" [node name="CreateButton" type="Button" parent="ConnectView/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(0, 100) layout_mode = 2 size_flags_stretch_ratio = 2.41 text = "Create Frequency" @@ -58,14 +62,20 @@ size_flags_vertical = 3 [node name="MorseView" type="Control" parent="."] unique_name_in_owner = true visible = false -anchors_preset = 0 -offset_right = 40.0 -offset_bottom = 40.0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 [node name="VBoxContainer" type="VBoxContainer" parent="MorseView"] -layout_mode = 0 -offset_right = 40.0 -offset_bottom = 40.0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 [node name="HBoxContainer" type="HBoxContainer" parent="MorseView/VBoxContainer"] layout_mode = 2 @@ -81,6 +91,7 @@ layout_mode = 2 [node name="PlayerContainer" type="VBoxContainer" parent="MorseView/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 +size_flags_vertical = 3 [node name="MorseButton" type="Button" parent="MorseView/VBoxContainer"] layout_mode = 2 @@ -88,8 +99,13 @@ size_flags_vertical = 3 size_flags_stretch_ratio = 2.0 text = "MORSE" +[node name="LeaveButton" type="Button" parent="MorseView/VBoxContainer"] +layout_mode = 2 +text = "Leave Frequency" + [connection signal="pressed" from="ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] [connection signal="pressed" from="ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] [connection signal="item_clicked" from="ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] [connection signal="button_down" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] [connection signal="button_up" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"] +[connection signal="pressed" from="MorseView/VBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"] diff --git a/scenes/main.gd b/scenes/main.gd index 881ac4b..4c386df 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -27,7 +27,7 @@ func _ready(): fill_buffer() if multiplayer_enabled: - $VBoxContainer/MultiplayerButton.visible = true + %MultiplayerButton.visible = true OS.open_midi_inputs() print(OS.get_connected_midi_inputs()) diff --git a/scenes/main.tscn b/scenes/main.tscn index e3f2099..6bec0ff 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=5 format=3 uid="uid://ctak1goemnnc5"] +[gd_scene load_steps=6 format=3 uid="uid://ctak1goemnnc5"] [ext_resource type="Script" uid="uid://dmeokosn7gr27" path="res://scenes/main.gd" id="1_8bx00"] +[ext_resource type="Theme" uid="uid://xxoc27tvaiut" path="res://scenes/GUITheme.tres" id="1_jyhfs"] [ext_resource type="PackedScene" uid="uid://xqic6oa5d7oc" path="res://scenes/MorseBanner.tscn" id="2_v02md"] [ext_resource type="Script" uid="uid://bjt60u6r1hqf7" path="res://addons/SharePlugin/Share.gd" id="3_sugp2"] @@ -15,6 +16,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +theme = ExtResource("1_jyhfs") script = ExtResource("1_8bx00") [node name="VBoxContainer" type="VBoxContainer" parent="."] @@ -57,15 +59,23 @@ layout_mode = 2 size_flags_vertical = 3 text = "Write Wav" -[node name="ResetButton" type="Button" parent="VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 + +[node name="ResetButton" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 size_flags_stretch_ratio = 0.5 text = "Reset" -[node name="MultiplayerButton" type="Button" parent="VBoxContainer"] +[node name="MultiplayerButton" type="Button" parent="VBoxContainer/HBoxContainer"] +unique_name_in_owner = true visible = false layout_mode = 2 +size_flags_horizontal = 3 +size_flags_stretch_ratio = 0.5 text = "Connect to Frequency" [node name="Player" type="AudioStreamPlayer" parent="."] @@ -78,5 +88,5 @@ script = ExtResource("3_sugp2") [connection signal="button_down" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_down"] [connection signal="button_up" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_up"] [connection signal="pressed" from="VBoxContainer/WavButton" to="." method="_on_wav_button_pressed"] -[connection signal="pressed" from="VBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"] -[connection signal="pressed" from="VBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"] diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index 3ad34ef..8e7a450 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -2,6 +2,8 @@ extends Control var server := "seba-geek.de" var port := "3784" +var ws_url := "wss://seba-geek.de/godot/cw-generator-ws/" +var IS_DEBUG = false var ws := WebSocketPeer.new() var available_freqs = [] @@ -30,9 +32,14 @@ func _ready() -> void: # FIXME: status / error messages # FIXME: randomize default join frquency # FIXME: automatic refresh - print("ws://%s:%s" % [server, port]) - ws.connect_to_url("ws://%s:%s" % [server, port]) - + if not IS_DEBUG: + print(ws_url) + ws.connect_to_url(ws_url) + else: + print("ws://%s:%s" % [server, port]) + ws.connect_to_url("ws://%s:%s" % [server, port]) + + func refresh_list() -> void: pass @@ -43,6 +50,8 @@ func _process(_delta: float) -> void: # print("moin ", state) while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count(): _handle_packet() + if state != WebSocketPeer.STATE_OPEN: + print("Error: websocket in state", state) func _handle_packet() -> void: var data := ws.get_packet().get_string_from_utf8() @@ -101,7 +110,16 @@ func _handle_packet() -> void: print("Error: player not part of freq ", player) return remove_player(parsed["player"]) - + "leave": + %MorseView.hide() + mmb_self.set_morse_state(false) + for mmb: MultiMorseBanner in mmb_others.values(): + mmb.set_morse_state(false) + mmb_self = null + mmb_others.clear() + for child in %PlayerContainer.get_children(): + %PlayerContainer.remove_child(child) + %ConnectView.show() _: print("Unhandled message: ", parsed["type"]) @@ -172,3 +190,7 @@ func _on_morse_button_button_up() -> void: if not mmb_self: return set_morse_state(false) + + +func _on_leave_button_pressed() -> void: + send_data({"cmd": "leave"}) diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index cdcecab..ff22ddd 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -62,6 +62,8 @@ class Client: await self._create_room(data) case "join": await self._join_room(data) + case "leave": + await self._leave_room() case "list": freqs = [{"freq": freq, "players": len(players)} for freq, players in self.freqs.items()] @@ -131,6 +133,10 @@ class Client: await other._send(type="morse-state", state=data["state"], from_player=self.id) async def _leave_room(self): + if not self.curr_freq: + self._send_error("You are not on a frequency") + return + for other in self._others(self.curr_freq): await other._send(type="player-left", player=self.id) try: @@ -141,6 +147,11 @@ class Client: del self.freqs[self.curr_freq] self.curr_freq = None + try: + await self._send(type="leave") + except Exception: + pass + def _others(self, freq): return [c for c in self.freqs[freq] if c.id != self.id] From 35d8abb2a9df9b7c6e92fb4f436a0b9ed0f0e49d Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 13 Apr 2025 18:09:00 +0200 Subject: [PATCH 05/11] MP: Logging, test js parameters (broken) --- project.godot | 1 + scenes/main.gd | 8 ++++++++ signalsrv/signalsrv.py | 9 ++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/project.godot b/project.godot index b9b9a88..c493726 100644 --- a/project.godot +++ b/project.godot @@ -20,6 +20,7 @@ config/icon="res://icon.svg" [autoload] MorseState="*res://autoloads/morse_state.gd" +Utils="*res://autoloads/utils.gd" [display] diff --git a/scenes/main.gd b/scenes/main.gd index 4c386df..26190d9 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -29,6 +29,14 @@ func _ready(): if multiplayer_enabled: %MultiplayerButton.visible = true + + + print("External freq: ", Utils.get_external_freq_param()) + if Utils.get_external_freq_param(): + + _on_multiplayer_button_pressed() + + # FIXME: make this dependant on button OS.open_midi_inputs() print(OS.get_connected_midi_inputs()) diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index ff22ddd..67d79f8 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -1,11 +1,18 @@ import asyncio +import datetime import json +import logging import re from websockets.asyncio.server import serve __VERSION__ = "0.0.1" +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(name)s %(levelname)s %(message)s" +) + class Client: freqs = {} @@ -53,7 +60,7 @@ class Client: await self._send_error("Invalid format in json") continue - print(f"{self.client} wrote:", data) + print(f"{datetime.datetime.now()} {self.client} wrote:", data) match data["cmd"]: case "quit": From bfe3418f799d3a5ed539096c002464ccf757026b Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 13 Apr 2025 18:34:24 +0200 Subject: [PATCH 06/11] Also add Utils script --- autoloads/utils.gd | 7 +++++++ autoloads/utils.gd.uid | 1 + 2 files changed, 8 insertions(+) create mode 100644 autoloads/utils.gd create mode 100644 autoloads/utils.gd.uid diff --git a/autoloads/utils.gd b/autoloads/utils.gd new file mode 100644 index 0000000..d583f43 --- /dev/null +++ b/autoloads/utils.gd @@ -0,0 +1,7 @@ +extends Node + +func get_external_freq_param(): + match OS.get_name(): + "web": + return JavaScriptBridge.eval("new URL(window.location.href).searchParams.get('freq')") + return null diff --git a/autoloads/utils.gd.uid b/autoloads/utils.gd.uid new file mode 100644 index 0000000..0cdce87 --- /dev/null +++ b/autoloads/utils.gd.uid @@ -0,0 +1 @@ +uid://ua7dk3y0v21e From 9d4b72d85f1a84140ca04d202a74122456d4d349 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sun, 13 Apr 2025 20:07:06 +0200 Subject: [PATCH 07/11] CW server: send out messages in parallel We now send out messages to all participants at the same time (or more or less, as asyncio permits). To not fail in case we can't send the message to one player, we ignore send-exceptions in these occasions and hope that the `for data in self.websocket` in _handle_client() will throw an error and kick the client out of our game. --- signalsrv/signalsrv.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index 67d79f8..3635a90 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -124,8 +124,7 @@ class Client: print("FREQ", self.curr_freq, freq, self.freqs) await self._send(type="join", freq=self.curr_freq, self_id=self.id, other_players=[c.id for c in self._others(freq)]) - for other in self._others(freq): - await other._send(type="player-joined", player=self.id) + await self._send_to_group(self._others(freq), type="player-joined", player=self.id) async def _handle_morse_state(self, data): if not self.curr_freq: @@ -136,16 +135,16 @@ class Client: await self._send_error("No state key with type bool in data") return - for other in self._others(self.curr_freq): - await other._send(type="morse-state", state=data["state"], from_player=self.id) + await self._send_to_group(self._others(self.curr_freq), + type="morse-state", state=data["state"], from_player=self.id) async def _leave_room(self): if not self.curr_freq: self._send_error("You are not on a frequency") return - for other in self._others(self.curr_freq): - await other._send(type="player-left", player=self.id) + await self._send_to_group(self._others(self.curr_freq), + type="player-left", player=self.id) try: self.freqs[self.curr_freq].remove(self) except ValueError: @@ -162,10 +161,20 @@ class Client: def _others(self, freq): return [c for c in self.freqs[freq] if c.id != self.id] - async def _send(self, **kwargs): + async def _send(self, ignore_exceptions=False, **kwargs): data = json.dumps(kwargs).encode() print(f" --> sending out to {self.client}: {data}") - await self.websocket.send(json.dumps(kwargs).encode() + b"\n") + try: + await self.websocket.send(json.dumps(kwargs).encode() + b"\n") + except Exception as e: + print(f"Error sending data to {self.client}: {e}") + if not ignore_exceptions: + raise + + async def _send_to_group(self, group, **kwargs): + async with asyncio.TaskGroup() as tg: + for member in group: + tg.create_task(member._send(ignore_exceptions=True, **kwargs)) async def _send_error(self, msg: str): await self._send(type="error", message=msg) From bdea597057f0d8817868ce2bfb0a43988cc400aa Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 14 Apr 2025 02:46:15 +0200 Subject: [PATCH 08/11] Autoconnect, reconnect, connection display We can now autoconnect via parameter on web, e.g. `freq=430.200`, which then autoconnects to that frequency, creating it when it is not already created. We also now reconnect on connection failure and display the connection state down below. --- autoloads/utils.gd | 4 +- scenes/MultiplayerConnect.tscn | 107 ++++++++++++++++++++++----------- scenes/main.gd | 1 - scenes/multiplayer_connect.gd | 80 +++++++++++++++++------- signalsrv/signalsrv.py | 5 +- 5 files changed, 136 insertions(+), 61 deletions(-) diff --git a/autoloads/utils.gd b/autoloads/utils.gd index d583f43..53184f7 100644 --- a/autoloads/utils.gd +++ b/autoloads/utils.gd @@ -1,7 +1,9 @@ extends Node +var first_connect_done := false + func get_external_freq_param(): match OS.get_name(): - "web": + "Web": return JavaScriptBridge.eval("new URL(window.location.href).searchParams.get('freq')") return null diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn index 6b8d360..f5d4758 100644 --- a/scenes/MultiplayerConnect.tscn +++ b/scenes/MultiplayerConnect.tscn @@ -13,8 +13,20 @@ grow_vertical = 2 theme = ExtResource("1_2wc0w") script = ExtResource("1_uyd8l") -[node name="ConnectView" type="Control" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ConnectView" type="Control" parent="VBoxContainer"] unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ConnectView"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -22,46 +34,42 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="ConnectView"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="HBoxContainer" type="HBoxContainer" parent="ConnectView/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ConnectView/VBoxContainer"] layout_mode = 2 -[node name="FrequencyCreator" type="TextEdit" parent="ConnectView/VBoxContainer/HBoxContainer"] +[node name="FrequencyCreator" type="TextEdit" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"] unique_name_in_owner = true custom_minimum_size = Vector2(100, 0) layout_mode = 2 theme_override_font_sizes/font_size = 20 text = "430.200" -[node name="Label" type="Label" parent="ConnectView/VBoxContainer/HBoxContainer"] +[node name="Label" type="Label" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"] layout_mode = 2 text = "MHz" -[node name="CreateButton" type="Button" parent="ConnectView/VBoxContainer/HBoxContainer"] +[node name="CreateButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"] custom_minimum_size = Vector2(0, 100) layout_mode = 2 size_flags_stretch_ratio = 2.41 text = "Create Frequency" -[node name="RefreshButton" type="Button" parent="ConnectView/VBoxContainer"] +[node name="RefreshButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer"] layout_mode = 2 text = "Refresh" -[node name="FreqList" type="ItemList" parent="ConnectView/VBoxContainer"] +[node name="FreqList" type="ItemList" parent="VBoxContainer/ConnectView/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 -[node name="MorseView" type="Control" parent="."] +[node name="MorseView" type="Control" parent="VBoxContainer"] unique_name_in_owner = true visible = false +layout_mode = 2 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MorseView"] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -69,43 +77,70 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="MorseView"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="HBoxContainer" type="HBoxContainer" parent="MorseView/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"] layout_mode = 2 -[node name="Label" type="Label" parent="MorseView/VBoxContainer/HBoxContainer"] +[node name="Label" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"] layout_mode = 2 text = "Freq:" -[node name="FreqLabel" type="Label" parent="MorseView/VBoxContainer/HBoxContainer"] +[node name="FreqLabel" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"] unique_name_in_owner = true layout_mode = 2 -[node name="PlayerContainer" type="VBoxContainer" parent="MorseView/VBoxContainer"] +[node name="PlayerContainer" type="VBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 -[node name="MorseButton" type="Button" parent="MorseView/VBoxContainer"] +[node name="MorseButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 size_flags_stretch_ratio = 2.0 text = "MORSE" -[node name="LeaveButton" type="Button" parent="MorseView/VBoxContainer"] +[node name="LeaveButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer"] layout_mode = 2 text = "Leave Frequency" -[connection signal="pressed" from="ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] -[connection signal="pressed" from="ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] -[connection signal="item_clicked" from="ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] -[connection signal="button_down" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] -[connection signal="button_up" from="MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"] -[connection signal="pressed" from="MorseView/VBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Status:" + +[node name="StatusLabel" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Disconnected" + +[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer2"] +layout_mode = 2 +mouse_filter = 0 +text = "Last Error:" + +[node name="ErrorLabel" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +mouse_filter = 0 +text = "" +autowrap_mode = 2 + +[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] +[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] +[connection signal="item_clicked" from="VBoxContainer/ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] +[connection signal="button_down" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] +[connection signal="button_up" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"] +[connection signal="pressed" from="VBoxContainer/MorseView/VBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"] +[connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/Label" to="." method="_on_error_label_gui_input"] +[connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/ErrorLabel" to="." method="_on_error_label_gui_input"] diff --git a/scenes/main.gd b/scenes/main.gd index 26190d9..41fad80 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -33,7 +33,6 @@ func _ready(): print("External freq: ", Utils.get_external_freq_param()) if Utils.get_external_freq_param(): - _on_multiplayer_button_pressed() # FIXME: make this dependant on button diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index 8e7a450..e1e5c00 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -5,6 +5,8 @@ var port := "3784" var ws_url := "wss://seba-geek.de/godot/cw-generator-ws/" var IS_DEBUG = false var ws := WebSocketPeer.new() +var ws_last_status = -1 +var autoconnect_to_freq: String var available_freqs = [] var mmb_self: MultiMorseBanner @@ -32,39 +34,61 @@ func _ready() -> void: # FIXME: status / error messages # FIXME: randomize default join frquency # FIXME: automatic refresh + _connect_ws() + + if not Utils.first_connect_done: + var cmdline_freq = Utils.get_external_freq_param() + if cmdline_freq: + autoconnect_to_freq = cmdline_freq + +func _connect_ws(): if not IS_DEBUG: - print(ws_url) + print("Connecting to ", ws_url) ws.connect_to_url(ws_url) else: print("ws://%s:%s" % [server, port]) ws.connect_to_url("ws://%s:%s" % [server, port]) - -func refresh_list() -> void: - pass - - func _process(_delta: float) -> void: ws.poll() var state := ws.get_ready_state() - # print("moin ", state) while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count(): _handle_packet() - if state != WebSocketPeer.STATE_OPEN: - print("Error: websocket in state", state) - + if ws_last_status != state: + match state: + WebSocketPeer.STATE_CONNECTING: + %StatusLabel.text = "Connecting...!" + WebSocketPeer.STATE_OPEN: + %StatusLabel.text = "Connected!" + WebSocketPeer.STATE_CLOSING: + %StatusLabel.text = "Disconnecting..." + WebSocketPeer.STATE_CLOSED: + %StatusLabel.text = "Disconnected :(" + + # Trigger reconnect + _trigger_reconnect(1) + ws_last_status = state + +func _trigger_reconnect(delay: int): + await get_tree().create_timer(delay).timeout + _connect_ws() + func _handle_packet() -> void: var data := ws.get_packet().get_string_from_utf8() - print(data) var parsed: Dictionary = JSON.parse_string(data) if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("type"): + print("Error: Could not parse data: ", data) return - print("parsed ", parsed) + + print("Recvd data: ", parsed) match parsed["type"]: "hello": # fetch frequency list on first join _on_refresh_button_pressed() + + if autoconnect_to_freq: + send_data({"cmd": "create", "freq": autoconnect_to_freq, "join-if-present": true}) "join": _join_freq(parsed["freq"], parsed["self_id"], parsed["other_players"]) @@ -111,15 +135,9 @@ func _handle_packet() -> void: return remove_player(parsed["player"]) "leave": - %MorseView.hide() - mmb_self.set_morse_state(false) - for mmb: MultiMorseBanner in mmb_others.values(): - mmb.set_morse_state(false) - mmb_self = null - mmb_others.clear() - for child in %PlayerContainer.get_children(): - %PlayerContainer.remove_child(child) - %ConnectView.show() + _leave_freq() + "error": + %ErrorLabel.text = parsed["message"] _: print("Unhandled message: ", parsed["type"]) @@ -136,6 +154,20 @@ func _join_freq(freq: String, player_id: String, other_players: Array): %ConnectView.hide() %MorseView.show() +func _leave_freq(): + %MorseView.hide() + if mmb_self: + mmb_self.set_morse_state(false) + mmb_self = null + + for mmb: MultiMorseBanner in mmb_others.values(): + mmb.set_morse_state(false) + + mmb_others.clear() + for child in %PlayerContainer.get_children(): + %PlayerContainer.remove_child(child) + %ConnectView.show() + func _on_refresh_button_pressed() -> void: var refresh_cmd := {"cmd": "list", "type": "cw-generator"} var data := JSON.stringify(refresh_cmd) + "\n" @@ -191,6 +223,10 @@ func _on_morse_button_button_up() -> void: return set_morse_state(false) - func _on_leave_button_pressed() -> void: send_data({"cmd": "leave"}) + + +func _on_error_label_gui_input(event: InputEvent) -> void: + if event is InputEventMouseButton and event.pressed and event.button_index == 1: + %ErrorLabel.text = "" diff --git a/signalsrv/signalsrv.py b/signalsrv/signalsrv.py index 3635a90..89fde8f 100644 --- a/signalsrv/signalsrv.py +++ b/signalsrv/signalsrv.py @@ -97,7 +97,10 @@ class Client: return if freq in self.freqs: - await self._send_error("Frequency already in use") + if data.get("join-if-present"): + await self._join_room({"freq": freq}) + else: + await self._send_error("Frequency already in use") return self.curr_freq = freq From d25f7fe0a3197b21670c68a63bf0904313707f91 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 14 Apr 2025 03:09:23 +0200 Subject: [PATCH 09/11] Better player tones, back button --- scenes/MultiplayerConnect.tscn | 6 ++++++ scenes/multiplayer_connect.gd | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn index f5d4758..90f7840 100644 --- a/scenes/MultiplayerConnect.tscn +++ b/scenes/MultiplayerConnect.tscn @@ -54,6 +54,11 @@ layout_mode = 2 size_flags_stretch_ratio = 2.41 text = "Create Frequency" +[node name="BackButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 10 +text = "Back" + [node name="RefreshButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer"] layout_mode = 2 text = "Refresh" @@ -137,6 +142,7 @@ text = "" autowrap_mode = 2 [connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"] +[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/BackButton" to="." method="_on_back_button_pressed"] [connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] [connection signal="item_clicked" from="VBoxContainer/ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] [connection signal="button_down" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index e1e5c00..13a5229 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -24,9 +24,11 @@ class PlayerData: var player_data := [ PlayerData.new(0, Color( 0, 255, 0), 880), - PlayerData.new(1, Color(255, 0, 0), 1200), - PlayerData.new(2, Color( 0, 0, 255), 600), - PlayerData.new(3, Color(255, 0, 255), 500), + PlayerData.new(1, Color(255, 0, 0), 1318), + PlayerData.new(2, Color( 0, 0, 255), 587), + PlayerData.new(3, Color(255, 0, 255), 440), + PlayerData.new(3, Color( 0, 255, 255), 783), + PlayerData.new(3, Color(255, 255, 0), 1567), ] func _ready() -> void: @@ -230,3 +232,7 @@ func _on_leave_button_pressed() -> void: func _on_error_label_gui_input(event: InputEvent) -> void: if event is InputEventMouseButton and event.pressed and event.button_index == 1: %ErrorLabel.text = "" + + +func _on_back_button_pressed() -> void: + get_tree().change_scene_to_file("res://scenes/main.tscn") From cfddacb278aa0a743beffe772c747128af803805 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Thu, 24 Apr 2025 15:02:11 +0200 Subject: [PATCH 10/11] Renaming / moving buttons Move "Leave" buttons always to upper right of screen. The "Get a Wav" button now has a text that expresses better what it does depending on the platform (save, download, share). --- scenes/MultiplayerConnect.tscn | 11 ++++++----- scenes/main.gd | 12 ++++++++---- scenes/main.tscn | 3 ++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scenes/MultiplayerConnect.tscn b/scenes/MultiplayerConnect.tscn index 90f7840..516a5d4 100644 --- a/scenes/MultiplayerConnect.tscn +++ b/scenes/MultiplayerConnect.tscn @@ -93,6 +93,11 @@ text = "Freq:" unique_name_in_owner = true layout_mode = 2 +[node name="LeaveButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 10 +text = "Leave Frequency" + [node name="PlayerContainer" type="VBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"] unique_name_in_owner = true layout_mode = 2 @@ -104,10 +109,6 @@ size_flags_vertical = 3 size_flags_stretch_ratio = 2.0 text = "MORSE" -[node name="LeaveButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer"] -layout_mode = 2 -text = "Leave Frequency" - [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 @@ -145,8 +146,8 @@ autowrap_mode = 2 [connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/BackButton" to="." method="_on_back_button_pressed"] [connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] [connection signal="item_clicked" from="VBoxContainer/ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"] +[connection signal="pressed" from="VBoxContainer/MorseView/VBoxContainer/HBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"] [connection signal="button_down" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"] [connection signal="button_up" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"] -[connection signal="pressed" from="VBoxContainer/MorseView/VBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"] [connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/Label" to="." method="_on_error_label_gui_input"] [connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/ErrorLabel" to="." method="_on_error_label_gui_input"] diff --git a/scenes/main.gd b/scenes/main.gd index 41fad80..d71bb6b 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -20,6 +20,12 @@ func fill_buffer(): phase = fmod(phase + increment, 1.0) func _ready(): + match OS.get_name(): + "Android": + %WavButton.text = "Share Wav" + "Web": + %WavButton.text = "Download Wav" + $Player.stream.mix_rate = sample_hz $Player.volume_db = -100 $Player.play() @@ -28,14 +34,12 @@ func _ready(): if multiplayer_enabled: %MultiplayerButton.visible = true - - - print("External freq: ", Utils.get_external_freq_param()) if Utils.get_external_freq_param(): + print("Direct connect to external freq: ", Utils.get_external_freq_param()) _on_multiplayer_button_pressed() - # FIXME: make this dependant on button + # FIXME: make this dependent on button OS.open_midi_inputs() print(OS.get_connected_midi_inputs()) diff --git a/scenes/main.tscn b/scenes/main.tscn index 6bec0ff..94699ae 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -55,9 +55,10 @@ size_flags_stretch_ratio = 2.0 text = "MORSE" [node name="WavButton" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 -text = "Write Wav" +text = "Save Wav" [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 From 807d9e2b0ff21e8093c13ef7b81f4df40599bf4f Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Thu, 24 Apr 2025 15:39:58 +0200 Subject: [PATCH 11/11] Make WebMIDI optional, MIDI for multiplayer We now only open MIDI by default on non-Web platforms. On web, there is an extra button to open MIDI devices. This allows us to no longer pester users with the "allow MIDI" popup when they don't want to use MIDI. If MIDI devices have been opened, the multiplayer part of this project now uses it as well, in the same way the "singleplayer" part does it. --- scenes/main.gd | 15 +++++++++++---- scenes/main.tscn | 9 +++++++++ scenes/multiplayer_connect.gd | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/scenes/main.gd b/scenes/main.gd index d71bb6b..aa5a678 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -32,16 +32,18 @@ func _ready(): playback = $Player.get_stream_playback() fill_buffer() + if OS.get_name() != "Web": + OS.open_midi_inputs() + print(OS.get_connected_midi_inputs()) + else: + %MidiButton.visible = true + if multiplayer_enabled: %MultiplayerButton.visible = true if Utils.get_external_freq_param(): print("Direct connect to external freq: ", Utils.get_external_freq_param()) _on_multiplayer_button_pressed() - - # FIXME: make this dependent on button - OS.open_midi_inputs() - print(OS.get_connected_midi_inputs()) func set_morse_state(state: bool): MorseState.set_state(state) @@ -163,3 +165,8 @@ func _on_reset_button_pressed() -> void: func _on_multiplayer_button_pressed() -> void: get_tree().change_scene_to_file("res://scenes/MultiplayerConnect.tscn") + + +func _on_midi_button_pressed() -> void: + OS.open_midi_inputs() + print(OS.get_connected_midi_inputs()) diff --git a/scenes/main.tscn b/scenes/main.tscn index 94699ae..128742c 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -64,6 +64,14 @@ text = "Save Wav" layout_mode = 2 size_flags_vertical = 3 +[node name="MidiButton" type="Button" parent="VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_stretch_ratio = 0.25 +text = "Open +Midi" + [node name="ResetButton" type="Button" parent="VBoxContainer/HBoxContainer"] layout_mode = 2 size_flags_horizontal = 3 @@ -89,5 +97,6 @@ script = ExtResource("3_sugp2") [connection signal="button_down" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_down"] [connection signal="button_up" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_up"] [connection signal="pressed" from="VBoxContainer/WavButton" to="." method="_on_wav_button_pressed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/MidiButton" to="." method="_on_midi_button_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"] [connection signal="pressed" from="VBoxContainer/HBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"] diff --git a/scenes/multiplayer_connect.gd b/scenes/multiplayer_connect.gd index 13a5229..fab6f27 100644 --- a/scenes/multiplayer_connect.gd +++ b/scenes/multiplayer_connect.gd @@ -225,6 +225,20 @@ func _on_morse_button_button_up() -> void: return set_morse_state(false) +func _input(input_event): + if input_event is InputEventMIDI: + _process_midi_event(input_event) + +func _process_midi_event(midi_event): + if not mmb_self: + return + + if midi_event.channel in [0, 9]: + if midi_event.message == MIDI_MESSAGE_NOTE_ON: + set_morse_state(true) + elif midi_event.message == MIDI_MESSAGE_NOTE_OFF: + set_morse_state(false) + func _on_leave_button_pressed() -> void: send_data({"cmd": "leave"})