Multiplayer Test #2

Merged
seba merged 11 commits from multiplayer-test into main 2025-05-10 21:13:42 +02:00
16 changed files with 900 additions and 9 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,153 @@
[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="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
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="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="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="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()
@ -19,14 +20,30 @@ 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()
playback = $Player.get_stream_playback()
fill_buffer()
OS.open_midi_inputs()
print(OS.get_connected_midi_inputs())
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()
func set_morse_state(state: bool):
MorseState.set_state(state)
@ -144,3 +161,12 @@ 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")
func _on_midi_button_pressed() -> void:
OS.open_midi_inputs()
print(OS.get_connected_midi_inputs())

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="."]
@ -53,16 +55,38 @@ 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="ResetButton" type="Button" parent="VBoxContainer"]
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
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
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 +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/ResetButton" to="." method="_on_reset_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"]

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,252 @@
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 _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"})
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()