Merge basic multiplayer support
Websocket based with python protocol server. Could upgrade this to WebRTC at some point by using the current protocol server as signalling server. Sometimes merges together separate dits to a big da, as the websockets don't flush their data fast enough.
This commit is contained in:
commit
0de9c11fc7
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
uid://ua7dk3y0v21e
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,6 +20,7 @@ config/icon="res://icon.svg"
|
|||
[autoload]
|
||||
|
||||
MorseState="*res://autoloads/morse_state.gd"
|
||||
Utils="*res://autoloads/utils.gd"
|
||||
|
||||
[display]
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[gd_resource type="Theme" format=3 uid="uid://xxoc27tvaiut"]
|
||||
|
||||
[resource]
|
||||
default_font_size = 30
|
|
@ -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
|
|
@ -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"]
|
|
@ -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())
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://b1k6j1jti114u
|
|
@ -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")
|
|
@ -0,0 +1 @@
|
|||
uid://di8r70441xdms
|
|
@ -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())
|
|
@ -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()
|
Loading…
Reference in New Issue