Move server calls to MusicServerClient and give players stable ids

Server requests move out of Settings into a new MusicServerClient
autoload with one function per endpoint. It sends requests through a
swappable HttpTransport so tests can fake the server, and it frees its
HTTPRequest nodes after use.

Players now have a UUID that never changes. Their position in
Settings.player_array is only used for the Alt+<number> keybinds.
Previously the position doubled as the id, so removing a player made
points and wins land on the wrong player, and could delete another
player's point from a song.

Also fixes:
- format_time showed 59.6 seconds as "00:60"
- searches containing regex characters such as "Q*bert" broke search
- SongObject and PlayerObject were Nodes that were never freed; they
  are now RefCounted
- search matching is split out of the UI code into find_matches()
This commit is contained in:
Sansan committed 2026-10-06 08:32:55 +02:00
1 parent ab338b0696
commit 56e9fe66d3
18 files changed
+273 -154

No files matched your search

+26 -30
View File
@@ -257,7 +257,7 @@ func fetch_song_list_at_start() -> void:
print("Unexpected data") print("Unexpected data")
Playlist.update_last_played_song() Playlist.update_last_played_song()
update_song_list() update_song_list()
Settings.make_request2("/music/list", fetch_song_list_at_start_done, true) MusicServerClient.fetch_song_list(fetch_song_list_at_start_done)
func play_next_song() -> void: func play_next_song() -> void:
print("play_next_song ", Playlist.latest_played_song) print("play_next_song ", Playlist.latest_played_song)
@@ -277,11 +277,6 @@ func play_next_song() -> void:
func fetch_song() -> void: func fetch_song() -> void:
print("fetch_song") print("fetch_song")
Playlist.is_song_downloading = true Playlist.is_song_downloading = true
var url: String = ""
if Settings.use_low_played_mode:
url = "/music/rand/low"
else:
url = "/music/rand"
var new_song_object: SongObject = SongObject.new() var new_song_object: SongObject = SongObject.new()
var fetch_song_done: Callable = func fetch_song_done(body: PackedByteArray) -> void: var fetch_song_done: Callable = func fetch_song_done(body: PackedByteArray) -> void:
print("fetch_song_done") print("fetch_song_done")
@@ -306,11 +301,11 @@ func fetch_song() -> void:
Playlist.is_song_downloading = false Playlist.is_song_downloading = false
if Settings.is_debug: if Settings.is_debug:
update_song_list() update_song_list()
Settings.make_request2("/music/info", fetch_song_info_done, true) MusicServerClient.fetch_song_info(fetch_song_info_done)
Settings.make_request2("/music/addQue", added_to_que_done, false) MusicServerClient.add_to_queue(added_to_que_done)
Settings.make_request2(url, fetch_song_done, true) MusicServerClient.fetch_random_song(Settings.use_low_played_mode, fetch_song_done)
func update_song_list() -> void: func update_song_list() -> void:
print("update_song_list") print("update_song_list")
@@ -335,7 +330,7 @@ func song_object_clicked(event: InputEvent, song_no: int) -> void:
sound.data = body sound.data = body
Playlist.add_song_to_song_object(song_no, sound) Playlist.add_song_to_song_object(song_no, sound)
music_player_container.play_song_object(song_no) music_player_container.play_song_object(song_no)
Settings.make_request2("/music?song=" + str(song_no), song_downloaded, true) MusicServerClient.fetch_song(song_no, song_downloaded)
else: else:
print("song already downloaded") print("song already downloaded")
music_player_container.play_song_object(song_no) music_player_container.play_song_object(song_no)
@@ -347,7 +342,7 @@ func reset_playlist() -> void:
fetch_song_list_at_start() fetch_song_list_at_start()
fetch_song() fetch_song()
Settings.make_request2("/music/reset", playlist_reset_done, false) MusicServerClient.reset_playlist(playlist_reset_done)
func reset_points() -> void: func reset_points() -> void:
print("reset_points") print("reset_points")
@@ -412,7 +407,7 @@ func get_suggestion_list() -> void:
inspiration_list.add_child(inspiration_label) inspiration_list.add_child(inspiration_label)
else: else:
print("Unexpected data") print("Unexpected data")
Settings.make_request2("/music/all", populate_list, true) MusicServerClient.fetch_all_soundtracks(populate_list)
func add_players() -> void: func add_players() -> void:
print("add_players") print("add_players")
@@ -423,15 +418,15 @@ func add_players() -> void:
func add_player() -> void: func add_player() -> void:
print("add_player") print("add_player")
if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS:
var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text, Settings.player_array.size()) var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text)
new_player_name_field.text = "" new_player_name_field.text = ""
Settings.player_array.append(new_player_object) Settings.player_array.append(new_player_object)
new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first")) new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first"))
new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match")) new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match"))
new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object.id)) new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object))
new_player_object.connect("point_given_sound", _on_make_point_given_sound) new_player_object.connect("point_given_sound", _on_make_point_given_sound)
new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object.id)) new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object))
new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object.id)) new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object))
load_players() load_players()
func load_players() -> void: func load_players() -> void:
@@ -442,43 +437,44 @@ func load_players() -> void:
print(player_object.player_name) print(player_object.player_name)
var new_player: Player = player.instantiate() var new_player: Player = player.instantiate()
new_player.id = counter new_player.player_object = player_object
new_player.list_index = counter
new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name)) new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
new_player.connect("player_removed", _on_player_removed.bind(counter)) new_player.connect("player_removed", _on_player_removed.bind(player_object))
players_list_container.add_child(new_player) players_list_container.add_child(new_player)
counter += 1 counter += 1
func _on_point_given(player_given_point: int) -> void: func _on_point_given(player_object: PlayerObject) -> void:
print("_on_point_given") print("_on_point_given")
log_window.add_log_row(Settings.player_array[player_given_point].player_name + " got a point") log_window.add_log_row(player_object.player_name + " got a point")
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
Playlist.add_point(player_given_point) Playlist.add_point(player_object.id)
update_song_list() update_song_list()
func _on_make_point_given_sound() -> void: func _on_make_point_given_sound() -> void:
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav")) music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
func _on_point_taken(player_taken_point: int) -> void: func _on_point_taken(player_object: PlayerObject) -> void:
print("_on_point_taken") print("_on_point_taken")
log_window.add_log_row(Settings.player_array[player_taken_point].player_name + " lost a point") log_window.add_log_row(player_object.player_name + " lost a point")
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav")) music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
Playlist.remove_point(player_taken_point) Playlist.remove_point(player_object.id)
update_song_list() update_song_list()
func _on_player_won(winning_player_id: int) -> void: func _on_player_won(player_object: PlayerObject) -> void:
print("_on_player_won") print("_on_player_won")
winner_window.show_winner(winning_player_id) winner_window.show_winner(player_object)
music_player_container.play_song(preload("res://sounds/winning.mp3")) music_player_container.play_song(preload("res://sounds/winning.mp3"))
Settings.add_to_stats = false Settings.add_to_stats = false
func _on_player_removed(player_to_remove: int) -> void: func _on_player_removed(player_object: PlayerObject) -> void:
print("_on_player_removed ", player_to_remove) print("_on_player_removed ", player_object.player_name)
Settings.player_array.remove_at(player_to_remove) Settings.player_array.erase(player_object)
load_players() load_players()
Playlist.remove_player_from_list(player_to_remove) Playlist.remove_player_from_list(player_object.id)
update_song_list() update_song_list()
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void: func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
+5 -5
View File
@@ -63,12 +63,12 @@ func _process(_delta: float) -> void:
music_time_label.text = format_text(progress_slider.value, 0.0) music_time_label.text = format_text(progress_slider.value, 0.0)
func format_time(time: float) -> String: func format_time(time: float) -> String:
var mins: String = "%02d" % floor(time / 60) var total_seconds: int = roundi(time)
var sec: String = "%02d" % round(fmod(time, 60)) @warning_ignore("integer_division")
return mins + ":" + sec return "%02d:%02d" % [total_seconds / 60, total_seconds % 60]
func format_text(part: float, total: float) -> String: func format_text(part: float, total: float) -> String:
if (Settings.hide_beginning && part <= Settings.hide_for_seconds) && Settings.hide_length: if (Settings.hide_beginning && part < Settings.hide_for_seconds) && Settings.hide_length:
return "??:?? / ??:??" return "??:?? / ??:??"
elif Settings.hide_length: elif Settings.hide_length:
return format_time(part) + " / ??:??" return format_time(part) + " / ??:??"
@@ -209,7 +209,7 @@ func get_sound_test_song() -> void:
var sound: AudioStream = AudioStreamMP3.new() var sound: AudioStream = AudioStreamMP3.new()
sound.data = body sound.data = body
play_song(sound) play_song(sound)
Settings.make_request2("/music/soundTest", test_sound, true) MusicServerClient.fetch_sound_test_song(test_sound)
##### LOCAL ##### LOCAL
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3' var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
+17 -13
View File
@@ -19,15 +19,19 @@ var remove_player := $HBoxContainer/RemovePlayer
signal change_character_clicked signal change_character_clicked
signal player_removed signal player_removed
@export ## The player shown in this row.
var id: int var player_object: PlayerObject
## Position in Settings.player_array. Only used for the keybinds: Alt+<list_index + 1>
## gives this player a point and Ctrl+Alt+<list_index + 1> takes one away.
var list_index: int
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
add_button.pressed.connect(add_point) add_button.pressed.connect(add_point)
add_button.set_shortcut(create_button_shortcut(id, false)) add_button.set_shortcut(create_button_shortcut(list_index, false))
minus_button.pressed.connect(minus_point) minus_button.pressed.connect(minus_point)
minus_button.set_shortcut(create_button_shortcut(id, true)) minus_button.set_shortcut(create_button_shortcut(list_index, true))
character.pressed.connect(change_character) character.pressed.connect(change_character)
remove_player.pressed.connect(func() -> void: player_removed.emit()) remove_player.pressed.connect(func() -> void: player_removed.emit())
update_score() update_score()
@@ -36,21 +40,21 @@ func _ready() -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
remove_player.visible = Settings.edit_players remove_player.visible = Settings.edit_players
func create_button_shortcut(scancode : int, ctrl_pressed: bool) -> Shortcut: func create_button_shortcut(index: int, ctrl_pressed: bool) -> Shortcut:
var button_shortcut: Shortcut = Shortcut.new() var button_shortcut: Shortcut = Shortcut.new()
var button_event : InputEventKey = InputEventKey.new() var button_event : InputEventKey = InputEventKey.new()
button_event.keycode = scancode + 49 button_event.keycode = KEY_1 + index
button_event.alt_pressed = true button_event.alt_pressed = true
button_event.ctrl_pressed = ctrl_pressed button_event.ctrl_pressed = ctrl_pressed
button_shortcut.events.append(button_event) button_shortcut.events.append(button_event)
return button_shortcut return button_shortcut
func add_point() -> void: func add_point() -> void:
Settings.player_array[id].add_point() player_object.add_point()
update_score() update_score()
func minus_point() -> void: func minus_point() -> void:
Settings.player_array[id].minus_point() player_object.minus_point()
update_score() update_score()
func change_character() -> void: func change_character() -> void:
@@ -58,22 +62,22 @@ func change_character() -> void:
func character_selected(file_name: String) -> void: func character_selected(file_name: String) -> void:
print("Back in player list with: " + file_name) print("Back in player list with: " + file_name)
Settings.player_array[id].character = load("res://characters/" + file_name) player_object.character = load("res://characters/" + file_name)
set_player_character() set_player_character()
func new_character_selected(character_texture: ImageTexture) -> void: func new_character_selected(character_texture: ImageTexture) -> void:
print("Back in player list with new") print("Back in player list with new")
Settings.player_array[id].character = character_texture player_object.character = character_texture
set_player_character() set_player_character()
func set_player_character() -> void: func set_player_character() -> void:
character.custom_minimum_size = Vector2(80, 40) character.custom_minimum_size = Vector2(80, 40)
character.ignore_texture_size = true character.ignore_texture_size = true
character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
character.texture_normal = Settings.player_array[id].character character.texture_normal = player_object.character
func update_score() -> void: func update_score() -> void:
if OS.has_feature("debug"): if OS.has_feature("debug"):
player_name_field.text = str(Settings.player_array[id].id) + " " + Settings.player_array[id].player_name + ": " + str(Settings.player_array[id].player_score) player_name_field.text = str(list_index + 1) + " " + player_object.player_name + ": " + str(player_object.player_score)
else: else:
player_name_field.text = Settings.player_array[id].player_name + ": " + str(Settings.player_array[id].player_score) player_name_field.text = player_object.player_name + ": " + str(player_object.player_score)
+12 -5
View File
@@ -1,4 +1,4 @@
extends Node extends RefCounted
class_name PlayerObject class_name PlayerObject
signal first_point_triggerd signal first_point_triggerd
@@ -8,8 +8,8 @@ signal point_given_sound
signal player_point_given signal player_point_given
signal player_point_taken signal player_point_taken
@export ## Unique id that never changes, unlike the player's position in Settings.player_array.
var id: int var id: String = _generate_uuid()
@export @export
var player_name: String var player_name: String
@@ -21,11 +21,18 @@ var is_first_point: bool = true
var character: Texture = load("res://noCharacter.png") var character: Texture = load("res://noCharacter.png")
func _init(new_name: String, new_id: int) -> void: func _init(new_name: String) -> void:
player_name = new_name player_name = new_name
id = new_id
player_score = 0 player_score = 0
## Returns a random version 4 UUID, e.g. "3f2b8c1e-9a4d-4e7f-b2c6-5d8e1f0a7b93".
static func _generate_uuid() -> String:
var bytes: PackedByteArray = Crypto.new().generate_random_bytes(16)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
var hex: String = bytes.hex_encode()
return "%s-%s-%s-%s-%s" % [hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), hex.substr(16, 4), hex.substr(20, 12)]
func add_point() -> void: func add_point() -> void:
if player_score < Settings.winning_score: if player_score < Settings.winning_score:
player_score += 1 player_score += 1
+26 -17
View File
@@ -66,12 +66,18 @@ func perform_search(search_text: String) -> void:
child.visible = false child.visible = false
label_pool.append(child) label_pool.append(child)
for soundtrack: String in find_matches(search_text):
add_soundtrack_reuse(soundtrack)
## Returns the soundtracks matching [param search_text], best matches first: plain
## case-insensitive matches, then matches ignoring spaces and special characters,
## then matches with the letters in order but not next to each other.
func find_matches(search_text: String) -> Array[String]:
var seen: Dictionary = {} var seen: Dictionary = {}
var search_text_lower: String = search_text.to_lower() var search_text_lower: String = search_text.to_lower()
var clean_search_text: String = clean_term(search_text) var clean_search_text: String = clean_term(search_text)
# Collect all matches first, then display in order var matches: Array[String] = []
var matches: Array = []
# Pass 1: Exact case-insensitive contains match on lowercase versions # Pass 1: Exact case-insensitive contains match on lowercase versions
for i in range(precomputed_soundtracks.size()): for i in range(precomputed_soundtracks.size()):
@@ -98,9 +104,7 @@ func perform_search(search_text: String) -> void:
seen[st_data["original"]] = true seen[st_data["original"]] = true
matches.append(st_data["original"]) matches.append(st_data["original"])
# Display all matches in the order they appear in soundtracks return matches
for soundtrack: String in matches:
add_soundtrack_reuse(soundtrack)
# Cleans a term by removing spaces and replacing special characters. # Cleans a term by removing spaces and replacing special characters.
# Matches MusicServer's clean_term() function. # Matches MusicServer's clean_term() function.
@@ -113,6 +117,9 @@ func compile_regex_cached(search_term: String) -> RegEx:
var regex = RegEx.new() var regex = RegEx.new()
var regText: String = ".*" var regText: String = ".*"
for letter: String in search_term: for letter: String in search_term:
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
if "\\^$.|?*+()[]{}".contains(letter):
regText += "\\"
regText += letter + ".*" regText += letter + ".*"
regex.compile(regText) regex.compile(regText)
regex_cache[search_term] = regex regex_cache[search_term] = regex
@@ -149,26 +156,28 @@ func get_list_of_soundtracks() -> void:
print("get_list_of_soundtracks") print("get_list_of_soundtracks")
var handle_soundtracks: Callable = func handle_soundtracks(array) -> void: var handle_soundtracks: Callable = func handle_soundtracks(array) -> void:
if typeof(array) == TYPE_ARRAY: if typeof(array) == TYPE_ARRAY:
soundtracks = []
precomputed_soundtracks = []
# Reuse existing labels: hide all and add to pool # Reuse existing labels: hide all and add to pool
for child: Label in search_list.get_children(): for child: Label in search_list.get_children():
child.visible = false child.visible = false
label_pool.append(child) label_pool.append(child)
set_soundtracks(array)
soundtracks.append_array(array)
# Precompute cleaned versions for all soundtracks
for soundtrack: String in soundtracks: for soundtrack: String in soundtracks:
var cleaned = clean_term(soundtrack)
precomputed_soundtracks.append({
"original": soundtrack,
"lowercase": soundtrack.to_lower(),
"cleaned": cleaned
})
add_soundtrack_reuse(soundtrack) add_soundtrack_reuse(soundtrack)
else: else:
print("Unexpected data") print("Unexpected data")
Settings.make_request2("/music/all/order", handle_soundtracks, true) MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks)
## Replaces the searchable soundtracks and precomputes their search forms.
func set_soundtracks(list: Array) -> void:
soundtracks = []
precomputed_soundtracks = []
soundtracks.append_array(list)
for soundtrack: String in soundtracks:
precomputed_soundtracks.append({
"original": soundtrack,
"lowercase": soundtrack.to_lower(),
"cleaned": clean_term(soundtrack)
})
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if self.visible == true: if self.visible == true:
-60
View File
@@ -30,66 +30,6 @@ var character_select_open: bool = false
var version: String = "1.9.0" var version: String = "1.9.0"
func make_request2(address: String, func_name: Callable, expect_data: bool) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
print("request done to address: ", default_path + address + " response_code: ", response_code)
if response_code == 200:
print("func_name: ", func_name.get_method())
if !expect_data:
func_name.call()
else:
var json: JSON = JSON.new()
var error: int = json.parse(body.get_string_from_utf8())
if error == OK:
var data_received = json.get_data()
print("data_received type: ", type_string(typeof(data_received)))
if typeof(data_received) == TYPE_ARRAY:
func_name.call(data_received)
elif func_name != null:
func_name.call(data_received)
else:
print("data_received type: ", type_string(typeof(body)))
func_name.call(body)
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
print("address: ", default_path + address)
var request_error: int = http_request.request(default_path + address)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func make_request3(address: String) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void:
print("request done to address: ", default_path + address + " response_code: ", response_code)
if response_code != 200:
print("Error: " + str(response_code))
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
var request_error: int = http_request.request(default_path + address)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func make_put_request(address: String) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void:
print("request done to address: ", default_path + address + " response_code: ", response_code)
if response_code != 200:
print("Error: " + str(response_code))
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
var headers: PackedStringArray = ["Content-Type: application/json"]
var request_error: int = http_request.request(default_path + address, headers, HTTPClient.METHOD_PUT)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func delete_children(node: Node) -> void: func delete_children(node: Node) -> void:
for n: Node in node.get_children(): for n: Node in node.get_children():
node.remove_child(n) node.remove_child(n)
+8 -9
View File
@@ -1,4 +1,4 @@
extends Node extends RefCounted
class_name SongObject class_name SongObject
@@ -8,7 +8,8 @@ var song_title: String
var soundtrack_title: String var soundtrack_title: String
var players_given_point: Array[int] = [] ## Ids of the players who got a point for this song.
var players_given_point: Array[String] = []
var is_answered: bool = false var is_answered: bool = false
@@ -40,16 +41,14 @@ func get_song_info() -> String:
else: else:
return "??? - ???" return "??? - ???"
func add_point(id: int) -> void: func add_point(player_id: String) -> void:
print("add_point") print("add_point")
players_given_point.append(id) if !players_given_point.has(player_id):
players_given_point.sort() players_given_point.append(player_id)
func remove_point(id: int) -> void: func remove_point(player_id: String) -> void:
print("remove_point") print("remove_point")
if players_given_point.has(id): players_given_point.erase(player_id)
players_given_point.remove_at(players_given_point.find(id))
players_given_point.sort()
func _to_string() -> String: func _to_string() -> String:
var format_string: String = "song_number: %d | soundtrack_title: %s | song_title: %s | is_answered: %s | has_played: %s | is_playing: %s" var format_string: String = "song_number: %d | soundtrack_title: %s | song_title: %s | is_answered: %s | has_played: %s | is_playing: %s"
+1 -1
View File
@@ -35,7 +35,7 @@ func set_character_name(character_name: String) -> void:
character_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL character_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
character_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED character_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
character_image.texture = new_image character_image.texture = new_image
Settings.make_request2("/character?name=" + character_name.uri_encode(), image_fetched, true) MusicServerClient.fetch_character_image(character_name, image_fetched)
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if self.visible == true && Settings.character_select_open == true: if self.visible == true && Settings.character_select_open == true:
+37
View File
@@ -0,0 +1,37 @@
class_name HttpTransport
extends RefCounted
## Sends HTTP requests for [code]MusicServerClient[/code] using [HTTPRequest] nodes.
##
## Tests subclass this to answer requests without a network connection.
var _owner: Node
func _init(owner: Node) -> void:
_owner = owner
## Requests [param url] and calls [param on_complete] with
## [code](response_code: int, body: PackedByteArray)[/code]. Connection errors are
## reported with response code 0.
func request(url: String, method: int, on_complete: Callable) -> void:
var http_request: HTTPRequest = HTTPRequest.new()
_owner.add_child(http_request)
http_request.request_completed.connect(
func(
result: int,
response_code: int,
_headers: PackedStringArray,
body: PackedByteArray,
) -> void:
http_request.queue_free()
on_complete.call(response_code if result == HTTPRequest.RESULT_SUCCESS else 0, body)
)
var headers: PackedStringArray = []
if method == HTTPClient.METHOD_PUT:
headers.append("Content-Type: application/json")
var error: int = http_request.request(url, headers, method)
if error != OK:
http_request.queue_free()
push_error("Could not send request to %s (error %d)" % [url, error])
on_complete.call(0, PackedByteArray())
+1
View File
@@ -0,0 +1 @@
uid://uinuvqrxeppl
+124
View File
@@ -0,0 +1,124 @@
extends Node
## Client for the MusicServer REST API.
##
## Registered as the [code]MusicServerClient[/code] autoload, so every script can call it
## directly, e.g. [code]MusicServerClient.fetch_song_list(on_done)[/code]. All HTTP traffic
## goes through [member transport]; tests replace it with a fake to answer requests
## without a running server.
## Sends the actual HTTP requests. Swap it out in tests.
var transport: HttpTransport
func _ready() -> void:
if transport == null:
transport = HttpTransport.new(self)
## Fetches the songs already in the server playlist, as an Array of
## [code]{SongNo, Soundtrack, Song}[/code] dictionaries.
func fetch_song_list(on_done: Callable) -> void:
get_json("/music/list", on_done)
## Downloads a random song as MP3 bytes. With [param low_played] the server prefers
## soundtracks that have been played the least.
func fetch_random_song(low_played: bool, on_done: Callable) -> void:
get_bytes("/music/rand/low" if low_played else "/music/rand", on_done)
## Adds the most recently fetched random song to the server playlist.
func add_to_queue(on_done: Callable) -> void:
send("/music/addQue", on_done)
## Fetches [code]{SongNo, Soundtrack, Song}[/code] for the most recently queued song.
func fetch_song_info(on_done: Callable) -> void:
get_json("/music/info", on_done)
## Downloads a song from the server playlist as MP3 bytes.
func fetch_song(song_no: int, on_done: Callable) -> void:
get_bytes("/music?song=" + str(song_no), on_done)
## Downloads the sound test song as MP3 bytes.
func fetch_sound_test_song(on_done: Callable) -> void:
get_bytes("/music/soundTest", on_done)
## Clears the server playlist.
func reset_playlist(on_done: Callable) -> void:
send("/music/reset", on_done)
## Fetches every soundtrack name as an Array of Strings.
func fetch_all_soundtracks(on_done: Callable) -> void:
get_json("/music/all", on_done)
## Fetches every soundtrack name, sorted, as an Array of Strings.
func fetch_all_soundtracks_ordered(on_done: Callable) -> void:
get_json("/music/all/order", on_done)
## Records in the server statistics that a song has been played.
func mark_song_played(song_no: int) -> void:
send("/music/played?song=" + str(song_no), Callable(), HTTPClient.METHOD_PUT)
## Fetches the names of all character images.
func fetch_character_list(on_done: Callable) -> void:
get_json("/characters", on_done)
## Downloads a character image as PNG bytes.
func fetch_character_image(character_name: String, on_done: Callable) -> void:
get_bytes("/character?name=" + character_name.uri_encode(), on_done)
## Starts syncing the server's soundtrack library with its music folder.
func start_sync(on_done: Callable) -> void:
send("/sync", on_done)
## Fetches the sync state: [code]{progress, time_spent}[/code] while running, or the
## sync result once finished.
func fetch_sync_progress(on_done: Callable) -> void:
get_json("/sync/progress", on_done)
## Requests [param path] and calls [param on_done] with the parsed JSON.
## Nothing is called if the request fails or the body is not valid JSON.
func get_json(path: String, on_done: Callable) -> void:
_request(path, HTTPClient.METHOD_GET, func(body: PackedByteArray) -> void:
var json: JSON = JSON.new()
if json.parse(body.get_string_from_utf8()) != OK:
push_warning("Invalid JSON from %s: %s" % [path, json.get_error_message()])
return
on_done.call(json.data)
)
## Requests [param path] and calls [param on_done] with the raw response body.
func get_bytes(path: String, on_done: Callable) -> void:
_request(path, HTTPClient.METHOD_GET, on_done)
## Requests [param path] and calls [param on_done] with no arguments when it succeeds.
## [param on_done] may be an empty Callable when the result does not matter.
func send(path: String, on_done: Callable, method: int = HTTPClient.METHOD_GET) -> void:
_request(path, method, func(_body: PackedByteArray) -> void:
if on_done.is_valid():
on_done.call()
)
func _request(path: String, method: int, on_success: Callable) -> void:
var url: String = Settings.default_path + path
transport.request(url, method, func(response_code: int, body: PackedByteArray) -> void:
if response_code != 200:
push_warning("Request to %s failed with response code %d" % [url, response_code])
return
on_success.call(body)
)
+1
View File
@@ -0,0 +1 @@
uid://cbb0fpkj7k38w
+1 -1
View File
@@ -45,7 +45,7 @@ func load_characters() -> void:
character_box.size = Vector2(235, 145) character_box.size = Vector2(235, 145)
character_box.set_character_name(character_name) character_box.set_character_name(character_name)
character_box.connect("new_character_selected", _on_character_selected) character_box.connect("new_character_selected", _on_character_selected)
Settings.make_request2("/characters", fetch_character_list, true) MusicServerClient.fetch_character_list(fetch_character_list)
func show_grid(player_name: String) -> void: func show_grid(player_name: String) -> void:
print("show_grid") print("show_grid")
+7 -6
View File
@@ -40,7 +40,7 @@ func song_is_answered(song_no: int = currently_playing_song) -> void:
song_object_array[song_no].is_answered = true song_object_array[song_no].is_answered = true
if Settings.add_to_stats: if Settings.add_to_stats:
print("add to stats: ", song_no) print("add to stats: ", song_no)
Settings.make_put_request("/music/played?song="+str(song_no)) MusicServerClient.mark_song_played(song_no)
func add_to_playlist(new_song_object: SongObject) -> void: func add_to_playlist(new_song_object: SongObject) -> void:
song_object_array.append(new_song_object) song_object_array.append(new_song_object)
@@ -76,15 +76,16 @@ func get_current_soundtrack_title() -> String:
func get_current_song_title() -> String: func get_current_song_title() -> String:
return song_object_array[currently_playing_song].get_song_title() return song_object_array[currently_playing_song].get_song_title()
func add_point(player_id: int) -> void: func add_point(player_id: String) -> void:
song_object_array[currently_playing_song].add_point(player_id) song_object_array[currently_playing_song].add_point(player_id)
func remove_point(player_id: int) -> void: func remove_point(player_id: String) -> void:
song_object_array[currently_playing_song].remove_point(player_id) song_object_array[currently_playing_song].remove_point(player_id)
func remove_player_from_list(player_to_remove: int) -> void: ## Removes a player's points from every song.
for song_list_object: SongObject in song_object_array: func remove_player_from_list(player_id: String) -> void:
song_object_array[song_list_object.song_number].players_given_point.remove_at(song_object_array[song_list_object.song_number].players_given_point.find(player_to_remove)) for song_object: SongObject in song_object_array:
song_object.remove_point(player_id)
func get_song(song_no: int) -> AudioStream: func get_song(song_no: int) -> AudioStream:
return song_object_array[song_no].song return song_object_array[song_no].song
+1
View File
@@ -23,6 +23,7 @@ config/icon="res://icon.svg"
Settings="*res://Settings.gd" Settings="*res://Settings.gd"
Playlist="*res://playlist.gd" Playlist="*res://playlist.gd"
MusicServerClient="*res://music_server_client.gd"
[debug] [debug]
+2 -2
View File
@@ -39,9 +39,9 @@ func set_label() -> void:
func update_players() -> void: func update_players() -> void:
Settings.delete_children(song_list_players) Settings.delete_children(song_list_players)
if song_object != null: if song_object != null:
for player_id: int in song_object.players_given_point: # Show players in the same order as the player list
for player: PlayerObject in Settings.player_array: for player: PlayerObject in Settings.player_array:
if player.id == player_id: if song_object.players_given_point.has(player.id):
var texture: TextureRect = TextureRect.new() var texture: TextureRect = TextureRect.new()
texture.texture = player.character texture.texture = player.character
texture.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL texture.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+2 -2
View File
@@ -105,7 +105,7 @@ func start_sync() -> void:
sync_started.emit() sync_started.emit()
show_sync_progress() show_sync_progress()
if !Settings.currently_syncing: if !Settings.currently_syncing:
Settings.make_request2("/sync", sync_request_sent, false) MusicServerClient.start_sync(sync_request_sent)
func _on_sync_timeout() -> void: func _on_sync_timeout() -> void:
print("_on_sync_timeout!") print("_on_sync_timeout!")
@@ -118,7 +118,7 @@ func _on_sync_timeout() -> void:
sync_time.text = data_received.time_spent if data_received.has("time_spent") else "" sync_time.text = data_received.time_spent if data_received.has("time_spent") else ""
else: else:
on_sync_finished(data_received) on_sync_finished(data_received)
Settings.make_request2("/sync/progress", get_progress, true) MusicServerClient.fetch_sync_progress(get_progress)
func on_sync_finished(data_received: Dictionary) -> void: func on_sync_finished(data_received: Dictionary) -> void:
has_synced = true has_synced = true
+2 -3
View File
@@ -17,10 +17,9 @@ func _ready() -> void:
visible = false visible = false
func show_winner(winning_player_id: int) -> void: func show_winner(winner: PlayerObject) -> void:
visible = true visible = true
# Set winner info # Set winner info
var winner: PlayerObject = Settings.player_array[winning_player_id]
winner_label.text = winner.player_name + " won with " + str(winner.player_score) + \ winner_label.text = winner.player_name + " won with " + str(winner.player_score) + \
" points!!" " points!!"
winner_picture.texture = winner.character winner_picture.texture = winner.character
@@ -41,7 +40,7 @@ func show_winner(winning_player_id: int) -> void:
var placing: int = 2 var placing: int = 2
for player: PlayerObject in sorted_players: for player: PlayerObject in sorted_players:
if player.id == winning_player_id: if player == winner:
continue # Skip the winner, already shown above continue # Skip the winner, already shown above
var label: Label = Label.new() var label: Label = Label.new()
label.text = str(placing) + ". " + player.player_name + " - " + \ label.text = str(placing) + ". " + player.player_name + " - " + \