Compare commits

...

9 Commits

Author SHA1 Message Date
Sebastian Lohff e48271ed95 Better player tones, back button 2025-04-14 03:09:23 +02:00
Sebastian Lohff aca5a97162 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.
2025-04-14 02:46:15 +02:00
Sebastian Lohff 5dda7af184 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.
2025-04-13 20:07:06 +02:00
Sebastian Lohff f3732b09d5 Also add Utils script 2025-04-13 18:34:24 +02:00
Sebastian Lohff 0e6ab98c6f MP: Logging, test js parameters (broken) 2025-04-13 18:09:00 +02:00
Sebastian Lohff fb500705ef Working Multiplayer
...now with a GUI Theme! Players can leave a room as well. Actually
usable on mobile!
2025-04-13 00:20:10 +02:00
Sebastian Lohff 970f5d951b WIP: Current state 2025-04-10 01:16:05 +02:00
Sebastian Lohff cd8befccba WIP: Multiplayer broken state 2025-04-09 21:50:13 +02:00
Sebastian Lohff e59a36a922 WIP: Multiplayer test 2025-03-21 01:41:49 +01:00
16 changed files with 860 additions and 5 deletions

9
autoloads/utils.gd Normal file
View File

@ -0,0 +1,9 @@
extends Node
var first_connect_done := false
func get_external_freq_param():
match OS.get_name():
"Web":
return JavaScriptBridge.eval("new URL(window.location.href).searchParams.get('freq')")
return null

1
autoloads/utils.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://ua7dk3y0v21e

View File

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

View File

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

View File

@ -20,6 +20,7 @@ config/icon="res://icon.svg"
[autoload]
MorseState="*res://autoloads/morse_state.gd"
Utils="*res://autoloads/utils.gd"
[display]

4
scenes/GUITheme.tres Normal file
View File

@ -0,0 +1,4 @@
[gd_resource type="Theme" format=3 uid="uid://xxoc27tvaiut"]
[resource]
default_font_size = 30

View File

@ -0,0 +1,21 @@
[gd_scene load_steps=3 format=3 uid="uid://ug3u6jf36dst"]
[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)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
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

View File

@ -0,0 +1,152 @@
[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"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_2wc0w")
script = ExtResource("1_uyd8l")
[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
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ConnectView/VBoxContainer"]
layout_mode = 2
[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="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "MHz"
[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="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"
[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="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
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Freq:"
[node name="FreqLabel" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[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="VBoxContainer/MorseView/VBoxContainer"]
layout_mode = 2
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
[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 = "<None>"
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"]
[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"]

View File

@ -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,7 +25,17 @@ func _ready():
$Player.play()
playback = $Player.get_stream_playback()
fill_buffer()
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())
@ -144,3 +155,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")

View File

@ -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,12 +59,25 @@ 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/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="."]
stream = SubResource("AudioStreamGenerator_kvn5v")
volume_db = -80.0
@ -73,4 +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/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"]

View File

@ -0,0 +1,111 @@
@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)
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 := 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() - morse_state.start_time) * 1000.0
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:
# 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()
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)

View File

@ -0,0 +1 @@
uid://b1k6j1jti114u

View File

@ -0,0 +1,238 @@
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 ws_last_status = -1
var autoconnect_to_freq: String
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), 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:
# FIXME: connection handling / reconnect
# 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("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 _process(_delta: float) -> void:
ws.poll()
var state := ws.get_ready_state()
while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count():
_handle_packet()
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()
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("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"])
"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)
"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"])
"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"])
"leave":
_leave_freq()
"error":
%ErrorLabel.text = parsed["message"]
_:
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, player_id)
for n in range(other_players.size()):
make_player(n + 1, other_players[n])
%FreqLabel.text = "%s MHz" % freq
%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"
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")
func make_player(no: int, player_id: String):
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)
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
set_morse_state(true)
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"})
func _on_error_label_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
%ErrorLabel.text = "<cleared>"
func _on_back_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/main.tscn")

View File

@ -0,0 +1 @@
uid://di8r70441xdms

204
signalsrv/signalsrv.py Normal file
View File

@ -0,0 +1,204 @@
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 = {}
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} id {self.id} disconnected: {exc}")
if self.curr_freq:
await self._leave_room()
@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:
await self._send_error("Invalid format in json")
continue
print(f"{datetime.datetime.now()} {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 "leave":
await self._leave_room()
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":
await self._handle_morse_state(data)
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:
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
self.freqs[freq] = [self]
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:
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.curr_freq = freq
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,
other_players=[c.id for c in self._others(freq)])
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:
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
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
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:
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
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]
async def _send(self, ignore_exceptions=False, **kwargs):
data = json.dumps(kwargs).encode()
print(f" --> sending out to {self.client}: {data}")
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)
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 = "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())

81
signalsrv/signalsrv2.py Normal file
View File

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