Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8263781abc | ||
|
|
0862d160e5 | ||
|
|
c86dacb890 | ||
|
|
f39865fffc | ||
|
|
c03c0e4efe | ||
|
|
06421bb63e | ||
|
|
ef6ac8f1fd | ||
|
|
a735c515bf | ||
|
|
0fc3be79e1 | ||
|
|
ae231975bd | ||
|
|
b9fa3c3d63 | ||
|
|
de94691073 | ||
|
|
94e8dfb602 | ||
|
|
70e970b24c | ||
|
|
e4352a9f34 | ||
|
|
4a934840f6 | ||
|
|
78e1facaf0 | ||
|
|
84e137e16a | ||
|
|
a4e6da4c28 | ||
|
|
cd4331881c | ||
|
|
06f457b027 | ||
|
|
222d231b1b | ||
|
|
b120440404 | ||
|
|
41974ea5e3 | ||
|
|
240c331ab2 | ||
|
|
62fa63fd8b | ||
|
|
4a45e8516d | ||
|
|
d21b3e2ce0 | ||
|
|
92e3a66075 | ||
|
|
b510aa47f2 | ||
|
|
8d21b0617d | ||
|
|
b2d547b650 | ||
|
|
845d08c260 | ||
|
|
13fdb3f68c | ||
|
|
e1e05f3904 | ||
|
|
b40b160fde | ||
|
|
a21b6e9b22 | ||
|
|
f7619baf34 | ||
|
|
8b71723a34 | ||
|
|
a7bf38c5f9 | ||
|
|
5519afb5f6 | ||
|
|
d77b72159d | ||
|
|
a0428e3475 | ||
|
|
674604425f | ||
|
|
390da577d7 | ||
|
|
b7d67b573e | ||
|
|
5831e1e73d | ||
|
|
5cdec5cc8f | ||
|
|
e94c75e84d | ||
|
|
bad4b3ad8b | ||
|
|
8c1987807c | ||
|
|
3c7b4e5c9d | ||
|
|
75c59eb223 | ||
|
|
10e9e7b2a4 | ||
|
|
29b11560b5 | ||
|
|
2a332c2d19 | ||
|
|
6cbbfee6a1 | ||
|
|
c7d49bc5cc | ||
|
|
6288739de0 | ||
|
|
d79f5f92c5 | ||
|
|
14c166614d | ||
|
|
428decf73a | ||
|
|
739d0f7af6 | ||
|
|
ed292877ef | ||
|
|
5a979fca48 | ||
|
|
1acc757981 | ||
|
|
16724922ed | ||
|
|
c1fc24adf9 | ||
|
|
f67dccbf52 | ||
|
|
c985a6ce5b | ||
|
|
56e9fe66d3 | ||
|
|
ab338b0696 | ||
|
|
9a91d969bd |
No files matched your search
@@ -1,64 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var about_panel: Panel = %AboutPanel
|
||||
|
||||
@onready
|
||||
var version_label: Label = %VersionLabel
|
||||
|
||||
@onready
|
||||
var whats_new_label: Label = %WhatsNewLabel
|
||||
|
||||
|
||||
|
||||
# Changelog as Dictionary with version as key and description as value
|
||||
const WHATS_NEW: Dictionary = {
|
||||
"1.9.0": "Made improvements to the Search window
|
||||
Made small changes to the inspiration list scrolling
|
||||
Made small changes to the player list
|
||||
Changed the term game to soundtrack
|
||||
#28: Made changes to the sync window
|
||||
#34: Fixed bug in Character Select when clicking or scrolling",
|
||||
"1.8.5": "#9: New winner screen with positions of all players
|
||||
#32: New setting for how long to hide the beginning
|
||||
#33: Fixed bug with hiding beginning when song restarts",
|
||||
"1.8.0": "#14: Updated About page
|
||||
#15: Create new keymap page
|
||||
#20: The winner song can now be stopped
|
||||
#24: The speed of the inspiration list can now be changed
|
||||
#29: New settings to hide time left and things like that
|
||||
#31: Added a QR page",
|
||||
"1.7.0": "#22: Now block add player if player name is empty
|
||||
#23: Add a log for when points are given to players
|
||||
#25: Fixed some graphical stuff
|
||||
#26: Changed so the same character can be on a song multiple times
|
||||
#27: Turning off statistics after win",
|
||||
"1.6.1": "#17: Removed Debug text in release
|
||||
#18: Fixed bug with new character select screen",
|
||||
"1.6.0": "#1: Fixed bug with inspiration list not reloading after sync
|
||||
#2: New dialog for sync
|
||||
#4: Now downloads characters from the server
|
||||
#5: New character select screen
|
||||
#6: Now show progress during sync
|
||||
#7: Blocking all requests to the server during sync
|
||||
#8: Added functionality to cache more than one song",
|
||||
"1.5.0": "Made big changes to players and the song list and how the local song list works",
|
||||
"0.9.0-Beta": "Fixed settings and updated the player view",
|
||||
"0.8.0-Beta": "Fixed reset buttons and some other small things",
|
||||
"0.7.8-Beta": "Added shortcuts. Added dialog for winner. Started cleaning code.",
|
||||
"0.7.5-Beta": "Added settings menu, most things don't do anything yet",
|
||||
"0.7-Beta": "Can now hop between songs"
|
||||
}
|
||||
|
||||
|
||||
func open() -> void:
|
||||
super()
|
||||
version_label.text = Settings.version
|
||||
|
||||
# Build changelog text with version headers
|
||||
var changelog_text: String = ""
|
||||
for version: String in WHATS_NEW.keys():
|
||||
changelog_text += "\n" + version + "\n"
|
||||
changelog_text += WHATS_NEW[version] + "\n"
|
||||
|
||||
whats_new_label.text = changelog_text
|
||||
@@ -1,61 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://cp54idaudb8dp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b7bh0of7olbcb" path="res://AboutWindow.gd" id="1_about_script"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[node name="AboutControl" type="Control" unique_id=1626010355 node_paths=PackedStringArray("panel")]
|
||||
theme = ExtResource("popup_theme")
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_about_script")
|
||||
panel = NodePath("AboutPanel")
|
||||
|
||||
[node name="AboutPanel" type="Panel" parent="." unique_id=1907299275]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 600.0
|
||||
offset_bottom = 640.0
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="AboutPanel" unique_id=200295721]
|
||||
layout_mode = 0
|
||||
offset_right = 600.0
|
||||
offset_bottom = 50.0
|
||||
text = "About"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="AboutPanel" unique_id=417677913]
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 50.0
|
||||
offset_right = 580.0
|
||||
offset_bottom = 630.0
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="ContentVBox" type="VBoxContainer" parent="AboutPanel/ScrollContainer" unique_id=1746337912]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="VersionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=352884126]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="DescriptionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=543094263]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Try your video game music knowledge with this VGM randomizer, invite your friends and see who is the best."
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="WhatsNewLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=1363040291]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
autowrap_mode = 2
|
||||
@@ -1,38 +0,0 @@
|
||||
extends ScrollContainer
|
||||
|
||||
# Scroll direction: 1 = down, -1 = up
|
||||
var scroll_direction: int = 1
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
scroll_direction = 1
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
var speed_pixels_per_second: float = float(Settings.inspiration_list_speed) * 60.0
|
||||
var scroll_amount: float = speed_pixels_per_second * delta * float(scroll_direction)
|
||||
|
||||
var scroll_bar = get_v_scroll_bar()
|
||||
|
||||
# Don't scroll if list is empty or too short
|
||||
if scroll_bar.max_value <= 0:
|
||||
return
|
||||
|
||||
var current_pos = scroll_bar.value
|
||||
|
||||
if scroll_direction == 1:
|
||||
# Scrolling down
|
||||
if current_pos + scroll_bar.page < scroll_bar.max_value:
|
||||
scroll_bar.value = min(current_pos + scroll_amount, scroll_bar.max_value - scroll_bar.page)
|
||||
else:
|
||||
# Reached bottom, reverse direction
|
||||
scroll_direction = -1
|
||||
scroll_bar.value = scroll_bar.max_value - scroll_bar.page
|
||||
else:
|
||||
# Scrolling up
|
||||
if current_pos > 0:
|
||||
scroll_bar.value = max(current_pos + scroll_amount, 0)
|
||||
else:
|
||||
# Reached top, reverse direction
|
||||
scroll_direction = 1
|
||||
scroll_bar.value = 0
|
||||
@@ -1,7 +0,0 @@
|
||||
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://rxexo3ur85as"]
|
||||
|
||||
[ext_resource type="StyleBox" uid="uid://blf3g88mugovr" path="res://neutral_gray.tres" id="1_alhp2"]
|
||||
|
||||
[resource]
|
||||
Label/styles/normal = ExtResource("1_alhp2")
|
||||
PanelContainer/styles/panel = ExtResource("1_alhp2")
|
||||
-572
@@ -1,572 +0,0 @@
|
||||
extends Control
|
||||
|
||||
# 601 LOC 11/9 - 2023
|
||||
# 628 LOC 15/9 - 2023
|
||||
# 631 LOC 16/9 - 2023
|
||||
# 554 LOC 30/5 - 2024
|
||||
# 551 LOC 3/6 - 2024
|
||||
# 386 LOC 12/6 - 2024
|
||||
|
||||
##TODO
|
||||
# 15. Fix for local play
|
||||
|
||||
@onready
|
||||
var open_button: Button = $Open
|
||||
|
||||
@onready
|
||||
var fileDialog: FileDialog = $FileDialog
|
||||
|
||||
@onready
|
||||
var inspiration_scroll: ScrollContainer = $ScrollContainer
|
||||
|
||||
@onready
|
||||
var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer
|
||||
|
||||
@onready
|
||||
var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel
|
||||
|
||||
@onready
|
||||
var song_label: Label = $VBoxContainer/SongLabel
|
||||
|
||||
@onready
|
||||
var add_player_container: HBoxContainer = $Players/VBoxContainer/AddPlayerContainer
|
||||
|
||||
@onready
|
||||
var add_players_button: TextureButton = $Players/VBoxContainer/HBoxContainer/AddPlayersButton
|
||||
|
||||
@onready
|
||||
var add_player_button: Button = $Players/VBoxContainer/AddPlayerContainer/AddPlayerButton
|
||||
|
||||
@onready
|
||||
var new_player_name_field: TextEdit = $Players/VBoxContainer/AddPlayerContainer/PlayerNameField
|
||||
|
||||
@onready
|
||||
var reset_playlist_button: Button = $ResetPlaylistButton
|
||||
|
||||
@onready
|
||||
var reset_points_button: Button = $ResetPointsButton
|
||||
|
||||
@onready
|
||||
var sound_test_button: Button = $SoundTestButton
|
||||
|
||||
@onready
|
||||
var sync_button: Button = $SyncButton
|
||||
|
||||
@onready
|
||||
var sync_window: Control = %SyncWindow
|
||||
|
||||
@onready
|
||||
var settings_button: Button = $SettingsButton
|
||||
|
||||
@onready
|
||||
var settings_window: Control = $SettingsWindow
|
||||
|
||||
@onready
|
||||
var shortcuts_window: Control = %ShortcutsWindow
|
||||
|
||||
@onready
|
||||
var qr_window: Control = %QRWindow
|
||||
|
||||
@onready
|
||||
var winner_window: Control = %WinnerWindow
|
||||
|
||||
@onready
|
||||
var qr_button: Button = $QRButton
|
||||
|
||||
@onready
|
||||
var statistics_button: Button = $StatisticsButton
|
||||
|
||||
@onready
|
||||
var statistic_popup: PopupPanel = $StatisticsPopupPanel
|
||||
|
||||
@onready
|
||||
var statistic_label: Label = $StatisticsPopupPanel/StatisticsLabel
|
||||
|
||||
@onready
|
||||
var shortcuts_button: Button = $ShortcutsButton
|
||||
|
||||
@onready
|
||||
var about_button: Button = $AboutButton
|
||||
|
||||
@onready
|
||||
var about_window: Control = %AboutWindow
|
||||
|
||||
@onready
|
||||
var show_answer_button: Button = $ShowAnswerButton
|
||||
|
||||
@onready
|
||||
var next_button: Button = $NextButton
|
||||
|
||||
@onready
|
||||
var music_list_scroll: ScrollContainer = $MusicListPanel/ScrollContainer
|
||||
|
||||
@onready
|
||||
var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList
|
||||
|
||||
@onready
|
||||
var player_container: VBoxContainer = $Players/VBoxContainer
|
||||
|
||||
@onready
|
||||
var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer
|
||||
|
||||
@onready
|
||||
var new_character_select: Control = %NewCharacterSelect
|
||||
|
||||
@onready
|
||||
var search_button: Button = $SearchButton
|
||||
|
||||
@onready
|
||||
var search_view: Control= $Search
|
||||
|
||||
@onready
|
||||
var auto_repeat_song_button: CheckButton = $RepeatSongCheckButton
|
||||
|
||||
@onready
|
||||
var music_player_container: PanelContainer = $MusicPlayer
|
||||
|
||||
@onready
|
||||
var log_window: Control = %Log
|
||||
|
||||
@onready
|
||||
var debug_label: Label = $DebugLabel
|
||||
|
||||
var player: PackedScene = preload("res://Player.tscn")
|
||||
var song_list_object: PackedScene = preload("res://song_list_object.tscn")
|
||||
|
||||
var songs: Array= []
|
||||
var soundtracks: Array = []
|
||||
var current_player: Node
|
||||
|
||||
## True while statistics are off because a player won, until points are reset.
|
||||
var stats_paused_by_win: bool = false
|
||||
|
||||
## Dims the window behind an open popup and keeps clicks from reaching the buttons under it.
|
||||
var popup_backdrop: ColorRect
|
||||
|
||||
## Seconds to wait before retrying a song download step that failed.
|
||||
var song_fetch_retry_seconds: float = 5.0
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
_setup_popup_backdrop()
|
||||
var debug: bool = OS.has_feature("debug")
|
||||
var is_mac: bool = OS.has_feature("macos")
|
||||
if debug:
|
||||
print("is_debug")
|
||||
debug_label.visible = true
|
||||
Settings.is_debug = true
|
||||
settings_window.set_winning_score(5)
|
||||
settings_window.set_selected_server(4)
|
||||
if is_mac:
|
||||
print("is_mac")
|
||||
next_button.pressed.connect(play_next_song)
|
||||
show_answer_button.pressed.connect(show_answer_pressed)
|
||||
search_button.pressed.connect(show_search)
|
||||
sync_button.pressed.connect(sync_soundtracks)
|
||||
sound_test_button.pressed.connect(music_player_container.get_sound_test_song)
|
||||
statistics_button.pressed.connect(get_statistics)
|
||||
shortcuts_button.pressed.connect(show_shortcuts)
|
||||
about_button.pressed.connect(show_about)
|
||||
settings_button.pressed.connect(show_settings)
|
||||
qr_button.pressed.connect(show_qr)
|
||||
reset_playlist_button.pressed.connect(reset_playlist)
|
||||
reset_points_button.pressed.connect(reset_points)
|
||||
|
||||
new_character_select.connect("new_new_character_selected", _on_new_character_selected)
|
||||
new_player_name_field.connect("enter_key_pressed", add_player)
|
||||
music_player_container.connect("play_next_song", play_next_song)
|
||||
music_player_container.connect("update_song_list", update_song_list)
|
||||
settings_window.connect("server_changed", server_updated)
|
||||
settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
|
||||
sync_window.connect("sync_started", _on_sync_started)
|
||||
sync_window.connect("sync_finished", _on_sync_finished)
|
||||
|
||||
|
||||
add_players_button.pressed.connect(add_players)
|
||||
add_player_button.pressed.connect(add_player)
|
||||
open_button.pressed.connect(open)
|
||||
auto_repeat_song_button.pressed.connect(repeat_song)
|
||||
|
||||
get_suggestion_list()
|
||||
fetch_song_list_at_start()
|
||||
search_view.get_list_of_soundtracks()
|
||||
new_character_select.load_characters()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.alt_pressed && event.keycode == KEY_UP:
|
||||
print("Alt + UP pressed")
|
||||
music_player_container.change_volume(0.05)
|
||||
if event.alt_pressed && event.keycode == KEY_DOWN:
|
||||
print("Alt + DOWN pressed")
|
||||
music_player_container.change_volume(-0.05)
|
||||
if event.alt_pressed && event.ctrl_pressed && event.keycode == KEY_LEFT:
|
||||
print("Ctrl + Alt + LEFT pressed")
|
||||
music_player_container.restart()
|
||||
elif event.alt_pressed && event.keycode == KEY_LEFT:
|
||||
print("Alt + LEFT pressed")
|
||||
music_player_container.seek(-5)
|
||||
if event.alt_pressed && event.keycode == KEY_RIGHT:
|
||||
print("Alt + RIGHT pressed")
|
||||
music_player_container.seek(5)
|
||||
if event.alt_pressed && event.keycode == KEY_ENTER:
|
||||
print("Alt + Enter pressed")
|
||||
Settings.fullscreen = !Settings.fullscreen
|
||||
settings_window.fullscreen_button.button_pressed = Settings.fullscreen
|
||||
if Settings.fullscreen == true:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||
if Settings.fullscreen == false:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||
if event.alt_pressed && event.keycode == KEY_K:
|
||||
shortcuts_window.toggle()
|
||||
if event.alt_pressed && event.keycode == KEY_L:
|
||||
log_window.toggle()
|
||||
|
||||
func server_updated() -> void:
|
||||
print("server_updated")
|
||||
update_from_server()
|
||||
reset_points()
|
||||
|
||||
func update_from_server() -> void:
|
||||
print("update_from_server")
|
||||
search_view.get_list_of_soundtracks()
|
||||
get_suggestion_list()
|
||||
reset_playlist()
|
||||
new_character_select.load_characters()
|
||||
|
||||
func repeat_song() -> void:
|
||||
Settings.auto_repeat_song = !Settings.auto_repeat_song
|
||||
|
||||
func show_answer_pressed() -> void:
|
||||
print("show_answer_pressed")
|
||||
Playlist.song_is_answered()
|
||||
update_song_list()
|
||||
|
||||
# Loads the songs already in the server playlist, then starts preloading new ones.
|
||||
# Preloading waits for the list, because loading the list clears the playlist.
|
||||
func fetch_song_list_at_start() -> void:
|
||||
print("fetch_song_list_at_start")
|
||||
var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void:
|
||||
Playlist.clear_song_list()
|
||||
if typeof(data) == TYPE_ARRAY:
|
||||
for d: Dictionary in data:
|
||||
var new_song_object: SongObject = SongObject.new()
|
||||
new_song_object.song_number = d.SongNo
|
||||
new_song_object.soundtrack_title = d.Soundtrack
|
||||
new_song_object.song_title = d.Song
|
||||
new_song_object.is_answered = true
|
||||
new_song_object.has_played = true
|
||||
Playlist.add_to_playlist(new_song_object)
|
||||
elif data != null:
|
||||
print("Unexpected data")
|
||||
Playlist.update_last_played_song()
|
||||
update_song_list()
|
||||
if !Playlist.is_song_downloading:
|
||||
fetch_song()
|
||||
MusicServerClient.fetch_song_list(fetch_song_list_at_start_done,
|
||||
_retry_later.bind(fetch_song_list_at_start))
|
||||
|
||||
func play_next_song() -> void:
|
||||
print("play_next_song ", Playlist.latest_played_song)
|
||||
if Playlist.first_song_played:
|
||||
Playlist.set_latest_played_song()
|
||||
Playlist.first_song_played = true
|
||||
music_player_container.play_song_object(Playlist.latest_played_song)
|
||||
if !(Playlist.get_preloaded_songs() > 0):
|
||||
next_button.disabled = true
|
||||
if !Playlist.is_song_downloading:
|
||||
fetch_song()
|
||||
|
||||
#not first "/music/addPlayed"
|
||||
#"/music/rand"
|
||||
#"/music/addQue"
|
||||
#"/music/info"
|
||||
func _popups() -> Array[PopupWindow]:
|
||||
return [search_view, sync_window, settings_window, qr_window, new_character_select,
|
||||
shortcuts_window, about_window, winner_window, log_window]
|
||||
|
||||
# The backdrop sits above the main window and below every popup. Popups close
|
||||
# themselves when clicked outside, and the backdrop keeps that click from reaching
|
||||
# the buttons behind them.
|
||||
func _setup_popup_backdrop() -> void:
|
||||
popup_backdrop = ColorRect.new()
|
||||
popup_backdrop.name = "PopupBackdrop"
|
||||
popup_backdrop.color = Color(0, 0, 0, 0.55)
|
||||
popup_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(popup_backdrop)
|
||||
popup_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
for popup: PopupWindow in _popups():
|
||||
move_child(popup, -1)
|
||||
popup.visibility_changed.connect(_update_popup_backdrop)
|
||||
_update_popup_backdrop()
|
||||
|
||||
func _update_popup_backdrop() -> void:
|
||||
popup_backdrop.visible = _popups().any(func(popup: PopupWindow) -> bool: return popup.visible)
|
||||
|
||||
# The cache size can be changed in the settings. Waiting until the settings are closed
|
||||
# avoids starting downloads while the user is still clicking the number up and down.
|
||||
func _on_settings_window_visibility_changed() -> void:
|
||||
if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs():
|
||||
fetch_song()
|
||||
|
||||
func fetch_song() -> void:
|
||||
print("fetch_song")
|
||||
Playlist.is_song_downloading = true
|
||||
_download_random_song(SongObject.new())
|
||||
|
||||
# Fetching a song takes three requests. If one fails, only that step is retried, so
|
||||
# the server and the game keep the same songs in their playlists.
|
||||
func _download_random_song(new_song_object: SongObject) -> void:
|
||||
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||
print("song_downloaded")
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = body
|
||||
new_song_object.song = sound
|
||||
_add_song_to_queue(new_song_object)
|
||||
MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded,
|
||||
_retry_later.bind(_download_random_song.bind(new_song_object)))
|
||||
|
||||
func _add_song_to_queue(new_song_object: SongObject) -> void:
|
||||
MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object),
|
||||
_retry_later.bind(_add_song_to_queue.bind(new_song_object)))
|
||||
|
||||
func _fetch_song_info(new_song_object: SongObject) -> void:
|
||||
var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void:
|
||||
print("song_info_fetched")
|
||||
new_song_object.song_number = data_received.SongNo
|
||||
new_song_object.soundtrack_title = data_received.Soundtrack
|
||||
new_song_object.song_title = data_received.Song
|
||||
print("new_song_object: ", new_song_object.to_string())
|
||||
Playlist.add_to_playlist(new_song_object)
|
||||
next_button.disabled = false
|
||||
if Playlist.should_preload_more_songs():
|
||||
fetch_song()
|
||||
else:
|
||||
Playlist.is_song_downloading = false
|
||||
if Settings.is_debug:
|
||||
update_song_list()
|
||||
MusicServerClient.fetch_song_info(song_info_fetched,
|
||||
_retry_later.bind(_fetch_song_info.bind(new_song_object)))
|
||||
|
||||
func _retry_later(step: Callable) -> void:
|
||||
print("Song download failed, retrying in %s seconds" % song_fetch_retry_seconds)
|
||||
get_tree().create_timer(song_fetch_retry_seconds).timeout.connect(step)
|
||||
|
||||
func update_song_list() -> void:
|
||||
print("update_song_list")
|
||||
if Playlist.currently_playing_song >= 0:
|
||||
soundtrack_label.text = Playlist.get_current_soundtrack_title()
|
||||
song_label.text = Playlist.get_current_song_title()
|
||||
Settings.delete_children(music_list)
|
||||
for s: SongObject in Playlist.get_playlist():
|
||||
var new_song_list_object: SongListObject = song_list_object.instantiate()
|
||||
new_song_list_object.song_object = s
|
||||
new_song_list_object.gui_input.connect(song_object_clicked.bind(s.song_number))
|
||||
music_list.add_child(new_song_list_object)
|
||||
|
||||
func song_object_clicked(event: InputEvent, song_no: int) -> void:
|
||||
if (event is InputEventMouseButton && event.pressed && event.button_index == MOUSE_BUTTON_LEFT):
|
||||
print("Clicked Song Object: ", Playlist.song_object_array[song_no].to_string())
|
||||
if Settings.character_select_open == false:
|
||||
if Playlist.get_song(song_no) == null:
|
||||
print("has no song, need to download")
|
||||
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = body
|
||||
Playlist.add_song_to_song_object(song_no, sound)
|
||||
music_player_container.play_song_object(song_no)
|
||||
MusicServerClient.fetch_song(song_no, song_downloaded)
|
||||
else:
|
||||
print("song already downloaded")
|
||||
music_player_container.play_song_object(song_no)
|
||||
|
||||
func reset_playlist() -> void:
|
||||
print("reset_playlist")
|
||||
var playlist_reset_done: Callable = func playlist_reset_done() -> void:
|
||||
Playlist.reset_playlist()
|
||||
fetch_song_list_at_start()
|
||||
|
||||
MusicServerClient.reset_playlist(playlist_reset_done)
|
||||
|
||||
func reset_points() -> void:
|
||||
print("reset_points")
|
||||
for player_objects: PlayerObject in Settings.player_array:
|
||||
player_objects.reset_points()
|
||||
load_players()
|
||||
# A new game starts, so turn statistics back on if winning turned them off
|
||||
if stats_paused_by_win:
|
||||
Settings.add_to_stats = true
|
||||
stats_paused_by_win = false
|
||||
|
||||
func show_about() -> void:
|
||||
print("show_about")
|
||||
about_window.open()
|
||||
|
||||
func show_shortcuts() -> void:
|
||||
print("show_shortcuts")
|
||||
shortcuts_window.open()
|
||||
|
||||
func show_settings() -> void:
|
||||
print("show_settings")
|
||||
settings_window.open()
|
||||
|
||||
func show_qr() -> void:
|
||||
print("show_qr")
|
||||
qr_window.open()
|
||||
|
||||
func get_statistics() -> void:
|
||||
print("get_statistics")
|
||||
statistic_popup.visible = true
|
||||
statistic_label.text = "Total amount of soundtracks in the playlist: " + str(soundtracks.size())
|
||||
|
||||
func sync_soundtracks() -> void:
|
||||
print("sync_soundtracks")
|
||||
sync_window.start_sync()
|
||||
|
||||
func _on_sync_started() -> void:
|
||||
print("_on_sync_started")
|
||||
sync_button.disabled = false
|
||||
sound_test_button.disabled = true
|
||||
reset_playlist_button.disabled = true
|
||||
show_answer_button.disabled = true
|
||||
next_button.disabled = true
|
||||
|
||||
func _on_sync_finished() -> void:
|
||||
print("_on_sync_finished")
|
||||
update_from_server()
|
||||
sound_test_button.disabled = false
|
||||
reset_playlist_button.disabled = false
|
||||
show_answer_button.disabled = false
|
||||
reset_points()
|
||||
|
||||
func get_suggestion_list() -> void:
|
||||
print("get_suggestion_list")
|
||||
var populate_list: Callable = func populate_list(array) -> void:
|
||||
if typeof(array) == TYPE_ARRAY:
|
||||
soundtracks = []
|
||||
Settings.delete_children(inspiration_list)
|
||||
soundtracks.append_array(array)
|
||||
for i: int in soundtracks.size():
|
||||
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
|
||||
else:
|
||||
print("Unexpected data")
|
||||
MusicServerClient.fetch_all_soundtracks(populate_list)
|
||||
|
||||
func add_players() -> void:
|
||||
print("add_players")
|
||||
Settings.edit_players = !Settings.edit_players
|
||||
add_player_container.visible = !add_player_container.visible
|
||||
new_player_name_field.grab_focus()
|
||||
|
||||
func add_player() -> void:
|
||||
print("add_player")
|
||||
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)
|
||||
new_player_name_field.text = ""
|
||||
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("match_point_triggerd", music_player_container._on_point_triggered.bind("match"))
|
||||
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("player_point_given", _on_point_given.bind(new_player_object))
|
||||
new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object))
|
||||
load_players()
|
||||
|
||||
func load_players() -> void:
|
||||
print("load_players")
|
||||
Settings.delete_children(players_list_container)
|
||||
var counter: int = 0
|
||||
for player_object: PlayerObject in Settings.player_array:
|
||||
print(player_object.player_name)
|
||||
|
||||
var new_player: Player = player.instantiate()
|
||||
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("player_removed", _on_player_removed.bind(player_object))
|
||||
|
||||
players_list_container.add_child(new_player)
|
||||
counter += 1
|
||||
|
||||
func _on_point_given(player_object: PlayerObject) -> void:
|
||||
print("_on_point_given")
|
||||
log_window.add_log_row(player_object.player_name + " got a point")
|
||||
if Playlist.currently_playing_song >= 0:
|
||||
Playlist.add_point(player_object.id)
|
||||
update_song_list()
|
||||
|
||||
func _on_make_point_given_sound() -> void:
|
||||
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
|
||||
|
||||
func _on_point_taken(player_object: PlayerObject) -> void:
|
||||
print("_on_point_taken")
|
||||
log_window.add_log_row(player_object.player_name + " lost a point")
|
||||
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
|
||||
if Playlist.currently_playing_song >= 0:
|
||||
Playlist.remove_point(player_object.id)
|
||||
update_song_list()
|
||||
|
||||
func _on_player_won(player_object: PlayerObject) -> void:
|
||||
print("_on_player_won")
|
||||
winner_window.show_winner(player_object)
|
||||
music_player_container.play_song(preload("res://sounds/winning.mp3"))
|
||||
# Songs played after the game is won don't count towards the statistics
|
||||
if Settings.add_to_stats:
|
||||
Settings.add_to_stats = false
|
||||
stats_paused_by_win = true
|
||||
|
||||
func _on_player_removed(player_object: PlayerObject) -> void:
|
||||
print("_on_player_removed ", player_object.player_name)
|
||||
Settings.player_array.erase(player_object)
|
||||
load_players()
|
||||
Playlist.remove_player_from_list(player_object.id)
|
||||
update_song_list()
|
||||
|
||||
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
|
||||
print("_on_player_change_character_clicked")
|
||||
current_player = new_player
|
||||
print("Change character for: " + player_name)
|
||||
new_character_select.show_grid(player_name)
|
||||
|
||||
func _on_new_character_selected(character_texture: ImageTexture) -> void:
|
||||
print("_on_new_character_selected")
|
||||
current_player.new_character_selected(character_texture)
|
||||
|
||||
func show_search() -> void:
|
||||
print("show_search")
|
||||
search_view.toggle()
|
||||
|
||||
|
||||
###Local
|
||||
|
||||
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
|
||||
|
||||
func open() -> void:
|
||||
fileDialog.popup()
|
||||
|
||||
func _on_file_dialog_dir_selected(path: String) -> void:
|
||||
print(path)
|
||||
dir_contents(path)
|
||||
|
||||
func dir_contents(path: String) -> void:
|
||||
var dir: DirAccess = DirAccess.open(path)
|
||||
if dir:
|
||||
dir.list_dir_begin()
|
||||
var file_name: String = dir.get_next()
|
||||
songs.clear()
|
||||
soundtracks = []
|
||||
while file_name != "":
|
||||
if dir.current_is_dir():
|
||||
#print("Found directory: " + file_name)
|
||||
soundtracks.append(file_name)
|
||||
else:
|
||||
#print("Found file: " + file_name)
|
||||
if file_name.ends_with(".mp3"):
|
||||
songs.append(path + "/" + file_name)
|
||||
file_name = dir.get_next()
|
||||
else:
|
||||
print("An error occurred when trying to access the path.")
|
||||
|
||||
for i: int in soundtracks.size():
|
||||
inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
|
||||
-502
@@ -1,502 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://xwq863o6uvsu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://MainWindow.gd" id="1_eu0t5"]
|
||||
[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"]
|
||||
[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"]
|
||||
[ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"]
|
||||
[ext_resource type="PackedScene" uid="uid://cdy4kvemwaiom" path="res://new_character_select.tscn" id="6_dhrvg"]
|
||||
[ext_resource type="Script" uid="uid://gf1bk6xbe3j" path="res://MusicListScrollContainer.gd" id="7_dj026"]
|
||||
[ext_resource type="Script" uid="uid://cgakje8gb37tf" path="res://PlayerNameField.gd" id="7_qsdfy"]
|
||||
[ext_resource type="Theme" uid="uid://rxexo3ur85as" path="res://LightGrayTheme.tres" id="7_wxbv6"]
|
||||
[ext_resource type="PackedScene" uid="uid://bxydgil1yifps" path="res://SearchWindow.tscn" id="9_5ijvr"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpdem7pdxweb5" path="res://SyncWindow.tscn" id="10_yxw0b"]
|
||||
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://SettingsWindow.tscn" id="11_k62u5"]
|
||||
[ext_resource type="PackedScene" uid="uid://bd1by80q1v27e" path="res://shortcutsWindow.tscn" id="13_8eebo"]
|
||||
[ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://Log.tscn" id="14_26rwn"]
|
||||
[ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://QRWindow.tscn" id="15_qr_window"]
|
||||
[ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://AboutWindow.tscn" id="16_about_window"]
|
||||
[ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_ychxr"]
|
||||
font_size = 25
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_qspbu"]
|
||||
font_size = 50
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_3m52w"]
|
||||
font_size = 35
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_03bm3"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 65
|
||||
unicode = 63743
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_jafqj"]
|
||||
events = [SubResource("InputEventKey_03bm3")]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qk7sj"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k1ygi"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_06rl4"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 86
|
||||
unicode = 8249
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_a7fvb"]
|
||||
events = [SubResource("InputEventKey_06rl4")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_7dnqw"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 67
|
||||
unicode = 231
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_d6fml"]
|
||||
events = [SubResource("InputEventKey_7dnqw")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_ujjlu"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 83
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_fbju4"]
|
||||
events = [SubResource("InputEventKey_ujjlu")]
|
||||
|
||||
[node name="Control" type="Control" unique_id=407149696]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_eu0t5")
|
||||
|
||||
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
|
||||
layout_mode = 0
|
||||
offset_right = 1920.0
|
||||
offset_bottom = 56.0
|
||||
|
||||
[node name="Title" type="Label" parent="." unique_id=1187907861]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 12.0
|
||||
offset_right = 369.0
|
||||
offset_bottom = 50.0
|
||||
size_flags_horizontal = 0
|
||||
text = "Music Player Randomizer"
|
||||
label_settings = SubResource("LabelSettings_ychxr")
|
||||
|
||||
[node name="Open" type="Button" parent="." unique_id=165052329]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 1296.0
|
||||
offset_top = 66.0
|
||||
offset_right = 1346.0
|
||||
offset_bottom = 97.0
|
||||
text = "Open"
|
||||
|
||||
[node name="FileDialog" type="FileDialog" parent="." unique_id=485771559]
|
||||
title = "Open a Directory"
|
||||
initial_position = 2
|
||||
size = Vector2i(616, 400)
|
||||
ok_button_text = "Select Current Folder"
|
||||
file_mode = 2
|
||||
access = 2
|
||||
show_hidden_files = true
|
||||
|
||||
[node name="MusicListPanel" type="PanelContainer" parent="." unique_id=2011292375]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 384.0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 896.0
|
||||
theme = ExtResource("7_wxbv6")
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="MusicListPanel" unique_id=707750459]
|
||||
layout_mode = 2
|
||||
follow_focus = true
|
||||
script = ExtResource("7_dj026")
|
||||
|
||||
[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761]
|
||||
layout_mode = 0
|
||||
offset_left = 1216.0
|
||||
offset_top = 128.0
|
||||
offset_right = 1857.0
|
||||
offset_bottom = 512.0
|
||||
horizontal_scroll_mode = 0
|
||||
vertical_scroll_mode = 3
|
||||
script = ExtResource("2_gxtxm")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=42468498]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 384.0
|
||||
|
||||
[node name="SoundtrackLabel" type="Label" parent="VBoxContainer" unique_id=173147793]
|
||||
custom_minimum_size = Vector2(1, 1)
|
||||
layout_mode = 2
|
||||
label_settings = SubResource("LabelSettings_qspbu")
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="SongLabel" type="Label" parent="VBoxContainer" unique_id=1687490027]
|
||||
custom_minimum_size = Vector2(1, 1)
|
||||
layout_mode = 2
|
||||
label_settings = SubResource("LabelSettings_3m52w")
|
||||
horizontal_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
|
||||
layout_mode = 0
|
||||
anchors_preset = 0
|
||||
anchor_left = 0.0
|
||||
anchor_top = 0.0
|
||||
anchor_right = 0.0
|
||||
anchor_bottom = 0.0
|
||||
offset_left = 65.0
|
||||
offset_top = 992.0
|
||||
offset_right = 1856.0
|
||||
offset_bottom = 1024.0
|
||||
grow_horizontal = 1
|
||||
grow_vertical = 1
|
||||
|
||||
[node name="Players" type="PanelContainer" parent="." unique_id=1708375945]
|
||||
layout_mode = 0
|
||||
offset_left = 1216.0
|
||||
offset_top = 576.0
|
||||
offset_right = 1857.0
|
||||
offset_bottom = 977.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Players" unique_id=270084211]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=513047023]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
text = "Players"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_jafqj")
|
||||
texture_normal = ExtResource("5_31tjv")
|
||||
|
||||
[node name="AddPlayerContainer" type="HBoxContainer" parent="Players/VBoxContainer" unique_id=1371948739]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Panel" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=1569854056]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_qk7sj")
|
||||
|
||||
[node name="PlayerNameField" type="TextEdit" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=332535327]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Player name"
|
||||
script = ExtResource("7_qsdfy")
|
||||
|
||||
[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
text = "Add"
|
||||
|
||||
[node name="Panel2" type="Panel" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=60215448]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi")
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 15
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 15
|
||||
|
||||
[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620]
|
||||
layout_mode = 0
|
||||
offset_left = 1360.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1471.0
|
||||
offset_bottom = 95.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Reset playlist"
|
||||
|
||||
[node name="ResetPointsButton" type="Button" parent="." unique_id=942880776]
|
||||
layout_mode = 0
|
||||
offset_left = 1488.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1599.0
|
||||
offset_bottom = 95.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Reset points"
|
||||
|
||||
[node name="SyncButton" type="Button" parent="." unique_id=1228313556]
|
||||
layout_mode = 0
|
||||
offset_left = 1616.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1759.0
|
||||
offset_bottom = 95.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Sync soundtracks"
|
||||
|
||||
[node name="SoundTestButton" type="Button" parent="." unique_id=1332971879]
|
||||
layout_mode = 0
|
||||
offset_left = 1776.0
|
||||
offset_top = 64.0
|
||||
offset_right = 1887.0
|
||||
offset_bottom = 95.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Sound test"
|
||||
|
||||
[node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039]
|
||||
layout_mode = 0
|
||||
offset_left = 65.0
|
||||
offset_top = 944.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 975.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_a7fvb")
|
||||
text = "Show answer"
|
||||
|
||||
[node name="NextButton" type="Button" parent="." unique_id=1744703488]
|
||||
layout_mode = 0
|
||||
offset_left = 193.0
|
||||
offset_top = 944.0
|
||||
offset_right = 368.0
|
||||
offset_bottom = 975.0
|
||||
focus_mode = 0
|
||||
disabled = true
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_d6fml")
|
||||
text = "Randomize new track"
|
||||
|
||||
[node name="SearchButton" type="Button" parent="." unique_id=1782453581]
|
||||
layout_mode = 0
|
||||
offset_left = 1354.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1424.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_fbju4")
|
||||
text = "Search"
|
||||
|
||||
[node name="SettingsButton" type="Button" parent="." unique_id=1193400323]
|
||||
layout_mode = 0
|
||||
offset_left = 1442.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1512.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Settings"
|
||||
|
||||
[node name="StatisticsButton" type="Button" parent="." unique_id=2045021269]
|
||||
layout_mode = 0
|
||||
offset_left = 1530.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1600.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Statistics"
|
||||
|
||||
[node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226]
|
||||
layout_mode = 0
|
||||
offset_left = 1626.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1696.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Shortcuts"
|
||||
|
||||
[node name="AboutButton" type="Button" parent="." unique_id=1057035253]
|
||||
layout_mode = 0
|
||||
offset_left = 1722.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1792.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "About"
|
||||
|
||||
[node name="QRButton" type="Button" parent="." unique_id=2021908559]
|
||||
layout_mode = 0
|
||||
offset_left = 1802.0
|
||||
offset_top = 8.0
|
||||
offset_right = 1872.0
|
||||
offset_bottom = 48.0
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "QR"
|
||||
|
||||
[node name="Search" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 56.0
|
||||
offset_top = 96.0
|
||||
offset_right = 56.0
|
||||
offset_bottom = 96.0
|
||||
|
||||
[node name="SyncWindow" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 210.0
|
||||
offset_top = 153.0
|
||||
offset_right = 210.0
|
||||
offset_bottom = 153.0
|
||||
|
||||
[node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
|
||||
[node name="QRWindow" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
|
||||
[node name="NewCharacterSelect" parent="." unique_id=1679856322 instance=ExtResource("6_dhrvg")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 405.0
|
||||
offset_top = 165.0
|
||||
offset_right = 1557.0
|
||||
offset_bottom = 805.0
|
||||
|
||||
[node name="ShortcutsWindow" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 562.0
|
||||
offset_top = 160.0
|
||||
offset_right = 1262.0
|
||||
offset_bottom = 860.0
|
||||
|
||||
[node name="StatisticsPopupPanel" type="PopupPanel" parent="." unique_id=298311407]
|
||||
theme = ExtResource("popup_theme")
|
||||
oversampling_override = 1.0
|
||||
initial_position = 2
|
||||
size = Vector2i(450, 100)
|
||||
|
||||
[node name="Label" type="Label" parent="StatisticsPopupPanel" unique_id=548316493]
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = 446.0
|
||||
offset_bottom = 96.0
|
||||
text = "Statistics"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="StatisticsLabel" type="Label" parent="StatisticsPopupPanel" unique_id=58226477]
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = 446.0
|
||||
offset_bottom = 96.0
|
||||
text = "Total amount of soundtracks in the playlist: 9000"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AboutWindow" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
|
||||
[node name="WinnerWindow" parent="." unique_id=999999999 instance=ExtResource("17_winner_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 720.0
|
||||
offset_top = 153.0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 153.0
|
||||
|
||||
[node name="RepeatSongCheckButton" type="CheckButton" parent="." unique_id=776835053]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 1032.0
|
||||
offset_right = 208.0
|
||||
offset_bottom = 1063.0
|
||||
focus_mode = 0
|
||||
button_pressed = true
|
||||
action_mode = 0
|
||||
text = "Auto repeat"
|
||||
|
||||
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 974.0
|
||||
offset_top = 904.0
|
||||
offset_right = 1014.0
|
||||
offset_bottom = 927.0
|
||||
scale = Vector2(3, 3)
|
||||
text = "DEBUG"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="Log" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 562.0
|
||||
offset_top = 132.0
|
||||
offset_right = 1262.0
|
||||
offset_bottom = 832.0
|
||||
|
||||
[connection signal="dir_selected" from="FileDialog" to="." method="_on_file_dialog_dir_selected"]
|
||||
@@ -1,8 +0,0 @@
|
||||
extends ScrollContainer
|
||||
|
||||
var max_value = 0;
|
||||
|
||||
func _draw():
|
||||
if max_value != self.get_v_scroll_bar().max_value:
|
||||
max_value = self.get_v_scroll_bar().max_value
|
||||
self.scroll_vertical = max_value
|
||||
-228
@@ -1,228 +0,0 @@
|
||||
class_name MusicPlayer
|
||||
extends PanelContainer
|
||||
|
||||
# LOC 20/1-24: 136
|
||||
# LOC 12/6-24: 170
|
||||
|
||||
@onready
|
||||
var play_button: TextureButton = $MusicPlayerContainer/PlayTextureButton
|
||||
|
||||
@onready
|
||||
var restart_button: TextureButton = $MusicPlayerContainer/RestartTextureButton
|
||||
|
||||
@onready
|
||||
var audio_player: AudioStreamPlayer = $AudioStreamPlayer
|
||||
|
||||
@onready
|
||||
var sound_player: AudioStreamPlayer = $AudioSoundPlayer
|
||||
|
||||
@onready
|
||||
var sound_effect_player: AudioStreamPlayer = $AudioSoundEffectPlayer
|
||||
|
||||
@onready
|
||||
var progress_slider: HSlider = $MusicPlayerContainer/MusicPlayerSlider
|
||||
|
||||
@onready
|
||||
var volume_slider: HSlider = $MusicPlayerContainer/VolumeSlider
|
||||
|
||||
@onready
|
||||
var music_time_label: Label = $MusicPlayerContainer/MusicTimeLabel
|
||||
|
||||
var play_icon: Texture = preload("res://icons/play_icon_light.svg")
|
||||
var pause_icon: Texture = preload("res://icons/pause_icon_light.svg")
|
||||
|
||||
var songs: Array = []
|
||||
|
||||
var is_changing: bool = false
|
||||
var playback_position: float
|
||||
var stream: AudioStream
|
||||
var song_finished: bool = false
|
||||
|
||||
signal update_song_list
|
||||
signal play_next_song
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
play_button.pressed.connect(play_or_pause)
|
||||
restart_button.pressed.connect(restart)
|
||||
progress_slider.drag_started.connect(_on_drag_started)
|
||||
progress_slider.drag_ended.connect(_on_drag_ended)
|
||||
|
||||
audio_player.finished.connect(_on_finished)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if audio_player.has_stream_playback() && !is_changing && !audio_player.stream_paused:
|
||||
if (!Settings.hide_beginning || audio_player.get_playback_position() > Settings.hide_for_seconds):
|
||||
progress_slider.value = audio_player.get_playback_position()
|
||||
if stream != null:
|
||||
music_time_label.text = format_text(progress_slider.value, stream.get_length())
|
||||
else:
|
||||
music_time_label.text = format_text(progress_slider.value, 0.0)
|
||||
|
||||
func format_time(time: float) -> String:
|
||||
var total_seconds: int = roundi(time)
|
||||
@warning_ignore("integer_division")
|
||||
return "%02d:%02d" % [total_seconds / 60, total_seconds % 60]
|
||||
|
||||
func format_text(part: float, total: float) -> String:
|
||||
if (Settings.hide_beginning && part < Settings.hide_for_seconds) && Settings.hide_length:
|
||||
return "??:?? / ??:??"
|
||||
elif Settings.hide_length:
|
||||
return format_time(part) + " / ??:??"
|
||||
elif Settings.hide_beginning && part < Settings.hide_for_seconds:
|
||||
return "??:?? / " + format_time(total)
|
||||
else:
|
||||
return format_time(part) + " / " + format_time(total)
|
||||
|
||||
func play_or_pause() -> void:
|
||||
if song_finished:
|
||||
restart()
|
||||
elif audio_player.stream_paused:
|
||||
play_button.texture_normal = pause_icon
|
||||
audio_player.stream_paused = false
|
||||
song_finished = false
|
||||
audio_player.seek(playback_position)
|
||||
print("continue")
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
if Settings.hide_ticks:
|
||||
progress_slider.tick_count = 0
|
||||
else:
|
||||
progress_slider.tick_count = round(stream.get_length() / 60)
|
||||
else:
|
||||
pause()
|
||||
|
||||
func pause() -> void:
|
||||
audio_player.stream_paused = true
|
||||
playback_position = audio_player.get_playback_position()
|
||||
play_button.texture_normal = play_icon
|
||||
|
||||
func restart() -> void:
|
||||
audio_player.stop()
|
||||
audio_player.stream_paused = false
|
||||
song_finished = false
|
||||
progress_slider.value = 0
|
||||
playback_position = audio_player.get_playback_position()
|
||||
audio_player.seek(playback_position)
|
||||
play_button.texture_normal = pause_icon
|
||||
audio_player.play()
|
||||
|
||||
func _on_finished() -> void:
|
||||
play_button.texture_normal = play_icon
|
||||
song_finished = true
|
||||
if !Settings.stop_after_current:
|
||||
play_next_song.emit()
|
||||
if Settings.auto_repeat_song:
|
||||
restart()
|
||||
|
||||
func _on_drag_started() -> void:
|
||||
is_changing = true
|
||||
|
||||
func _on_drag_ended(_changed: bool) -> void:
|
||||
audio_player.seek(progress_slider.value)
|
||||
playback_position = progress_slider.value
|
||||
is_changing = false
|
||||
|
||||
func seek(new_position: float) -> void:
|
||||
progress_slider.value += new_position
|
||||
is_changing = true
|
||||
audio_player.seek(progress_slider.value)
|
||||
playback_position = progress_slider.value
|
||||
is_changing = false
|
||||
|
||||
func change_volume(value: float) -> void:
|
||||
volume_slider.value += value
|
||||
volume_slider.change_volume(volume_slider.value)
|
||||
|
||||
func _on_point_triggered(point: String) -> void:
|
||||
if point == "first":
|
||||
var value: int = randi_range(0, 10)
|
||||
if value == 0:
|
||||
play_sound(preload("res://sounds/sound1.mp3"))
|
||||
elif value < 5:
|
||||
play_sound(preload("res://sounds/intro_long.mp3"))
|
||||
else:
|
||||
play_sound(preload("res://sounds/intro_short.mp3"))
|
||||
elif point == "match":
|
||||
play_sound(preload("res://sounds/sound0.mp3"))
|
||||
|
||||
func play_sound_effect(sound_effect_name: AudioStream) -> void:
|
||||
sound_effect_player.stream = sound_effect_name
|
||||
sound_effect_player.play()
|
||||
|
||||
func play_sound(sound_name: AudioStream) -> void:
|
||||
sound_player.stream = sound_name
|
||||
audio_player.stop()
|
||||
sound_player.play()
|
||||
song_finished = true
|
||||
play_button.texture_normal = play_icon
|
||||
progress_slider.value = 0
|
||||
#if stream != null:
|
||||
# music_time_label.text = format_text(progress_slider.value, stream.get_length())
|
||||
|
||||
func play_song(song: AudioStream) -> void:
|
||||
audio_player.stream = song
|
||||
audio_player.play()
|
||||
sound_player.stop()
|
||||
song_finished = false
|
||||
play_button.texture_normal = pause_icon
|
||||
stream = audio_player.stream
|
||||
progress_slider.value = 0
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
if Settings.hide_ticks:
|
||||
progress_slider.tick_count = 0
|
||||
else:
|
||||
progress_slider.tick_count = round(stream.get_length() / 60)
|
||||
music_time_label.text = format_text(progress_slider.value, stream.get_length())
|
||||
|
||||
func play_song_object(song_object_no: int) -> void:
|
||||
print("play_song_object")
|
||||
if audio_player.is_playing():
|
||||
audio_player.stop()
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
audio_player.stream = Playlist.get_song(song_object_no)
|
||||
sound_player.stop()
|
||||
audio_player.play()
|
||||
song_finished = false
|
||||
play_button.texture_normal = pause_icon
|
||||
stream = audio_player.stream
|
||||
progress_slider.value = 0
|
||||
progress_slider.max_value = round(stream.get_length())
|
||||
if Settings.hide_ticks:
|
||||
progress_slider.tick_count = 0
|
||||
else:
|
||||
progress_slider.tick_count = round(stream.get_length() / 60)
|
||||
music_time_label.text = format_text(progress_slider.value, stream.get_length())
|
||||
Playlist.set_currently_playing_song(song_object_no)
|
||||
Playlist.unset_is_playing()
|
||||
if !Settings.hide_next_track:
|
||||
print("Show answer now!!")
|
||||
Playlist.song_is_answered(song_object_no)
|
||||
Playlist.song_has_played(song_object_no)
|
||||
Playlist.song_is_playing(song_object_no)
|
||||
update_song_list.emit()
|
||||
|
||||
func get_sound_test_song() -> void:
|
||||
var test_sound: Callable = func test_sound(body: PackedByteArray) -> void:
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = body
|
||||
play_song(sound)
|
||||
MusicServerClient.fetch_sound_test_song(test_sound)
|
||||
|
||||
##### LOCAL
|
||||
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
|
||||
|
||||
func load_mp3(_path: String) -> AudioStream:
|
||||
var file: FileAccess = FileAccess.open(_path, FileAccess.READ)
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = file.get_buffer(file.get_length())
|
||||
return sound
|
||||
|
||||
func play_local_song() -> void:
|
||||
if songs:
|
||||
local_path = songs[0]
|
||||
print(local_path)
|
||||
print(FileAccess.file_exists(local_path))
|
||||
play_sound(load_mp3(local_path))
|
||||
|
||||
func sound_test_local() -> void:
|
||||
play_sound(preload("res://sounds/01. Opening.mp3"))
|
||||
@@ -1,94 +0,0 @@
|
||||
[gd_scene load_steps=10 format=3 uid="uid://ds15cgsf8vpvc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1v0rquma6nt8" path="res://MusicPlayer.gd" id="1_t24ra"]
|
||||
[ext_resource type="AudioStream" uid="uid://n2g8jddr85h2" path="res://sounds/01. Opening.mp3" id="2_xti80"]
|
||||
[ext_resource type="Texture2D" uid="uid://comxqfiykp54f" path="res://icons/play_icon_light.svg" id="3_6g308"]
|
||||
[ext_resource type="Texture2D" uid="uid://ccb6rvbldlgdg" path="res://icons/reload_light_icon.svg" id="4_jleuo"]
|
||||
[ext_resource type="PackedScene" uid="uid://w400rnew7453" path="res://volume_slider.tscn" id="5_iifuj"]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_r4qo3"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 88
|
||||
physical_keycode = 88
|
||||
key_label = 88
|
||||
unicode = 120
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_ipcfh"]
|
||||
events = [SubResource("InputEventKey_r4qo3")]
|
||||
|
||||
[sub_resource type="InputEventKey" id="InputEventKey_2nvce"]
|
||||
device = -1
|
||||
alt_pressed = true
|
||||
keycode = 90
|
||||
unicode = 90
|
||||
|
||||
[sub_resource type="Shortcut" id="Shortcut_i7swq"]
|
||||
events = [SubResource("InputEventKey_2nvce")]
|
||||
|
||||
[node name="MusicPlayer" type="PanelContainer"]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -121.0
|
||||
offset_top = -15.5
|
||||
offset_right = 121.0
|
||||
offset_bottom = 16.5
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_vertical = 4
|
||||
script = ExtResource("1_t24ra")
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource("2_xti80")
|
||||
bus = &"music"
|
||||
|
||||
[node name="AudioSoundPlayer" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource("2_xti80")
|
||||
bus = &"music"
|
||||
|
||||
[node name="AudioSoundEffectPlayer" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource("2_xti80")
|
||||
bus = &"music"
|
||||
|
||||
[node name="MusicPlayerContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
|
||||
[node name="PlayTextureButton" type="TextureButton" parent="MusicPlayerContainer"]
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_ipcfh")
|
||||
texture_normal = ExtResource("3_6g308")
|
||||
ignore_texture_size = true
|
||||
stretch_mode = 0
|
||||
|
||||
[node name="RestartTextureButton" type="TextureButton" parent="MusicPlayerContainer"]
|
||||
custom_minimum_size = Vector2(32, 32)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
shortcut = SubResource("Shortcut_i7swq")
|
||||
texture_normal = ExtResource("4_jleuo")
|
||||
ignore_texture_size = true
|
||||
stretch_mode = 0
|
||||
flip_h = true
|
||||
|
||||
[node name="MusicPlayerSlider" type="HSlider" parent="MusicPlayerContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
scrollable = false
|
||||
|
||||
[node name="MusicTimeLabel" type="Label" parent="MusicPlayerContainer"]
|
||||
layout_mode = 2
|
||||
text = "0:00 / 0:00"
|
||||
|
||||
[node name="VolumeSlider" parent="MusicPlayerContainer" instance=ExtResource("5_iifuj")]
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
@@ -1,78 +0,0 @@
|
||||
class_name Player
|
||||
extends Control
|
||||
|
||||
@onready
|
||||
var player_name_field := $HBoxContainer/Name
|
||||
|
||||
@onready
|
||||
var add_button := $HBoxContainer/AddPoint
|
||||
|
||||
@onready
|
||||
var minus_button := $HBoxContainer/RemovePoint
|
||||
|
||||
@onready
|
||||
var character := $HBoxContainer/Character
|
||||
|
||||
@onready
|
||||
var remove_player := $HBoxContainer/RemovePlayer
|
||||
|
||||
signal change_character_clicked
|
||||
signal player_removed
|
||||
|
||||
## The player shown in this row.
|
||||
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.
|
||||
func _ready() -> void:
|
||||
add_button.pressed.connect(add_point)
|
||||
add_button.set_shortcut(create_button_shortcut(list_index, false))
|
||||
minus_button.pressed.connect(minus_point)
|
||||
minus_button.set_shortcut(create_button_shortcut(list_index, true))
|
||||
character.pressed.connect(change_character)
|
||||
remove_player.pressed.connect(func() -> void: player_removed.emit())
|
||||
update_score()
|
||||
set_player_character()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
remove_player.visible = Settings.edit_players
|
||||
|
||||
func create_button_shortcut(index: int, ctrl_pressed: bool) -> Shortcut:
|
||||
var button_shortcut: Shortcut = Shortcut.new()
|
||||
var button_event : InputEventKey = InputEventKey.new()
|
||||
button_event.keycode = KEY_1 + index
|
||||
button_event.alt_pressed = true
|
||||
button_event.ctrl_pressed = ctrl_pressed
|
||||
button_shortcut.events.append(button_event)
|
||||
return button_shortcut
|
||||
|
||||
func add_point() -> void:
|
||||
player_object.add_point()
|
||||
update_score()
|
||||
|
||||
func minus_point() -> void:
|
||||
player_object.minus_point()
|
||||
update_score()
|
||||
|
||||
func change_character() -> void:
|
||||
change_character_clicked.emit()
|
||||
|
||||
func new_character_selected(character_texture: ImageTexture) -> void:
|
||||
print("Back in player list with new")
|
||||
player_object.character = character_texture
|
||||
set_player_character()
|
||||
|
||||
func set_player_character() -> void:
|
||||
character.custom_minimum_size = Vector2(80, 40)
|
||||
character.ignore_texture_size = true
|
||||
character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
|
||||
character.texture_normal = player_object.character
|
||||
|
||||
func update_score() -> void:
|
||||
if OS.has_feature("debug"):
|
||||
player_name_field.text = str(list_index + 1) + " " + player_object.player_name + ": " + str(player_object.player_score)
|
||||
else:
|
||||
player_name_field.text = player_object.player_name + ": " + str(player_object.player_score)
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://cslogy2csnd7a"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dl6no2wr2c86y" path="res://Player.gd" id="1_if4kc"]
|
||||
[ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_hpj3s"]
|
||||
[ext_resource type="Texture2D" uid="uid://b2kj6m8qpsgb1" path="res://icons/person_remove_light.svg" id="3_j3uxe"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_821k2"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w10e8"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_78a8g"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i56hb"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b2crt"]
|
||||
bg_color = Color(0.180392, 0.180392, 0.180392, 1)
|
||||
|
||||
[node name="Player" type="Control"]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("1_if4kc")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
custom_minimum_size = Vector2(2.08165e-12, 40)
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Panel4" type="Panel" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(5, 40)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_821k2")
|
||||
|
||||
[node name="Name" type="Label" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(130, 40)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 1
|
||||
text = ": 0"
|
||||
vertical_alignment = 1
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="Panel3" type="Panel" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(16, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_w10e8")
|
||||
|
||||
[node name="Character" type="TextureButton" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(80, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
texture_normal = ExtResource("2_hpj3s")
|
||||
ignore_texture_size = true
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Panel2" type="Panel" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(30, 40)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_78a8g")
|
||||
|
||||
[node name="AddPoint" type="Button" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "+1"
|
||||
|
||||
[node name="Panel5" type="Panel" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(3, 40)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_i56hb")
|
||||
|
||||
[node name="RemovePoint" type="Button" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "-1"
|
||||
|
||||
[node name="Panel" type="Panel" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(30, 40)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_b2crt")
|
||||
|
||||
[node name="RemovePlayer" type="TextureButton" parent="HBoxContainer"]
|
||||
custom_minimum_size = Vector2(40, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
texture_normal = ExtResource("3_j3uxe")
|
||||
stretch_mode = 0
|
||||
@@ -1,38 +0,0 @@
|
||||
extends TextEdit
|
||||
|
||||
signal enter_key_pressed
|
||||
signal close_pressed
|
||||
|
||||
@export
|
||||
var LIMIT: int = 10
|
||||
var current_text = ''
|
||||
var cursor_line = 0
|
||||
var cursor_column = 0
|
||||
|
||||
func _ready():
|
||||
text_changed.connect(_on_text_changed)
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.alt_pressed && event.keycode == KEY_A:
|
||||
close_pressed.emit()
|
||||
if has_focus():
|
||||
if event.keycode == KEY_ENTER || event.keycode == KEY_KP_ENTER:
|
||||
enter_key_pressed.emit()
|
||||
|
||||
func _on_text_changed():
|
||||
if text.contains("\n"):
|
||||
text = text.replace("\n", "")
|
||||
|
||||
var new_text : String = text
|
||||
if new_text.length() > LIMIT:
|
||||
text = current_text
|
||||
# when replacing the text, the cursor will get moved to the beginning of the
|
||||
# text, so move it back to where it was
|
||||
set_caret_line(cursor_line)
|
||||
set_caret_column(cursor_column)
|
||||
|
||||
current_text = text
|
||||
# save current position of cursor for when we have reached the limit
|
||||
cursor_line = get_caret_line()
|
||||
cursor_column = get_caret_column()
|
||||
@@ -1 +0,0 @@
|
||||
uid://cgakje8gb37tf
|
||||
@@ -1,66 +0,0 @@
|
||||
extends RefCounted
|
||||
class_name PlayerObject
|
||||
|
||||
signal first_point_triggerd
|
||||
signal match_point_triggerd
|
||||
signal winner_triggerd
|
||||
signal point_given_sound
|
||||
signal player_point_given
|
||||
signal player_point_taken
|
||||
|
||||
## Unique id that never changes, unlike the player's position in Settings.player_array.
|
||||
var id: String = _generate_uuid()
|
||||
|
||||
@export
|
||||
var player_name: String
|
||||
|
||||
@export
|
||||
var player_score: int
|
||||
|
||||
var is_first_point: bool = true
|
||||
|
||||
## Picture shown until the player picks a character.
|
||||
const NO_CHARACTER_PATH: String = "res://noCharacter.png"
|
||||
|
||||
var character: Texture = load(NO_CHARACTER_PATH)
|
||||
|
||||
func _init(new_name: String) -> void:
|
||||
player_name = new_name
|
||||
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:
|
||||
if player_score < Settings.winning_score:
|
||||
player_score += 1
|
||||
player_point_given.emit()
|
||||
var first_point: bool = player_score == 1 && is_first_point
|
||||
if player_score == 1:
|
||||
is_first_point = false
|
||||
# With a low winning score the first point can also be match point or the win,
|
||||
# so the more important event is checked first
|
||||
if player_score == Settings.winning_score:
|
||||
winner_triggerd.emit()
|
||||
elif player_score == Settings.winning_score - 1:
|
||||
match_point_triggerd.emit()
|
||||
elif first_point:
|
||||
first_point_triggerd.emit()
|
||||
else:
|
||||
point_given_sound.emit()
|
||||
|
||||
func minus_point() -> void:
|
||||
if player_score > 0:
|
||||
player_score -= 1
|
||||
player_point_taken.emit()
|
||||
if player_score == 0:
|
||||
is_first_point = true
|
||||
|
||||
func reset_points() -> void:
|
||||
player_score = 0
|
||||
is_first_point = true
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var qr_panel: Panel = $QRPanel
|
||||
|
||||
@onready
|
||||
var qr_code_rect: QRCodeRect = $QRPanel/QRCodeRect
|
||||
|
||||
@onready
|
||||
var url_label: Label = $QRPanel/URLLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
qr_code_rect.light_module_color = Color.WHITE
|
||||
qr_code_rect.dark_module_color = Color.BLACK
|
||||
qr_code_rect.quiet_zone_size = 4
|
||||
|
||||
# Generate QR code for the search URL
|
||||
var url: String = Settings.default_path + "/search"
|
||||
qr_code_rect.data = url
|
||||
url_label.text = qr_code_rect.data
|
||||
print("QR code generated for: ", url)
|
||||
|
||||
func open() -> void:
|
||||
super()
|
||||
# Regenerate QR code in case settings changed
|
||||
var url: String = Settings.default_path + "/search"
|
||||
qr_code_rect.data = url
|
||||
url_label.text = qr_code_rect.data
|
||||
|
||||
+13
-1
@@ -5,7 +5,7 @@ Releases are made from the version in `project.godot`
|
||||
same version in the About window.
|
||||
|
||||
1. Bump the version, e.g. from `1.9.0` to `1.10.0`, and add a `WHATS_NEW` entry in
|
||||
`AboutWindow.gd`.
|
||||
`popups/about_popup.gd`.
|
||||
2. Merge to `main` and push.
|
||||
|
||||
The `Build` workflow (`.gitea/workflows/gitea-build.yaml`) then:
|
||||
@@ -27,3 +27,15 @@ Unpack `MusicPlayer_<version>_linux.tar.gz` and run `./install.sh` inside the
|
||||
`MusicPlayer` folder. It installs the game to `~/.local/bin` and adds it, with its icon,
|
||||
to the app menu. The bare `MusicPlayer_<version>.x86_64` is still published for anyone
|
||||
who just wants to run the file.
|
||||
|
||||
## Updating
|
||||
|
||||
When the server has a newer release, the top bar says "Version X is available". Clicking
|
||||
it offers **Update and restart**: the game downloads its new file from the release
|
||||
(`MusicPlayer_<version>.exe` on Windows, `MusicPlayer_<version>.x86_64` on Linux, through
|
||||
the server's `/download/<platform>` routes), puts it in place of the running file and
|
||||
starts it, and the quiz continues where it was. On Windows the old file is kept as
|
||||
`<game>.old` until the next start. So keep the release file names as they are: the server
|
||||
finds them by their ending. Where the game can't replace itself (run from the editor, a
|
||||
folder it can't write to, or another platform), it opens the download in the browser
|
||||
instead.
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var search_list: VBoxContainer = $ScrollContainer/VBoxContainer
|
||||
|
||||
@onready
|
||||
var search_panel: Panel = %SearchPanel
|
||||
|
||||
@onready
|
||||
var clear_button: Button = $ClearButton
|
||||
|
||||
@onready
|
||||
var search_bar: TextEdit = $Searchbar
|
||||
|
||||
var soundtracks: Array = []
|
||||
|
||||
# Precomputed soundtrack data: array of dicts with original, lowercase, and cleaned fields
|
||||
var precomputed_soundtracks: Array = []
|
||||
|
||||
# Regex cache: maps cleaned search terms to compiled RegEx objects
|
||||
var regex_cache: Dictionary = {}
|
||||
|
||||
var label_pool: Array[Label] = []
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
clear_button.pressed.connect(clear)
|
||||
search_bar.grab_focus()
|
||||
search_bar.text_changed.connect(search)
|
||||
visibility_changed.connect(focus)
|
||||
|
||||
func focus() -> void:
|
||||
if self.visible == true:
|
||||
search_bar.grab_focus()
|
||||
clear()
|
||||
|
||||
func close() -> void:
|
||||
clear()
|
||||
super()
|
||||
|
||||
func search() -> void:
|
||||
print(search_bar.text)
|
||||
var search_text: String = search_bar.text
|
||||
|
||||
if search_text == "":
|
||||
show_all_soundtracks()
|
||||
return
|
||||
|
||||
perform_search(search_text)
|
||||
|
||||
func show_all_soundtracks() -> void:
|
||||
# Hide all visible labels and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
if child.visible:
|
||||
child.visible = false
|
||||
label_pool.append(child)
|
||||
|
||||
# Show all soundtracks in order
|
||||
for soundtrack: String in soundtracks:
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
|
||||
func perform_search(search_text: String) -> void:
|
||||
# Hide all visible labels and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
if child.visible:
|
||||
child.visible = false
|
||||
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 search_text_lower: String = search_text.to_lower()
|
||||
var clean_search_text: String = clean_term(search_text)
|
||||
|
||||
var matches: Array[String] = []
|
||||
|
||||
# Pass 1: Exact case-insensitive contains match on lowercase versions
|
||||
for i in range(precomputed_soundtracks.size()):
|
||||
var st_data = precomputed_soundtracks[i]
|
||||
if st_data["lowercase"].contains(search_text_lower):
|
||||
if not st_data["original"] in seen:
|
||||
seen[st_data["original"]] = true
|
||||
matches.append(st_data["original"])
|
||||
|
||||
# Pass 2: Contains match on cleaned terms
|
||||
for i in range(precomputed_soundtracks.size()):
|
||||
var st_data = precomputed_soundtracks[i]
|
||||
if st_data["cleaned"].contains(clean_search_text):
|
||||
if not st_data["original"] in seen:
|
||||
seen[st_data["original"]] = true
|
||||
matches.append(st_data["original"])
|
||||
|
||||
# Pass 3: Regex match on cleaned terms
|
||||
var regex = compile_regex_cached(clean_search_text)
|
||||
for i in range(precomputed_soundtracks.size()):
|
||||
var st_data = precomputed_soundtracks[i]
|
||||
if regex.search(st_data["cleaned"]):
|
||||
if not st_data["original"] in seen:
|
||||
seen[st_data["original"]] = true
|
||||
matches.append(st_data["original"])
|
||||
|
||||
return matches
|
||||
|
||||
# Cleans a term by removing spaces and replacing special characters.
|
||||
# Matches MusicServer's clean_term() function.
|
||||
func clean_term(term: String) -> String:
|
||||
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and").to_lower()
|
||||
|
||||
# Compiles a regex pattern with caching to avoid recompiling the same pattern multiple times.
|
||||
func compile_regex_cached(search_term: String) -> RegEx:
|
||||
if not search_term in regex_cache:
|
||||
var regex = RegEx.new()
|
||||
var regText: String = ".*"
|
||||
for letter: String in search_term:
|
||||
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
|
||||
if "\\^$.|?*+()[]{}".contains(letter):
|
||||
regText += "\\"
|
||||
regText += letter + ".*"
|
||||
regex.compile(regText)
|
||||
regex_cache[search_term] = regex
|
||||
return regex_cache[search_term]
|
||||
|
||||
func add_soundtrack(soundtrack: String) -> void:
|
||||
# Backward compatibility - delegates to reuse version
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
|
||||
# Optimized version that reuses label nodes from pool instead of creating new ones
|
||||
func add_soundtrack_reuse(soundtrack: String) -> void:
|
||||
var label: Label
|
||||
if label_pool.size() > 0:
|
||||
label = label_pool.pop_front()
|
||||
label.text = soundtrack
|
||||
label.visible = true
|
||||
# Move to end to maintain display order
|
||||
search_list.move_child(label, search_list.get_child_count() - 1)
|
||||
else:
|
||||
label = Label.new()
|
||||
label.text = soundtrack
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
search_list.add_child(label)
|
||||
label.visible = true
|
||||
|
||||
func check_if_soundtrack_exists(soundtrack: String) -> bool:
|
||||
var soundtrack_exists: bool = false
|
||||
for child: Label in search_list.get_children():
|
||||
if child.text == soundtrack and child.visible:
|
||||
soundtrack_exists = true
|
||||
return soundtrack_exists
|
||||
|
||||
func get_list_of_soundtracks() -> void:
|
||||
print("get_list_of_soundtracks")
|
||||
var handle_soundtracks: Callable = func handle_soundtracks(array) -> void:
|
||||
if typeof(array) == TYPE_ARRAY:
|
||||
# Reuse existing labels: hide all and add to pool
|
||||
for child: Label in search_list.get_children():
|
||||
child.visible = false
|
||||
label_pool.append(child)
|
||||
set_soundtracks(array)
|
||||
for soundtrack: String in soundtracks:
|
||||
add_soundtrack_reuse(soundtrack)
|
||||
else:
|
||||
print("Unexpected data")
|
||||
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 clear() -> void:
|
||||
search_bar.text = ""
|
||||
show_all_soundtracks()
|
||||
search_bar.grab_focus()
|
||||
@@ -1,52 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://bxydgil1yifps"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bg5yo08638600" path="res://SearchWindow.gd" id="1_m7bij"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[node name="Control" type="Control" unique_id=616238617 node_paths=PackedStringArray("panel")]
|
||||
theme = ExtResource("popup_theme")
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_m7bij")
|
||||
panel = NodePath("SearchPanel")
|
||||
|
||||
[node name="SearchPanel" type="Panel" parent="." unique_id=1262945994]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 640.0
|
||||
|
||||
[node name="ClearButton" type="Button" parent="." unique_id=1549439398]
|
||||
layout_mode = 0
|
||||
offset_left = 936.0
|
||||
offset_top = 16.0
|
||||
offset_right = 1027.0
|
||||
offset_bottom = 56.0
|
||||
scale = Vector2(2, 2)
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Clear"
|
||||
|
||||
[node name="Searchbar" type="TextEdit" parent="." unique_id=2112669372]
|
||||
layout_mode = 0
|
||||
offset_left = 32.0
|
||||
offset_top = 16.0
|
||||
offset_right = 468.0
|
||||
offset_bottom = 56.0
|
||||
scale = Vector2(2, 2)
|
||||
placeholder_text = "Search"
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=213733486]
|
||||
layout_mode = 0
|
||||
offset_top = 128.0
|
||||
offset_right = 576.0
|
||||
offset_bottom = 384.0
|
||||
scale = Vector2(2, 2)
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1567403144]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
extends Node
|
||||
|
||||
const MAX_NUMBER_OF_PLAYERS: int = 7
|
||||
|
||||
var default_path: String = "https://music.sanplex.xyz"
|
||||
var selected_server: int = 0
|
||||
|
||||
var is_local: bool = false
|
||||
|
||||
var is_debug: bool = false
|
||||
|
||||
var stop_after_current: bool = true
|
||||
var auto_repeat_song: bool = true
|
||||
var hide_next_track: bool = true
|
||||
var add_to_stats: bool = true
|
||||
var use_low_played_mode: bool = false
|
||||
var winning_score: int = 20
|
||||
var fullscreen: bool = false
|
||||
var play_local: bool = false
|
||||
var inspiration_list_speed: int = 1
|
||||
var hide_for_seconds: float = 5.0
|
||||
var hide_beginning: bool = false
|
||||
var hide_length: bool = false
|
||||
var hide_ticks: bool = false
|
||||
|
||||
var player_array: Array[PlayerObject]
|
||||
var edit_players: bool = false
|
||||
var currently_syncing: bool = false
|
||||
var character_select_open: bool = false
|
||||
|
||||
## The game version, set in Project Settings > Application > Config > Version.
|
||||
## Releases are made from it, see RELEASING.md.
|
||||
var version: String = ProjectSettings.get_setting("application/config/version", "")
|
||||
|
||||
func delete_children(node: Node) -> void:
|
||||
for n: Node in node.get_children():
|
||||
node.remove_child(n)
|
||||
n.queue_free()
|
||||
@@ -1,218 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var settings_panel: Panel = %SettingsPanel
|
||||
|
||||
@onready
|
||||
var stop_after_current_button: CheckButton = %StopAfterCurrentCheckButton
|
||||
|
||||
@onready
|
||||
var hide_next_track_button: CheckButton = %HideNextTrackCheckButton
|
||||
|
||||
@onready
|
||||
var add_to_database_button: CheckButton = %AddToDatabaseCheckButton
|
||||
|
||||
@onready
|
||||
var low_played_button: CheckButton = %LowPlayedCheckButton
|
||||
|
||||
@onready
|
||||
var hide_beginning_button: CheckButton = %HideBeginningCheckButton
|
||||
|
||||
@onready
|
||||
var hide_length_button: CheckButton = %HideLengthCheckButton
|
||||
|
||||
@onready
|
||||
var hide_ticks_button: CheckButton = %HideTicksCheckButton
|
||||
|
||||
@onready
|
||||
var score_label: Label = %ScoreLabel
|
||||
|
||||
@onready
|
||||
var lower_winning_score_button: Button = %LowerButton
|
||||
|
||||
@onready
|
||||
var increase_winning_score_button: Button = %IncreaseButton
|
||||
|
||||
@onready
|
||||
var cache_label: Label = %CacheLabel
|
||||
|
||||
@onready
|
||||
var lower_cache_button: Button = %LowerCacheButton
|
||||
|
||||
@onready
|
||||
var increase_cache_button: Button = %IncreaseCacheButton
|
||||
|
||||
@onready
|
||||
var inspiration_speed_label: Label = %InspirationListSpeedLabel
|
||||
|
||||
@onready
|
||||
var lower_inspiration_speed_button: Button = %LowerInspirationSpeedButton
|
||||
|
||||
@onready
|
||||
var increase_inspiration_speed_button: Button = %IncreaseInspirationSpeedButton
|
||||
|
||||
@onready
|
||||
var hide_for_seconds_label: Label = %HideForSecondsLabel
|
||||
|
||||
@onready
|
||||
var lower_hide_for_seconds_button: Button = %LowerHideForSecondsButton
|
||||
|
||||
@onready
|
||||
var increase_hide_for_seconds_button: Button = %IncreaseHideForSecondsButton
|
||||
|
||||
@onready
|
||||
var select_server_button: OptionButton = %SelectServerButton
|
||||
|
||||
@onready
|
||||
var fullscreen_button: CheckButton = %FullscreenButton
|
||||
|
||||
@onready
|
||||
var local_button: CheckButton = %LocalButton
|
||||
|
||||
signal server_changed
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
fullscreen_button.button_pressed = Settings.fullscreen
|
||||
select_server_button.selected = Settings.selected_server
|
||||
|
||||
stop_after_current_button.pressed.connect(stop_after_current)
|
||||
hide_next_track_button.pressed.connect(hide_next_track)
|
||||
add_to_database_button.pressed.connect(add_to_database)
|
||||
low_played_button.pressed.connect(low_played)
|
||||
hide_beginning_button.pressed.connect(hide_beginning)
|
||||
hide_length_button.pressed.connect(hide_length)
|
||||
hide_ticks_button.pressed.connect(hide_ticks)
|
||||
lower_winning_score_button.pressed.connect(lower_winning_score)
|
||||
increase_winning_score_button.pressed.connect(increase_winning_score)
|
||||
lower_cache_button.pressed.connect(lower_cache)
|
||||
increase_cache_button.pressed.connect(increase_cache)
|
||||
lower_inspiration_speed_button.pressed.connect(lower_inspiration_speed)
|
||||
increase_inspiration_speed_button.pressed.connect(increase_inspiration_speed)
|
||||
lower_hide_for_seconds_button.pressed.connect(lower_hide_for_seconds)
|
||||
increase_hide_for_seconds_button.pressed.connect(increase_hide_for_seconds)
|
||||
fullscreen_button.pressed.connect(fullscreen)
|
||||
local_button.pressed.connect(local_play)
|
||||
|
||||
stop_after_current_button.button_pressed = Settings.stop_after_current
|
||||
hide_next_track_button.button_pressed = Settings.hide_next_track
|
||||
add_to_database_button.button_pressed = Settings.add_to_stats
|
||||
low_played_button.button_pressed = Settings.use_low_played_mode
|
||||
hide_beginning_button.button_pressed = Settings.hide_beginning
|
||||
hide_length_button.button_pressed = Settings.hide_length
|
||||
hide_ticks_button.button_pressed = Settings.hide_ticks
|
||||
score_label.text = str(Settings.winning_score)
|
||||
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||
inspiration_speed_label.text = str(Settings.inspiration_list_speed)
|
||||
hide_for_seconds_label.text = str(Settings.hide_for_seconds)
|
||||
fullscreen_button.button_pressed = Settings.fullscreen
|
||||
update_hide_for_seconds_enabled()
|
||||
select_server_button.select(Settings.selected_server)
|
||||
select_server_button.item_selected.connect(select_server)
|
||||
visibility_changed.connect(_on_visibility_changed)
|
||||
|
||||
# Settings can change outside this window, e.g. winning turns statistics off
|
||||
func _on_visibility_changed() -> void:
|
||||
if visible:
|
||||
add_to_database_button.button_pressed = Settings.add_to_stats
|
||||
|
||||
func fullscreen() -> void:
|
||||
Settings.fullscreen = !Settings.fullscreen
|
||||
if Settings.fullscreen == true:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||
if Settings.fullscreen == false:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||
|
||||
func local_play() -> void:
|
||||
Settings.play_local = !Settings.play_local
|
||||
|
||||
func stop_after_current() -> void:
|
||||
Settings.stop_after_current = !Settings.stop_after_current
|
||||
|
||||
func hide_next_track() -> void:
|
||||
Settings.hide_next_track = !Settings.hide_next_track
|
||||
|
||||
func add_to_database() -> void:
|
||||
Settings.add_to_stats = !Settings.add_to_stats
|
||||
|
||||
func low_played() -> void:
|
||||
Settings.use_low_played_mode = !Settings.use_low_played_mode
|
||||
|
||||
func hide_beginning() -> void:
|
||||
Settings.hide_beginning = !Settings.hide_beginning
|
||||
update_hide_for_seconds_enabled()
|
||||
|
||||
func update_hide_for_seconds_enabled() -> void:
|
||||
var enabled: bool = Settings.hide_beginning
|
||||
%HideForSecondsHBoxContainer.visible = enabled
|
||||
|
||||
func hide_length() -> void:
|
||||
Settings.hide_length = !Settings.hide_length
|
||||
|
||||
func hide_ticks() -> void:
|
||||
Settings.hide_ticks = !Settings.hide_ticks
|
||||
|
||||
func lower_winning_score() -> void:
|
||||
Settings.winning_score = max(Settings.winning_score - 1, 1)
|
||||
score_label.text = str(Settings.winning_score)
|
||||
|
||||
func increase_winning_score() -> void:
|
||||
Settings.winning_score += 1
|
||||
score_label.text = str(Settings.winning_score)
|
||||
|
||||
func set_winning_score(new_score: int) -> void:
|
||||
Settings.winning_score = new_score
|
||||
score_label.text = str(Settings.winning_score)
|
||||
|
||||
func lower_cache() -> void:
|
||||
Playlist.number_of_tracks_to_preload -= 1
|
||||
if Playlist.number_of_tracks_to_preload < 1:
|
||||
Playlist.number_of_tracks_to_preload = 1
|
||||
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||
|
||||
func increase_cache() -> void:
|
||||
Playlist.number_of_tracks_to_preload += 1
|
||||
cache_label.text = str(Playlist.number_of_tracks_to_preload)
|
||||
|
||||
func lower_inspiration_speed() -> void:
|
||||
Settings.inspiration_list_speed -= 1
|
||||
if Settings.inspiration_list_speed < 1:
|
||||
Settings.inspiration_list_speed = 1
|
||||
inspiration_speed_label.text = str(Settings.inspiration_list_speed)
|
||||
|
||||
func increase_inspiration_speed() -> void:
|
||||
Settings.inspiration_list_speed += 1
|
||||
inspiration_speed_label.text = str(Settings.inspiration_list_speed )
|
||||
|
||||
func lower_hide_for_seconds() -> void:
|
||||
Settings.hide_for_seconds -= 1.0
|
||||
if Settings.hide_for_seconds < 0:
|
||||
Settings.hide_for_seconds = 0
|
||||
hide_for_seconds_label.text = str(Settings.hide_for_seconds)
|
||||
|
||||
func increase_hide_for_seconds() -> void:
|
||||
Settings.hide_for_seconds += 1.0
|
||||
if Settings.hide_for_seconds > 10.0:
|
||||
Settings.hide_for_seconds = 10.0
|
||||
hide_for_seconds_label.text = str(Settings.hide_for_seconds)
|
||||
|
||||
func select_server(new_server: int) -> void:
|
||||
print("select_server")
|
||||
Settings.default_path = select_server_button.get_item_text(new_server)
|
||||
Settings.selected_server = new_server
|
||||
print("Settings.default_path: " + Settings.default_path)
|
||||
print("Settings.selected_server: " + str(Settings.selected_server))
|
||||
print("new_server: " + str(new_server))
|
||||
print("select_server_button.get_item_text(new_server): " + select_server_button.get_item_text(new_server))
|
||||
server_changed.emit()
|
||||
|
||||
func set_selected_server(new_server: int) -> void:
|
||||
print("set_selected_server")
|
||||
select_server_button.select(new_server)
|
||||
Settings.default_path = select_server_button.get_item_text(new_server)
|
||||
Settings.selected_server = new_server
|
||||
print("Settings.default_path: " + Settings.default_path)
|
||||
print("Settings.selected_server: " + str(Settings.selected_server))
|
||||
print("new_server: " + str(new_server))
|
||||
print("select_server_button.get_item_text(new_server): " + select_server_button.get_item_text(new_server))
|
||||
server_changed.emit()
|
||||
@@ -1,259 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://dldpeo5y3l5hq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dukqyjoduf1af" path="res://SettingsWindow.gd" id="1_bt55j"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_3xrlm"]
|
||||
font_size = 25
|
||||
|
||||
[node name="SettingsControl" type="Control" unique_id=1298840328 node_paths=PackedStringArray("panel")]
|
||||
theme = ExtResource("popup_theme")
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_bt55j")
|
||||
panel = NodePath("SettingsPanel")
|
||||
|
||||
[node name="SettingsPanel" type="Panel" parent="." unique_id=45537159]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 500.0
|
||||
offset_bottom = 640.0
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="SettingsPanel" unique_id=1325795358]
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = 8.0
|
||||
offset_right = 480.0
|
||||
offset_bottom = 632.0
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel/ScrollContainer" unique_id=1796635163]
|
||||
clip_contents = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Label" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1111531402]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
text = "Settings"
|
||||
label_settings = SubResource("LabelSettings_3xrlm")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="StopAfterCurrentCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=508305575]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
button_pressed = true
|
||||
action_mode = 0
|
||||
text = "Stop after current"
|
||||
|
||||
[node name="HideNextTrackCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1568244599]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
button_pressed = true
|
||||
action_mode = 0
|
||||
text = "Hide next track"
|
||||
|
||||
[node name="AddToDatabaseCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1673750542]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
action_mode = 0
|
||||
text = "Turn on played to database"
|
||||
|
||||
[node name="LowPlayedCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1672657048]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
action_mode = 0
|
||||
text = "Use low played mode"
|
||||
|
||||
[node name="HideBeginningCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1370617867]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
action_mode = 0
|
||||
text = "Hide beginning of songs"
|
||||
|
||||
[node name="HideForSecondsHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1368485120]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HideForSecondsTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1822369383]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Hide Start Seconds: "
|
||||
|
||||
[node name="HideForSecondsLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1580108945]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 0)
|
||||
layout_mode = 2
|
||||
text = "5.0"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LowerHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=180643442]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "-1"
|
||||
|
||||
[node name="IncreaseHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=340681770]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "+1"
|
||||
|
||||
[node name="HideLengthCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=67806372]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
action_mode = 0
|
||||
text = "Hide length of songs"
|
||||
|
||||
[node name="HideTicksCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=706322752]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
action_mode = 0
|
||||
text = "Hide ticks in playbar"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=2140610992]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Label2" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1892907487]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Winning Score: "
|
||||
|
||||
[node name="ScoreLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=444542662]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 0)
|
||||
layout_mode = 2
|
||||
text = "20"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LowerButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=614806355]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "-1"
|
||||
|
||||
[node name="IncreaseButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1837169457]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "+1"
|
||||
|
||||
[node name="CacheHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=658210492]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CacheTitleLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=365012193]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Predownload songs: "
|
||||
|
||||
[node name="CacheLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1558370172]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 0)
|
||||
layout_mode = 2
|
||||
text = "1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LowerCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1990385038]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "-1"
|
||||
|
||||
[node name="IncreaseCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1806295533]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "+1"
|
||||
|
||||
[node name="InspirationHBoxContainer" type="HBoxContainer" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1686756540]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InspirationListSpeedTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1397861506]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Inspiration List Speed: "
|
||||
|
||||
[node name="InspirationListSpeedLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=867387668]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(48, 0)
|
||||
layout_mode = 2
|
||||
text = "1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LowerInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=845209534]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "-1"
|
||||
|
||||
[node name="IncreaseInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1122701153]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(30, 2.08165e-12)
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "+1"
|
||||
|
||||
[node name="SelectServerButton" type="OptionButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=434350694]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
selected = 0
|
||||
item_count = 5
|
||||
popup/item_0/text = "https://music.sanplex.xyz"
|
||||
popup/item_0/id = 1
|
||||
popup/item_1/text = "https://tmusic.sanplex.xyz"
|
||||
popup/item_1/id = 2
|
||||
popup/item_2/text = "https://rmusic.sanplex.xyz"
|
||||
popup/item_2/id = 3
|
||||
popup/item_3/text = "https://old-music.sanplex.xyz"
|
||||
popup/item_3/id = 4
|
||||
popup/item_4/text = "http://localhost:8080"
|
||||
popup/item_4/id = 5
|
||||
|
||||
[node name="FullscreenButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=400304684]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
text = "Fullscreen"
|
||||
|
||||
[node name="QuickSyncButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=49497448]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
button_pressed = true
|
||||
action_mode = 0
|
||||
text = "Quick sync"
|
||||
|
||||
[node name="LocalButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=905844805]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
disabled = true
|
||||
text = "Local"
|
||||
@@ -1,55 +0,0 @@
|
||||
extends RefCounted
|
||||
|
||||
class_name SongObject
|
||||
|
||||
var song_number: int
|
||||
|
||||
var song_title: String
|
||||
|
||||
var soundtrack_title: String
|
||||
|
||||
## Ids of the players who got a point for this song.
|
||||
var players_given_point: Array[String] = []
|
||||
|
||||
var is_answered: bool = false
|
||||
|
||||
var has_played: bool = false
|
||||
|
||||
var is_playing: bool = false
|
||||
|
||||
var song: AudioStream
|
||||
|
||||
func get_soundtrack_title() -> String:
|
||||
if is_answered:
|
||||
return soundtrack_title
|
||||
else:
|
||||
return "????????"
|
||||
|
||||
func get_song_title() -> String:
|
||||
if is_answered:
|
||||
return song_title
|
||||
else:
|
||||
return "??????"
|
||||
|
||||
func get_song_info() -> String:
|
||||
if Settings.is_debug:
|
||||
var format_string: String = "%d. %s - %s | played: %s | answered: %s"
|
||||
return format_string % [(song_number + 1), soundtrack_title, song_title, has_played, is_answered]
|
||||
elif is_answered:
|
||||
var format_string: String = "%d. %s - %s"
|
||||
return format_string % [(song_number + 1), soundtrack_title, song_title]
|
||||
else:
|
||||
return "??? - ???"
|
||||
|
||||
func add_point(player_id: String) -> void:
|
||||
print("add_point")
|
||||
if !players_given_point.has(player_id):
|
||||
players_given_point.append(player_id)
|
||||
|
||||
func remove_point(player_id: String) -> void:
|
||||
print("remove_point")
|
||||
players_given_point.erase(player_id)
|
||||
|
||||
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"
|
||||
return format_string % [(song_number), soundtrack_title, song_title, is_answered, has_played, is_playing]
|
||||
+23
-3
@@ -26,8 +26,8 @@ pass.
|
||||
|
||||
| Folder | Contents |
|
||||
|---|---|
|
||||
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client |
|
||||
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncWindow.tscn`) run against a fake server, including the point keybinds |
|
||||
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client and the API classes |
|
||||
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncPopup.tscn`) run against a fake server, including the point keybinds and the Statistics tab |
|
||||
| `tests/fakes/` | `FakeHttpTransport`, `AutoloadSnapshot` and the `MainWindowTestSuite` base class |
|
||||
| `tests/data/` | Files the fake server sends, e.g. a short MP3 |
|
||||
|
||||
@@ -48,7 +48,27 @@ assert_int(server.count("/music/rand")).is_equal(1)
|
||||
server.uninstall()
|
||||
```
|
||||
|
||||
Paths without a response get a 404, so the code under test sees a failed request.
|
||||
Paths are relative to the API (`/music/list` is `<server>/api/v1/music/list`). Paths
|
||||
without a response get a 404, so the code under test sees a failed request.
|
||||
|
||||
Like the real server, the fake hands out tokens on `POST /token` and answers 401 to
|
||||
requests without a valid one, so the client's token handling runs in every test. Token
|
||||
requests are counted in `server.token_requests`, not in `requested_paths`. Use
|
||||
`server.expire_tokens()` to make the client renew its token, `server.hold("/token")` to
|
||||
keep it waiting for one, and `server.respond_json("/token", {...}, 401)` for a refused
|
||||
password.
|
||||
|
||||
## API classes
|
||||
|
||||
The classes in `server/generated/` (`SongInfo`, `StatisticsSummary`, ...) are generated
|
||||
from the server's OpenAPI spec, committed as `server/openapi.json`. To update them, copy
|
||||
a new spec there (`just openapi` in MusicServer, or `<server>/openapi.json`) and run:
|
||||
|
||||
```bash
|
||||
godot --headless --path . -s tools/generate_api_types.gd
|
||||
```
|
||||
|
||||
`tests/unit/test_api_types.gd` parses a sample response with each class.
|
||||
|
||||
## Writing tests
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://cxlsmxnrp0agh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dxpb22xbwf3t1" path="res://winner_window.gd" id="1_winner_script"]
|
||||
[ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_no_character"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_hr75l"]
|
||||
font_size = 35
|
||||
|
||||
[node name="WinnerControl" type="Control" unique_id=1111111111 node_paths=PackedStringArray("panel")]
|
||||
theme = ExtResource("popup_theme")
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_winner_script")
|
||||
panel = NodePath("WinnerPanel")
|
||||
|
||||
[node name="WinnerPanel" type="Panel" parent="." unique_id=89212869]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 500.0
|
||||
offset_bottom = 600.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="WinnerPanel" unique_id=471159349]
|
||||
layout_mode = 0
|
||||
offset_top = 25.0
|
||||
offset_right = 500.0
|
||||
offset_bottom = 282.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
alignment = 1
|
||||
|
||||
[node name="WinnerPicture" type="TextureRect" parent="WinnerPanel/VBoxContainer" unique_id=1543774067]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(480, 200)
|
||||
layout_mode = 2
|
||||
texture = ExtResource("2_no_character")
|
||||
expand_mode = 3
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="WinnerLabel" type="Label" parent="WinnerPanel/VBoxContainer" unique_id=1437597286]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Winner!"
|
||||
label_settings = SubResource("LabelSettings_hr75l")
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 2
|
||||
|
||||
[node name="PlayersList" type="VBoxContainer" parent="WinnerPanel/VBoxContainer" unique_id=471159350]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
@@ -1,46 +0,0 @@
|
||||
class_name Character
|
||||
extends Control
|
||||
|
||||
@onready
|
||||
var panel: Panel = $Panel
|
||||
|
||||
@onready
|
||||
var character_image: TextureRect = %CharacterTextureRect
|
||||
|
||||
@onready
|
||||
var character_name_label: Label = %CharacterName
|
||||
|
||||
signal new_character_selected(img_texture: ImageTexture)
|
||||
|
||||
func set_character_name(character_name: String) -> void:
|
||||
panel.size = Vector2(240, 145)
|
||||
#margin.size = Vector2(240, 145)
|
||||
#margin set margin 15, 10, 15, 0
|
||||
character_name_label.text = character_name.split(".")[0]
|
||||
character_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
var image_fetched: Callable = func image_fetched(image: PackedByteArray) -> void:
|
||||
var img: Image = Image.new()
|
||||
var err: Error = img.load_png_from_buffer(image)
|
||||
if err != OK:
|
||||
print(err)
|
||||
var err2: Error = img.load_jpg_from_buffer(image)
|
||||
if err2 != OK:
|
||||
print(err2)
|
||||
var new_image: ImageTexture = ImageTexture.new()
|
||||
new_image.set_image(img)
|
||||
character_image.custom_minimum_size = Vector2(200, 100)
|
||||
character_image.size = Vector2(200, 100)
|
||||
character_image.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
character_image.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
character_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
character_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
character_image.texture = new_image
|
||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if self.visible == true && Settings.character_select_open == true:
|
||||
if event is InputEventMouseButton && event.is_pressed() && event.get_button_index() == MOUSE_BUTTON_LEFT:
|
||||
var evLocal: InputEvent = make_input_local(event)
|
||||
if Rect2(Vector2(0, 0), panel.size).has_point(evLocal.position):
|
||||
print(character_name_label.text)
|
||||
new_character_selected.emit(character_image.texture)
|
||||
@@ -1,45 +0,0 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://c7ecjj2af6usf"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://i35b3scbpwpu" path="res://character.gd" id="1_pjt0s"]
|
||||
[ext_resource type="Texture2D" uid="uid://cv3hdd6bsruau" path="res://icon.svg" id="2_pjt0s"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pjt0s"]
|
||||
bg_color = Color(0.302, 0.302, 0.302, 1)
|
||||
|
||||
[node name="Control" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
script = ExtResource("1_pjt0s")
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
layout_mode = 0
|
||||
offset_right = 240.0
|
||||
offset_bottom = 145.0
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_pjt0s")
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
|
||||
layout_mode = 0
|
||||
offset_right = 240.0
|
||||
offset_bottom = 145.0
|
||||
theme_override_constants/margin_left = 15
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 15
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="CharacterTextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(200, 100)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
texture = ExtResource("2_pjt0s")
|
||||
expand_mode = 3
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterName" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Aloy"
|
||||
horizontal_alignment = 1
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
class_name Anim
|
||||
extends RefCounted
|
||||
## Short animations made with tweens, used all over the game: popping, fading, flashing
|
||||
## and pulsing.
|
||||
##
|
||||
## Every animation is short so a quiz never waits for one. With
|
||||
## [member Settings.reduce_animations] on, nothing moves: each helper jumps straight to
|
||||
## where the animation would end.
|
||||
|
||||
## How long quick feedback, such as a button press, takes.
|
||||
const FAST: float = 0.12
|
||||
## How long most animations take.
|
||||
const NORMAL: float = 0.2
|
||||
## How long flashes and bigger moments take.
|
||||
const SLOW: float = 0.45
|
||||
|
||||
|
||||
## Whether animations are on.
|
||||
static func enabled() -> bool:
|
||||
return !Settings.reduce_animations
|
||||
|
||||
|
||||
## A new tween on [param node] for the animation called [param key], stopping an earlier
|
||||
## one with the same key so they don't fight. Returns null when animations are off or the
|
||||
## node is not in the tree.
|
||||
static func tween(node: Node, key: StringName = &"anim") -> Tween:
|
||||
stop(node, key)
|
||||
if !enabled() || !node.is_inside_tree():
|
||||
return null
|
||||
var new_tween: Tween = node.create_tween()
|
||||
node.set_meta(StringName("_tween_" + key), new_tween)
|
||||
return new_tween
|
||||
|
||||
|
||||
## Stops the animation called [param key] on [param node], if one is running.
|
||||
static func stop(node: Node, key: StringName = &"anim") -> void:
|
||||
var meta: StringName = StringName("_tween_" + key)
|
||||
var old: Variant = node.get_meta(meta) if node.has_meta(meta) else null
|
||||
if old is Tween && (old as Tween).is_valid():
|
||||
(old as Tween).kill()
|
||||
|
||||
|
||||
## Makes [param control] grow from its middle.
|
||||
static func center_pivot(control: Control) -> void:
|
||||
control.pivot_offset = control.size / 2
|
||||
|
||||
|
||||
## Makes [param control] jump to [param peak] times its size and spring back.
|
||||
static func pop(control: Control, peak: float = 1.3, duration: float = NORMAL) -> void:
|
||||
var pop_tween: Tween = tween(control, &"pop")
|
||||
control.scale = Vector2.ONE
|
||||
if pop_tween == null:
|
||||
return
|
||||
center_pivot(control)
|
||||
control.scale = Vector2.ONE * peak
|
||||
pop_tween.tween_property(control, "scale", Vector2.ONE, duration) \
|
||||
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Makes [param control] grow from [param from] times its size with a bounce.
|
||||
static func bounce_in(control: Control, from: float = 0.4, duration: float = SLOW) -> void:
|
||||
var bounce_tween: Tween = tween(control, &"pop")
|
||||
control.scale = Vector2.ONE
|
||||
if bounce_tween == null:
|
||||
return
|
||||
center_pivot(control)
|
||||
control.scale = Vector2.ONE * from
|
||||
bounce_tween.tween_property(control, "scale", Vector2.ONE, duration * 1.6) \
|
||||
.set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Fades [param item] in from invisible.
|
||||
static func fade_in(item: CanvasItem, duration: float = NORMAL) -> void:
|
||||
var fade_tween: Tween = tween(item, &"fade")
|
||||
item.modulate.a = 1.0
|
||||
if fade_tween == null:
|
||||
return
|
||||
item.modulate.a = 0.0
|
||||
fade_tween.tween_property(item, "modulate:a", 1.0, duration)
|
||||
|
||||
|
||||
## Slides [param control] in from [param offset] away from where it is, fading in.
|
||||
static func slide_in(control: Control, offset: Vector2, duration: float = NORMAL) -> void:
|
||||
var slide_tween: Tween = tween(control, &"slide")
|
||||
if control.has_meta(&"_slide_home"):
|
||||
control.position = control.get_meta(&"_slide_home")
|
||||
control.modulate.a = 1.0
|
||||
if slide_tween == null:
|
||||
return
|
||||
var home: Vector2 = control.position
|
||||
control.set_meta(&"_slide_home", home)
|
||||
control.position = home + offset
|
||||
control.modulate.a = 0.0
|
||||
slide_tween.set_parallel()
|
||||
slide_tween.tween_property(control, "position", home, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
slide_tween.tween_property(control, "modulate:a", 1.0, duration)
|
||||
slide_tween.chain().tween_callback(func() -> void: control.remove_meta(&"_slide_home"))
|
||||
|
||||
|
||||
## Scales [param control] to [param target] times its size, e.g. a little bigger under
|
||||
## the mouse.
|
||||
static func scale_to(control: Control, target: float, duration: float = FAST) -> void:
|
||||
var scale_tween: Tween = tween(control, &"scale")
|
||||
if scale_tween == null:
|
||||
control.scale = Vector2.ONE
|
||||
return
|
||||
center_pivot(control)
|
||||
scale_tween.tween_property(control, "scale", Vector2.ONE * target, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Lights up [param container] with [param colour] for a moment, e.g. a row that just
|
||||
## changed. A coloured layer is laid over the container and fades away.
|
||||
static func flash(container: Container, colour: Color, duration: float = SLOW) -> void:
|
||||
if !enabled() || !container.is_inside_tree():
|
||||
return
|
||||
var layer: ColorRect = _layer(container, colour)
|
||||
var flash_tween: Tween = layer.create_tween()
|
||||
flash_tween.tween_property(layer, "color:a", 0.0, duration) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
flash_tween.tween_callback(layer.queue_free)
|
||||
|
||||
|
||||
## Keeps [param container] glowing in [param colour], softly pulsing, until the returned
|
||||
## layer is freed. With animations off the glow stays still.
|
||||
static func pulse(container: Container, colour: Color) -> ColorRect:
|
||||
var layer: ColorRect = _layer(container, colour)
|
||||
layer.name = "Pulse"
|
||||
if enabled():
|
||||
var pulse_tween: Tween = layer.create_tween().set_loops()
|
||||
pulse_tween.tween_property(layer, "color:a", colour.a * 0.3, 0.6) \
|
||||
.set_trans(Tween.TRANS_SINE)
|
||||
pulse_tween.tween_property(layer, "color:a", colour.a, 0.6) \
|
||||
.set_trans(Tween.TRANS_SINE)
|
||||
return layer
|
||||
|
||||
|
||||
# A layer of [param colour] over the whole of [param container] that lets clicks through
|
||||
static func _layer(container: Container, colour: Color) -> ColorRect:
|
||||
var layer: ColorRect = ColorRect.new()
|
||||
layer.color = colour
|
||||
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(layer)
|
||||
return layer
|
||||
@@ -0,0 +1 @@
|
||||
uid://67is3ejxq0j8
|
||||
@@ -0,0 +1,118 @@
|
||||
class_name AnimatedProgressBar
|
||||
extends ProgressBar
|
||||
## A progress bar with some life in it:
|
||||
## - [method wait] shows Godot's busy animation until there is any progress;
|
||||
## - [method move_to] glides to each new value instead of jumping;
|
||||
## - a soft highlight sweeps along the filled part, kept inside its rounded corners.
|
||||
##
|
||||
## The bar's style, e.g. [code]SyncProgressBar[/code], has a [code]shine[/code] colour for
|
||||
## the highlight, and two more styles go with it: [code]<style>Fill[/code], a Panel shaped
|
||||
## like the filled part that the highlight is kept inside, and [code]<style>Text[/code],
|
||||
## the Label with the percentage. With Reduce animations the bar only jumps to its values,
|
||||
## with no busy animation or highlight.
|
||||
|
||||
## How wide the highlight is, in pixels.
|
||||
const SHINE_WIDTH: float = 260.0
|
||||
## How fast the highlight moves, in pixels per second.
|
||||
const SHINE_SPEED: float = 520.0
|
||||
## How far past the end the highlight goes before starting over, so sweeps have a pause.
|
||||
const SHINE_GAP: float = 600.0
|
||||
|
||||
# The filled part's shape, which only its children are drawn inside
|
||||
var _mask: Panel
|
||||
var _shine: TextureRect
|
||||
# The percentage, drawn above the highlight rather than under it
|
||||
var _label: Label
|
||||
var _shine_x: float = -SHINE_WIDTH
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_mask = Panel.new()
|
||||
_mask.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_mask.clip_children = CanvasItem.CLIP_CHILDREN_ONLY
|
||||
add_child(_mask)
|
||||
_shine = TextureRect.new()
|
||||
_shine.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_shine.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_shine.stretch_mode = TextureRect.STRETCH_SCALE
|
||||
_mask.add_child(_shine)
|
||||
_label = Label.new()
|
||||
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
_label.visible = show_percentage
|
||||
add_child(_label)
|
||||
show_percentage = false
|
||||
_mask.theme_type_variation = StringName(String(theme_type_variation) + "Fill")
|
||||
_label.theme_type_variation = StringName(String(theme_type_variation) + "Text")
|
||||
theme_changed.connect(_update_style)
|
||||
value_changed.connect(func(_value: float) -> void: _update_label())
|
||||
_update_style()
|
||||
_update_label()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var fill_width: float = fill_width()
|
||||
_mask.size = Vector2(fill_width, size.y)
|
||||
_mask.visible = fill_width > 0.0 && !indeterminate && Anim.enabled()
|
||||
if !_mask.visible:
|
||||
return
|
||||
_shine_x += SHINE_SPEED * delta
|
||||
if _shine_x > fill_width + SHINE_GAP:
|
||||
_shine_x = -SHINE_WIDTH
|
||||
_shine.position = Vector2(_shine_x, 0.0)
|
||||
_shine.size = Vector2(SHINE_WIDTH, size.y)
|
||||
|
||||
|
||||
## Shows the busy animation, with no value, until [method move_to] has some progress.
|
||||
func wait() -> void:
|
||||
Anim.stop(self, &"progress")
|
||||
value = min_value
|
||||
indeterminate = Anim.enabled()
|
||||
_update_label()
|
||||
|
||||
|
||||
## Glides to [param target]. Any progress ends the busy animation.
|
||||
func move_to(target: float) -> void:
|
||||
if target > min_value:
|
||||
indeterminate = false
|
||||
var glide: Tween = Anim.tween(self, &"progress")
|
||||
if glide == null:
|
||||
value = target
|
||||
return
|
||||
glide.tween_property(self, "value", target, Anim.SLOW) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## How wide the filled part is, the way [ProgressBar] draws it.
|
||||
func fill_width() -> float:
|
||||
var margins: float = get_theme_stylebox(&"fill").get_minimum_size().x
|
||||
var filled: float = roundf(ratio_done() * (size.x - margins))
|
||||
return filled + margins if filled > 0.0 else 0.0
|
||||
|
||||
|
||||
func _update_style() -> void:
|
||||
var shine: Color = get_theme_color(&"shine")
|
||||
var clear: Color = shine
|
||||
clear.a = 0.0
|
||||
var gradient: Gradient = Gradient.new()
|
||||
gradient.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
||||
gradient.colors = PackedColorArray([clear, shine, clear])
|
||||
var texture: GradientTexture2D = GradientTexture2D.new()
|
||||
texture.gradient = gradient
|
||||
texture.width = 256
|
||||
texture.height = 1
|
||||
_shine.texture = texture
|
||||
|
||||
|
||||
func _update_label() -> void:
|
||||
if _label == null:
|
||||
return
|
||||
_label.text = "" if indeterminate else "%d%%" % roundi(ratio_done() * 100.0)
|
||||
|
||||
|
||||
## How much is done, from 0 to 1.
|
||||
func ratio_done() -> float:
|
||||
var span: float = max_value - min_value
|
||||
return clampf((value - min_value) / span, 0.0, 1.0) if span > 0.0 else 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bxjx3kacfqg6j
|
||||
@@ -0,0 +1,30 @@
|
||||
class_name AudioFile
|
||||
extends RefCounted
|
||||
## Reads songs from files: the songs downloaded from the server, and the songs in a music
|
||||
## folder.
|
||||
|
||||
## The file types Godot can play from a file while the game runs.
|
||||
const PLAYABLE_EXTENSIONS: Array[String] = ["mp3", "ogg", "wav"]
|
||||
|
||||
|
||||
## Whether [param path] is a file type the game can play.
|
||||
static func is_playable(path: String) -> bool:
|
||||
return path.get_extension().to_lower() in PLAYABLE_EXTENSIONS
|
||||
|
||||
|
||||
## Reads the song in [param path]. Returns null if the file is missing, is not a playable
|
||||
## type, or is broken.
|
||||
static func load(path: String) -> AudioStream:
|
||||
if !FileAccess.file_exists(path):
|
||||
return null
|
||||
var stream: AudioStream = null
|
||||
match path.get_extension().to_lower():
|
||||
"mp3":
|
||||
stream = AudioStreamMP3.load_from_file(path)
|
||||
"ogg":
|
||||
stream = AudioStreamOggVorbis.load_from_file(path)
|
||||
"wav":
|
||||
stream = AudioStreamWAV.load_from_file(path)
|
||||
if stream == null || stream.get_length() <= 0.0:
|
||||
return null
|
||||
return stream
|
||||
@@ -0,0 +1 @@
|
||||
uid://drle78nqw2ve0
|
||||
@@ -0,0 +1,97 @@
|
||||
class_name ButtonBounce
|
||||
extends RefCounted
|
||||
## Small animations that make buttons feel alive: a button grows a little under the mouse,
|
||||
## shrinks while it is held down, and can pop when its icon changes.
|
||||
##
|
||||
## The button is scaled around its middle, so its place in a container does not change.
|
||||
## With [member Settings.reduce_animations] on, buttons keep their size.
|
||||
|
||||
## How big a button is under the mouse.
|
||||
const HOVER_SCALE: Vector2 = Vector2(1.08, 1.08)
|
||||
## How big a button is while it is held down.
|
||||
const PRESSED_SCALE: Vector2 = Vector2(0.92, 0.92)
|
||||
## How small a button starts when it pops.
|
||||
const POP_SCALE: Vector2 = Vector2(0.8, 0.8)
|
||||
## How long growing or shrinking takes, in seconds.
|
||||
const DURATION: float = 0.08
|
||||
## How long a pop takes, in seconds.
|
||||
const POP_DURATION: float = 0.18
|
||||
|
||||
const _TWEEN_META: StringName = &"button_bounce_tween"
|
||||
const _PIVOT_META: StringName = &"button_bounce_pivot"
|
||||
const _ADDED_META: StringName = &"button_bounce_added"
|
||||
|
||||
|
||||
## Gives every button in [param tree] the bounce, also buttons added later, e.g. rows
|
||||
## of the player list. Check buttons are left out: they span a whole settings row.
|
||||
static func add_to_every_button(tree: SceneTree) -> void:
|
||||
if !tree.node_added.is_connected(_on_node_added):
|
||||
tree.node_added.connect(_on_node_added)
|
||||
var nodes: Array[Node] = [tree.root]
|
||||
while !nodes.is_empty():
|
||||
var node: Node = nodes.pop_back()
|
||||
_on_node_added(node)
|
||||
nodes.append_array(node.get_children())
|
||||
|
||||
|
||||
static func _on_node_added(node: Node) -> void:
|
||||
if node is BaseButton && !(node is CheckButton) && !(node is CheckBox):
|
||||
add_to(node)
|
||||
|
||||
|
||||
## Makes [param button] grow under the mouse and shrink while held down. Adding it to
|
||||
## the same button twice does nothing.
|
||||
static func add_to(button: BaseButton) -> void:
|
||||
if button.has_meta(_ADDED_META):
|
||||
return
|
||||
button.set_meta(_ADDED_META, true)
|
||||
_keep_pivot_centred(button)
|
||||
button.mouse_entered.connect(func() -> void:
|
||||
if !button.disabled:
|
||||
_scale_to(button, HOVER_SCALE))
|
||||
button.mouse_exited.connect(func() -> void: _scale_to(button, Vector2.ONE))
|
||||
button.button_down.connect(func() -> void: _scale_to(button, PRESSED_SCALE))
|
||||
button.button_up.connect(func() -> void: _scale_to(button, resting_scale(button)))
|
||||
|
||||
|
||||
## The size [param button] goes back to when it is not held down: bigger under the mouse.
|
||||
static func resting_scale(button: BaseButton) -> Vector2:
|
||||
return HOVER_SCALE if button.is_hovered() && !button.disabled else Vector2.ONE
|
||||
|
||||
|
||||
## Shrinks [param button] and lets it spring back, e.g. when its icon changes.
|
||||
static func pop(button: BaseButton) -> void:
|
||||
if !Anim.enabled():
|
||||
return
|
||||
_keep_pivot_centred(button)
|
||||
button.scale = POP_SCALE
|
||||
var tween: Tween = _new_tween(button)
|
||||
tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(button, ^"scale", resting_scale(button), POP_DURATION)
|
||||
|
||||
|
||||
static func _scale_to(button: BaseButton, target: Vector2) -> void:
|
||||
if !Anim.enabled():
|
||||
button.scale = Vector2.ONE
|
||||
return
|
||||
var tween: Tween = _new_tween(button)
|
||||
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(button, ^"scale", target, DURATION)
|
||||
|
||||
|
||||
# A new tween for [param button], stopping the one before so they never fight
|
||||
static func _new_tween(button: BaseButton) -> Tween:
|
||||
if button.has_meta(_TWEEN_META):
|
||||
var running: Tween = button.get_meta(_TWEEN_META)
|
||||
if running.is_valid():
|
||||
running.kill()
|
||||
var tween: Tween = button.create_tween()
|
||||
button.set_meta(_TWEEN_META, tween)
|
||||
return tween
|
||||
|
||||
|
||||
static func _keep_pivot_centred(button: BaseButton) -> void:
|
||||
button.pivot_offset = button.size / 2
|
||||
if !button.has_meta(_PIVOT_META):
|
||||
button.set_meta(_PIVOT_META, true)
|
||||
button.resized.connect(func() -> void: button.pivot_offset = button.size / 2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4vekt3m8q1p7
|
||||
@@ -0,0 +1,111 @@
|
||||
class_name CharacterCache
|
||||
extends RefCounted
|
||||
## Character pictures kept on disk in the [code]characters[/code] folder next to the
|
||||
## settings, so each one is downloaded only when it changed on the server.
|
||||
##
|
||||
## [code]index.json[/code] in the folder records, for each picture, when the server last
|
||||
## changed it and when it was downloaded:
|
||||
## [codeblock]
|
||||
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
|
||||
## [/codeblock]
|
||||
## A cached picture is used while the server's change time matches the index. Servers that
|
||||
## can't tell when pictures changed answer 404 for [code]/character/details[/code]; then a
|
||||
## cached picture is used until it is [constant MAX_AGE_SECONDS] old. When the server
|
||||
## can't be reached at all, the cached pictures are used as they are.
|
||||
|
||||
const FOLDER_NAME: String = "characters"
|
||||
const INDEX_FILE_NAME: String = FileCache.INDEX_FILE_NAME
|
||||
## How long a picture is used before downloading it again, when the server can't tell
|
||||
## whether it changed.
|
||||
const MAX_AGE_SECONDS: int = 7 * 24 * 60 * 60
|
||||
## The change time of a picture when the server can't tell it.
|
||||
const UNKNOWN_TIME: int = FileCache.UNKNOWN_TIME
|
||||
|
||||
# The pictures on disk and their index
|
||||
var _files: FileCache = FileCache.new(FOLDER_NAME)
|
||||
# When the server last changed each picture on the list, or UNKNOWN_TIME
|
||||
var _modified: Dictionary[String, int] = {}
|
||||
# True when the server could not be reached and only cached pictures are shown
|
||||
var _cache_only: bool = false
|
||||
|
||||
|
||||
## The folder the pictures are kept in.
|
||||
static func folder() -> String:
|
||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
||||
|
||||
|
||||
## Deletes every cached picture. They are downloaded again when next asked for.
|
||||
func clear() -> void:
|
||||
_files.clear()
|
||||
|
||||
|
||||
## Fetches the names of the characters and calls [param on_done] with them as an
|
||||
## [code]Array[String][/code]. Cached pictures of characters no longer on the server are
|
||||
## deleted.
|
||||
func load_list(on_done: Callable) -> void:
|
||||
_files.load_index()
|
||||
_cache_only = false
|
||||
var details_fetched: Callable = func details_fetched(details: Array) -> void:
|
||||
_modified.clear()
|
||||
for character: CharacterDetails in details:
|
||||
_modified[character.name] = character.modified
|
||||
_list_loaded(on_done)
|
||||
MusicServerClient.fetch_character_details(details_fetched, _load_names_only.bind(on_done))
|
||||
|
||||
|
||||
## Calls [param on_done] with the picture of [param character_name] as an [Image], from
|
||||
## disk when the cached one is still fresh, otherwise from the server.
|
||||
func fetch_image(character_name: String, on_done: Callable) -> void:
|
||||
if (_cache_only || _is_fresh(character_name)) && _files.has_file(character_name):
|
||||
var cached: Image = CharacterCard.image_from_bytes(_files.read(character_name))
|
||||
if cached != null:
|
||||
on_done.call(cached)
|
||||
return
|
||||
GameLog.warning("The cached picture of ", character_name, " is broken")
|
||||
var image_fetched: Callable = func image_fetched(bytes: PackedByteArray) -> void:
|
||||
var picture: Image = CharacterCard.image_from_bytes(bytes)
|
||||
if picture == null:
|
||||
GameLog.warning("Could not load the picture of ", character_name)
|
||||
return
|
||||
_files.store(character_name, bytes, _modified.get(character_name, UNKNOWN_TIME))
|
||||
on_done.call(picture)
|
||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
||||
|
||||
|
||||
# Servers older than /character/details only list the names
|
||||
func _load_names_only(on_done: Callable) -> void:
|
||||
var list_fetched: Callable = func list_fetched(list: Variant) -> void:
|
||||
_modified.clear()
|
||||
if list is Array:
|
||||
for character_name: Variant in list:
|
||||
_modified[str(character_name)] = UNKNOWN_TIME
|
||||
_list_loaded(on_done)
|
||||
MusicServerClient.fetch_character_list(list_fetched, _load_cached_only.bind(on_done))
|
||||
|
||||
|
||||
# The server can't be reached, so the pictures already cached are shown and kept
|
||||
func _load_cached_only(on_done: Callable) -> void:
|
||||
GameLog.warning("Could not get the characters from the server, showing the cached ones")
|
||||
_cache_only = true
|
||||
var names: Array[String] = []
|
||||
for character_name: String in _files.keys():
|
||||
if _files.has_file(character_name):
|
||||
names.append(character_name)
|
||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
||||
on_done.call(names)
|
||||
|
||||
|
||||
func _list_loaded(on_done: Callable) -> void:
|
||||
_files.keep_only(_modified)
|
||||
var names: Array[String] = []
|
||||
names.assign(_modified.keys())
|
||||
on_done.call(names)
|
||||
|
||||
|
||||
func _is_fresh(character_name: String) -> bool:
|
||||
if !_files.keys().has(character_name):
|
||||
return false
|
||||
var modified: int = _modified.get(character_name, UNKNOWN_TIME)
|
||||
if modified != UNKNOWN_TIME:
|
||||
return _files.modified(character_name) == modified
|
||||
return _files.age(character_name) < MAX_AGE_SECONDS
|
||||
@@ -0,0 +1 @@
|
||||
uid://34xwe7vs2md7
|
||||
@@ -0,0 +1,149 @@
|
||||
class_name CoverCache
|
||||
extends RefCounted
|
||||
## Soundtrack covers kept on disk in the [code]covers[/code] folder next to the settings,
|
||||
## by soundtrack UUID, so each one is downloaded only when it changed on the server (its
|
||||
## [code]cover_modified[/code] time). See [FileCache].
|
||||
##
|
||||
## Covers are downloaded a few at a time, in the order they are asked for, so opening the
|
||||
## statistics doesn't send a request for every soundtrack at once.
|
||||
##
|
||||
## It also remembers each soundtrack's cover time from the soundtrack lists it is given,
|
||||
## so [method fetch_cover_of] finds a cover by the soundtrack alone, e.g. in the quiz.
|
||||
|
||||
## Emitted when the cover time of a soundtrack is new or changed, e.g. once the soundtrack
|
||||
## list has loaded or a cover was changed.
|
||||
signal cover_times_changed
|
||||
|
||||
const FOLDER_NAME: String = "covers"
|
||||
## How many covers are downloaded at the same time.
|
||||
const MAX_DOWNLOADS: int = 3
|
||||
|
||||
# The covers on disk and their index
|
||||
var _files: FileCache = FileCache.new(FOLDER_NAME)
|
||||
# Covers waiting to be downloaded: [uuid, modified]
|
||||
var _waiting: Array[Array] = []
|
||||
# Who is waiting for each cover that is asked for or downloading: uuid -> Array[Callable]
|
||||
var _callbacks: Dictionary = {}
|
||||
var _downloading: int = 0
|
||||
# The server's cover time of each soundtrack seen: uuid -> int, 0 for none
|
||||
var _times: Dictionary = {}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_files.load_index()
|
||||
|
||||
|
||||
## The folder the covers are kept in.
|
||||
static func folder() -> String:
|
||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
||||
|
||||
|
||||
## Deletes every cached cover. They are downloaded again when next asked for.
|
||||
func clear() -> void:
|
||||
_files.clear()
|
||||
|
||||
|
||||
## Deletes the covers of soundtracks that are not in [param covers] (UUID -> cover
|
||||
## time), and those whose soundtrack no longer has a cover.
|
||||
func keep_only(covers: Dictionary) -> void:
|
||||
set_cover_times(covers)
|
||||
var kept: Dictionary = {}
|
||||
for uuid: String in covers:
|
||||
if int(covers[uuid]) > 0:
|
||||
kept[uuid] = true
|
||||
_files.keep_only(kept)
|
||||
|
||||
|
||||
## Remembers the cover times in [param covers] (UUID -> cover time, 0 for no cover) and
|
||||
## emits [signal cover_times_changed] when any is new or changed.
|
||||
func set_cover_times(covers: Dictionary) -> void:
|
||||
var changed: bool = false
|
||||
for uuid: String in covers:
|
||||
var modified: int = int(covers[uuid])
|
||||
if _times.get(uuid, -1) != modified:
|
||||
_times[uuid] = modified
|
||||
changed = true
|
||||
if changed:
|
||||
cover_times_changed.emit()
|
||||
|
||||
|
||||
## The cover time of the soundtrack [param uuid], 0 when it has no cover or none is known.
|
||||
func cover_time(uuid: String) -> int:
|
||||
return _times.get(uuid, 0)
|
||||
|
||||
|
||||
## Like [method fetch_cover], with the cover time remembered for the soundtrack.
|
||||
func fetch_cover_of(uuid: String, on_done: Callable) -> void:
|
||||
fetch_cover(uuid, cover_time(uuid), on_done)
|
||||
|
||||
|
||||
## Asks the server for the cover time of the soundtrack [param uuid] again, e.g. after
|
||||
## its cover was changed, then calls [param on_done] with the new time, 0 for no cover.
|
||||
## A removed cover is deleted from disk too. When the server doesn't answer, it is called
|
||||
## with the time it had.
|
||||
func refresh(uuid: String, on_done: Callable) -> void:
|
||||
var with_soundtrack: Callable = func with_soundtrack(
|
||||
soundtrack: SoundtrackWithSongList) -> void:
|
||||
if soundtrack.cover_modified <= 0:
|
||||
_files.remove(uuid)
|
||||
set_cover_times({uuid: soundtrack.cover_modified})
|
||||
on_done.call(soundtrack.cover_modified)
|
||||
# Without an answer the cover is left as it was
|
||||
MusicServerClient.fetch_soundtrack(uuid, with_soundtrack,
|
||||
func() -> void: on_done.call(cover_time(uuid)))
|
||||
|
||||
|
||||
## Calls [param on_done] with the cover of the soundtrack [param uuid] as an [Image]:
|
||||
## from disk when the cached one has the server's time [param modified], otherwise once
|
||||
## it has been downloaded. Nothing is called when the soundtrack has no cover
|
||||
## ([param modified] 0) or the download fails.
|
||||
func fetch_cover(uuid: String, modified: int, on_done: Callable) -> void:
|
||||
if modified <= 0 || uuid.is_empty():
|
||||
return
|
||||
if _files.has_file(uuid) && _files.modified(uuid) == modified:
|
||||
var cached: Image = CharacterCard.image_from_bytes(_files.read(uuid))
|
||||
if cached != null:
|
||||
on_done.call(cached)
|
||||
return
|
||||
if _callbacks.has(uuid):
|
||||
_callbacks[uuid].append(on_done)
|
||||
return
|
||||
_callbacks[uuid] = [on_done]
|
||||
_waiting.append([uuid, modified])
|
||||
_download_next()
|
||||
|
||||
|
||||
## How many covers are waiting for their turn or downloading.
|
||||
func busy() -> int:
|
||||
return _callbacks.size()
|
||||
|
||||
|
||||
func _download_next() -> void:
|
||||
while _downloading < MAX_DOWNLOADS && !_waiting.is_empty():
|
||||
var next: Array = _waiting.pop_front()
|
||||
var uuid: String = next[0]
|
||||
_downloading += 1
|
||||
MusicServerClient.fetch_soundtrack_cover(uuid,
|
||||
_on_downloaded.bind(uuid, next[1]), _on_failed.bind(uuid))
|
||||
|
||||
|
||||
func _on_downloaded(bytes: PackedByteArray, uuid: String, modified: int) -> void:
|
||||
_downloading -= 1
|
||||
var cover: Image = CharacterCard.image_from_bytes(bytes)
|
||||
if cover == null:
|
||||
GameLog.warning("The cover of soundtrack ", uuid, " could not be read")
|
||||
else:
|
||||
_files.store(uuid, bytes, modified)
|
||||
for on_done: Callable in _callbacks.get(uuid, []):
|
||||
# A card may be gone by the time its cover arrives
|
||||
if cover != null && on_done.is_valid():
|
||||
on_done.call(cover)
|
||||
_callbacks.erase(uuid)
|
||||
_download_next()
|
||||
|
||||
|
||||
# A 404 means the server has no cover for it yet
|
||||
func _on_failed(uuid: String) -> void:
|
||||
_downloading -= 1
|
||||
_callbacks.erase(uuid)
|
||||
_download_next()
|
||||
@@ -0,0 +1 @@
|
||||
uid://djxm2kkq8hol1
|
||||
@@ -0,0 +1,140 @@
|
||||
class_name FileCache
|
||||
extends RefCounted
|
||||
## Downloaded files kept in a folder next to the settings, with an [code]index.json[/code]
|
||||
## that records for each file when the server last changed it and when it was
|
||||
## downloaded:
|
||||
## [codeblock]
|
||||
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
|
||||
## [/codeblock]
|
||||
## Used by [CharacterCache] and [CoverCache], which decide when a file is fresh.
|
||||
|
||||
const INDEX_FILE_NAME: String = "index.json"
|
||||
## The change time of a file when the server can't tell it.
|
||||
const UNKNOWN_TIME: int = -1
|
||||
|
||||
## The folder's name in [method Settings.config_dir].
|
||||
var folder_name: String
|
||||
|
||||
# What index.json holds: key -> {"modified": int, "downloaded": int}
|
||||
var _index: Dictionary = {}
|
||||
|
||||
|
||||
func _init(new_folder_name: String) -> void:
|
||||
folder_name = new_folder_name
|
||||
|
||||
|
||||
## The folder the files are kept in.
|
||||
func folder() -> String:
|
||||
return Settings.config_dir().path_join(folder_name)
|
||||
|
||||
|
||||
## Reads [code]index.json[/code]. A missing or broken index starts empty.
|
||||
func load_index() -> void:
|
||||
_index = {}
|
||||
var path: String = folder().path_join(INDEX_FILE_NAME)
|
||||
if FileAccess.file_exists(path):
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||
if parsed is Dictionary:
|
||||
_index = parsed
|
||||
|
||||
|
||||
## The keys of the files in the index.
|
||||
func keys() -> Array:
|
||||
return _index.keys()
|
||||
|
||||
|
||||
## Whether the file for [param key] is in the index and on disk.
|
||||
func has_file(key: String) -> bool:
|
||||
return _index.has(key) && FileAccess.file_exists(file_path(key))
|
||||
|
||||
|
||||
## When the server last changed the file for [param key], as recorded when it was
|
||||
## downloaded, or [constant UNKNOWN_TIME].
|
||||
func modified(key: String) -> int:
|
||||
return int(_index.get(key, {}).get("modified", UNKNOWN_TIME))
|
||||
|
||||
|
||||
## How many seconds ago the file for [param key] was downloaded.
|
||||
func age(key: String) -> float:
|
||||
return Time.get_unix_time_from_system() - float(_index.get(key, {}).get("downloaded", 0))
|
||||
|
||||
|
||||
## The file for [param key].
|
||||
func read(key: String) -> PackedByteArray:
|
||||
return FileAccess.get_file_as_bytes(file_path(key))
|
||||
|
||||
|
||||
## Writes [param bytes] as the file for [param key], last changed on the server at
|
||||
## [param modified_time], and saves the index. Returns false if it could not be written.
|
||||
func store(key: String, bytes: PackedByteArray, modified_time: int) -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(folder())
|
||||
var file: FileAccess = FileAccess.open(file_path(key), FileAccess.WRITE)
|
||||
if file == null:
|
||||
GameLog.warning("Could not cache ", key, " in ", folder_name, ": ",
|
||||
error_string(FileAccess.get_open_error()))
|
||||
return false
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
_index[key] = {
|
||||
"modified": modified_time,
|
||||
"downloaded": int(Time.get_unix_time_from_system()),
|
||||
}
|
||||
save_index()
|
||||
return true
|
||||
|
||||
|
||||
## Deletes the file for [param key] and its entry, and saves the index.
|
||||
func remove(key: String) -> void:
|
||||
DirAccess.remove_absolute(file_path(key))
|
||||
if _index.erase(key):
|
||||
save_index()
|
||||
|
||||
|
||||
## Deletes every file whose key is not in [param kept], e.g. files of things the server
|
||||
## no longer has.
|
||||
func keep_only(kept: Dictionary) -> void:
|
||||
var removed: bool = false
|
||||
for key: String in _index.keys():
|
||||
if !kept.has(key):
|
||||
DirAccess.remove_absolute(file_path(key))
|
||||
_index.erase(key)
|
||||
removed = true
|
||||
if removed:
|
||||
save_index()
|
||||
|
||||
|
||||
## Deletes every file in the folder, the index too, e.g. to free the space they take.
|
||||
func clear() -> void:
|
||||
_index = {}
|
||||
if !DirAccess.dir_exists_absolute(folder()):
|
||||
return
|
||||
for file_name: String in DirAccess.get_files_at(folder()):
|
||||
DirAccess.remove_absolute(folder().path_join(file_name))
|
||||
|
||||
|
||||
## How many bytes the files in [param dir] take, leaving out the paths in [param skip]
|
||||
## and any subfolders. 0 when the folder doesn't exist.
|
||||
static func folder_size(dir: String, skip: PackedStringArray = []) -> int:
|
||||
if !DirAccess.dir_exists_absolute(dir):
|
||||
return 0
|
||||
var total: int = 0
|
||||
for file_name: String in DirAccess.get_files_at(dir):
|
||||
var path: String = dir.path_join(file_name)
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||
if file != null && !skip.has(path):
|
||||
total += file.get_length()
|
||||
return total
|
||||
|
||||
|
||||
func save_index() -> void:
|
||||
DirAccess.make_dir_recursive_absolute(folder())
|
||||
var file: FileAccess = FileAccess.open(folder().path_join(INDEX_FILE_NAME),
|
||||
FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify(_index, "\t", true))
|
||||
|
||||
|
||||
## Where the file for [param key] is. Keys come from the server, so they are made safe
|
||||
## as file names.
|
||||
func file_path(key: String) -> String:
|
||||
return folder().path_join(key.validate_filename())
|
||||
@@ -0,0 +1 @@
|
||||
uid://0jfq23w54ihw
|
||||
@@ -0,0 +1,47 @@
|
||||
class_name FolderCharacters
|
||||
extends RefCounted
|
||||
## Characters read from a folder on this computer instead of the server: every PNG, JPG
|
||||
## or WebP picture in it is a character, named by its file name.
|
||||
##
|
||||
## Has the same two calls as [CharacterCache], so the character select popup uses either.
|
||||
|
||||
## The file endings of the pictures that are characters.
|
||||
const PICTURE_EXTENSIONS: Array[String] = ["png", "jpg", "jpeg", "webp"]
|
||||
|
||||
## The folder the pictures are in.
|
||||
var path: String = ""
|
||||
|
||||
|
||||
func _init(folder_path: String) -> void:
|
||||
path = folder_path
|
||||
|
||||
|
||||
## The file names of the pictures in [param folder], in natural order. Hidden files and
|
||||
## other files are left out.
|
||||
static func picture_names(folder: String) -> Array[String]:
|
||||
var names: Array[String] = []
|
||||
for file_name: String in DirAccess.get_files_at(folder):
|
||||
if !file_name.begins_with(".") \
|
||||
&& PICTURE_EXTENSIONS.has(file_name.get_extension().to_lower()):
|
||||
names.append(file_name)
|
||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
||||
return names
|
||||
|
||||
|
||||
## Calls [param on_done] with the names of the characters as an [code]Array[String][/code].
|
||||
func load_list(on_done: Callable) -> void:
|
||||
on_done.call(picture_names(path))
|
||||
|
||||
|
||||
## Calls [param on_done] with the picture of [param character_name] as an [Image]. Nothing
|
||||
## is called when the picture is gone or can't be read.
|
||||
func fetch_image(character_name: String, on_done: Callable) -> void:
|
||||
var file: String = path.path_join(character_name.validate_filename())
|
||||
if !FileAccess.file_exists(file):
|
||||
GameLog.warning("The picture of ", character_name, " is not in ", path)
|
||||
return
|
||||
var picture: Image = CharacterCard.image_from_bytes(FileAccess.get_file_as_bytes(file))
|
||||
if picture == null:
|
||||
GameLog.warning("Could not read the picture ", file)
|
||||
return
|
||||
on_done.call(picture)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc2nkmd162ibl
|
||||
@@ -0,0 +1,21 @@
|
||||
extends Node
|
||||
## Writes messages about what the game is doing, for finding problems.
|
||||
##
|
||||
## Registered as the [code]GameLog[/code] autoload. Use [method debug] instead of
|
||||
## [code]print()[/code]: the messages show in debug builds and the editor, and are left
|
||||
## out of released games. Problems the player may notice go through [method warning].
|
||||
|
||||
|
||||
## Prints [param parts] joined together, like [code]print()[/code], in debug builds only.
|
||||
func debug(...parts: Array) -> void:
|
||||
if OS.is_debug_build():
|
||||
print(_join(parts))
|
||||
|
||||
|
||||
## Reports a problem in the editor's debugger and the console, in every build.
|
||||
func warning(...parts: Array) -> void:
|
||||
push_warning(_join(parts))
|
||||
|
||||
|
||||
func _join(parts: Array) -> String:
|
||||
return "".join(parts.map(func(part: Variant) -> String: return str(part)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://c37mlo6svk03q
|
||||
@@ -0,0 +1,73 @@
|
||||
class_name IniFile
|
||||
extends RefCounted
|
||||
## A small INI file for settings people edit by hand: [code][section][/code] headers and
|
||||
## [code]key = value[/code] lines.
|
||||
##
|
||||
## Lines starting with [code];[/code] or [code]#[/code] are comments. Comments take whole
|
||||
## lines, so a value such as a password can hold any character. Quotes around a value are
|
||||
## optional and removed. Section and key names are not case-sensitive.
|
||||
|
||||
const TRUE_WORDS: Array[String] = ["true", "yes", "on", "1"]
|
||||
const FALSE_WORDS: Array[String] = ["false", "no", "off", "0"]
|
||||
|
||||
var _sections: Dictionary[String, Dictionary] = {}
|
||||
|
||||
|
||||
## Reads INI [param text]. Lines that are neither a section, a setting nor a comment are
|
||||
## skipped with a warning.
|
||||
static func parse(text: String) -> IniFile:
|
||||
var ini: IniFile = IniFile.new()
|
||||
var section: String = ""
|
||||
var line_number: int = 0
|
||||
for raw_line: String in text.split("\n"):
|
||||
line_number += 1
|
||||
var line: String = raw_line.strip_edges()
|
||||
if line.is_empty() or line.begins_with(";") or line.begins_with("#"):
|
||||
continue
|
||||
if line.begins_with("[") and line.ends_with("]"):
|
||||
section = line.substr(1, line.length() - 2).strip_edges().to_lower()
|
||||
continue
|
||||
var equals: int = line.find("=")
|
||||
if equals <= 0:
|
||||
push_warning("Line %d is not a setting: %s" % [line_number, line])
|
||||
continue
|
||||
var key: String = line.substr(0, equals).strip_edges().to_lower()
|
||||
var value: String = line.substr(equals + 1).strip_edges()
|
||||
if value.length() >= 2 and value[0] in ["\"", "'"] and value[-1] == value[0]:
|
||||
value = value.substr(1, value.length() - 2)
|
||||
if !ini._sections.has(section):
|
||||
ini._sections[section] = {}
|
||||
ini._sections[section][key] = value
|
||||
return ini
|
||||
|
||||
|
||||
## Reads the INI file at [param path]. A missing file gives an empty [IniFile].
|
||||
static func load_file(path: String) -> IniFile:
|
||||
return parse(FileAccess.get_file_as_string(path))
|
||||
|
||||
|
||||
func has_value(section: String, key: String) -> bool:
|
||||
return _sections.get(section.to_lower(), {}).has(key.to_lower())
|
||||
|
||||
|
||||
## The sections that have at least one setting.
|
||||
func get_sections() -> Array[String]:
|
||||
return _sections.keys()
|
||||
|
||||
|
||||
func get_string(section: String, key: String, default: String = "") -> String:
|
||||
return _sections.get(section.to_lower(), {}).get(key.to_lower(), default)
|
||||
|
||||
|
||||
## Reads true/false, yes/no, on/off or 1/0. Anything else gives [param default] and a
|
||||
## warning.
|
||||
func get_bool(section: String, key: String, default: bool) -> bool:
|
||||
if !has_value(section, key):
|
||||
return default
|
||||
var value: String = get_string(section, key).to_lower()
|
||||
if value in TRUE_WORDS:
|
||||
return true
|
||||
if value in FALSE_WORDS:
|
||||
return false
|
||||
push_warning("%s in [%s] should be true or false, not %s" % [key, section, value])
|
||||
return default
|
||||
@@ -0,0 +1 @@
|
||||
uid://b262vckl8hbn4
|
||||
@@ -0,0 +1,34 @@
|
||||
class_name ListRow
|
||||
extends PanelContainer
|
||||
## One item in a list of names, such as the inspiration list or a sync result column.
|
||||
##
|
||||
## Every row has its own background, alternating between the theme's [code]ListRowEven[/code]
|
||||
## and [code]ListRowOdd[/code] styles, so it is clear where an item starts even when its
|
||||
## text wraps over several lines.
|
||||
|
||||
## The label showing the item's text.
|
||||
var label: Label
|
||||
|
||||
## The item's text.
|
||||
var text: String:
|
||||
get:
|
||||
return label.text
|
||||
|
||||
|
||||
## Creates the row for item number [param row] (counting from 0) in a list. The text uses
|
||||
## the theme's [param label_style], e.g. [code]TextLarge[/code].
|
||||
static func create(
|
||||
item_text: String,
|
||||
row: int,
|
||||
label_style: StringName,
|
||||
alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT,
|
||||
) -> ListRow:
|
||||
var list_row: ListRow = ListRow.new()
|
||||
list_row.theme_type_variation = &"ListRowOdd" if row % 2 != 0 else &"ListRowEven"
|
||||
list_row.label = Label.new()
|
||||
list_row.label.theme_type_variation = label_style
|
||||
list_row.label.text = item_text
|
||||
list_row.label.horizontal_alignment = alignment
|
||||
list_row.label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
list_row.add_child(list_row.label)
|
||||
return list_row
|
||||
File renamed without changes.
@@ -0,0 +1,147 @@
|
||||
class_name SoundtrackSearch
|
||||
extends RefCounted
|
||||
## Finds soundtrack names matching a search text. Matches ignore case, spaces and
|
||||
## special characters. Searches of at least [code]Settings.LETTERS_FOR_LOOSE_SEARCH[/code]
|
||||
## letters also find the letters in order with others in between.
|
||||
##
|
||||
## Used by the Search popup and the statistics view, so both find the same soundtracks.
|
||||
##
|
||||
## A soundtrack is also found by its alternative names, e.g. "FF7" for "Final Fantasy
|
||||
## VII". The results are always the real names; [method matched_alternative] tells which
|
||||
## alternative name matched.
|
||||
|
||||
## Each searchable name as a Dictionary: the soundtrack's real name ("original"), the name
|
||||
## searched in ("text"), and its lowercase and cleaned forms.
|
||||
var _names: Array[Dictionary] = []
|
||||
# The alternative name that matched in the last search, by real name
|
||||
var _matched_alternatives: Dictionary = {}
|
||||
|
||||
## Compiled patterns for the letters-in-order search, by cleaned search term.
|
||||
var _regex_cache: Dictionary = {}
|
||||
|
||||
|
||||
## Replaces the names to search in. [param alternatives] has the other names of a
|
||||
## soundtrack, by its real name.
|
||||
func set_names(names: Array, alternatives: Dictionary = {}) -> void:
|
||||
_names.clear()
|
||||
_matched_alternatives.clear()
|
||||
for soundtrack: String in names:
|
||||
_add_name(soundtrack, soundtrack)
|
||||
for alternative: String in alternatives.get(soundtrack, []):
|
||||
_add_name(soundtrack, alternative)
|
||||
|
||||
|
||||
## The alternative name of [param soundtrack_name] that matched in the last
|
||||
## [method find_matches], or an empty String when its real name matched.
|
||||
func matched_alternative(soundtrack_name: String) -> String:
|
||||
return _matched_alternatives.get(soundtrack_name, "")
|
||||
|
||||
|
||||
## Returns the names 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 search_text_lower: String = search_text.to_lower()
|
||||
var clean_search_text: String = clean_term(search_text)
|
||||
var passes: Array[Callable] = [
|
||||
func(entry: Dictionary) -> bool: return entry.lowercase.contains(search_text_lower),
|
||||
func(entry: Dictionary) -> bool: return entry.cleaned.contains(clean_search_text),
|
||||
]
|
||||
if clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
||||
var regex: RegEx = _compile_regex_cached(clean_search_text)
|
||||
passes.append(
|
||||
func(entry: Dictionary) -> bool: return regex.search(entry.cleaned) != null)
|
||||
|
||||
var matches: Array[String] = []
|
||||
_matched_alternatives.clear()
|
||||
for matches_pass: Callable in passes:
|
||||
for entry: Dictionary in _names:
|
||||
if !seen.has(entry.original) && matches_pass.call(entry):
|
||||
seen[entry.original] = true
|
||||
matches.append(entry.original)
|
||||
if entry.text != entry.original:
|
||||
_matched_alternatives[entry.original] = entry.text
|
||||
return matches
|
||||
|
||||
|
||||
## The positions of the letters in [param soundtrack_name] that [param search_text]
|
||||
## matched, for highlighting them: the text itself if the name contains it, otherwise the
|
||||
## letters found in order the way [method find_matches] finds them. Empty if it does not
|
||||
## match.
|
||||
static func matched_letters(soundtrack_name: String, search_text: String) -> Array[int]:
|
||||
var positions: Array[int] = []
|
||||
if search_text.is_empty():
|
||||
return positions
|
||||
var start: int = soundtrack_name.to_lower().find(search_text.to_lower())
|
||||
if start >= 0:
|
||||
for i: int in search_text.length():
|
||||
positions.append(start + i)
|
||||
return positions
|
||||
|
||||
# Clean the name piece by piece, remembering which letter each cleaned letter came from
|
||||
var cleaned: String = ""
|
||||
var source: Array[int] = []
|
||||
var i: int = 0
|
||||
while i < soundtrack_name.length():
|
||||
var piece: String
|
||||
var length: int = 1
|
||||
if soundtrack_name.substr(i, 2).to_lower() == "'n":
|
||||
piece = "and"
|
||||
length = 2
|
||||
else:
|
||||
piece = clean_term(soundtrack_name[i])
|
||||
for letter: String in piece:
|
||||
cleaned += letter
|
||||
source.append(i)
|
||||
i += length
|
||||
|
||||
var clean_search_text: String = clean_term(search_text)
|
||||
var found: int = cleaned.find(clean_search_text)
|
||||
var cleaned_positions: Array[int] = []
|
||||
if found >= 0:
|
||||
for j: int in clean_search_text.length():
|
||||
cleaned_positions.append(found + j)
|
||||
elif clean_search_text.length() >= Settings.LETTERS_FOR_LOOSE_SEARCH:
|
||||
var at: int = 0
|
||||
for letter: String in clean_search_text:
|
||||
at = cleaned.find(letter, at)
|
||||
if at < 0:
|
||||
return positions
|
||||
cleaned_positions.append(at)
|
||||
at += 1
|
||||
for cleaned_position: int in cleaned_positions:
|
||||
if !positions.has(source[cleaned_position]):
|
||||
positions.append(source[cleaned_position])
|
||||
return positions
|
||||
|
||||
|
||||
## Cleans a term by removing spaces and replacing special characters.
|
||||
## Matches MusicServer's clean_term() function.
|
||||
static func clean_term(term: String) -> String:
|
||||
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and") \
|
||||
.replace("'n", "and").to_lower()
|
||||
|
||||
|
||||
func _add_name(soundtrack: String, text: String) -> void:
|
||||
_names.append({
|
||||
"original": soundtrack,
|
||||
"text": text,
|
||||
"lowercase": text.to_lower(),
|
||||
"cleaned": clean_term(text),
|
||||
})
|
||||
|
||||
|
||||
# Compiles the letters-in-order pattern for [param search_term], once per term
|
||||
func _compile_regex_cached(search_term: String) -> RegEx:
|
||||
if not search_term in _regex_cache:
|
||||
var regex: RegEx = RegEx.new()
|
||||
var regex_text: String = ".*"
|
||||
for letter: String in search_term:
|
||||
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
|
||||
if "\\^$.|?*+()[]{}".contains(letter):
|
||||
regex_text += "\\"
|
||||
regex_text += letter + ".*"
|
||||
regex.compile(regex_text)
|
||||
_regex_cache[search_term] = regex
|
||||
return _regex_cache[search_term]
|
||||
@@ -0,0 +1 @@
|
||||
uid://df8ox6xddobu2
|
||||
@@ -0,0 +1,174 @@
|
||||
class_name Updater
|
||||
extends Node
|
||||
## Updates the game in place to the newest release: downloads the new game next to the
|
||||
## running one, checks it, swaps the files and starts the new version.
|
||||
##
|
||||
## The game is one file on Windows and Linux (the game data is embedded). A running .exe
|
||||
## can't be overwritten on Windows, but it can be renamed, so it is moved aside to
|
||||
## [code]<exe>.old[/code], which [method remove_old_files] deletes at the next start. On
|
||||
## Linux the new file simply replaces the running one.
|
||||
##
|
||||
## [member executable_path], [member os_name], [member in_editor], [member start_process]
|
||||
## and [member quit] can be replaced, so tests choose the platform and nothing restarts.
|
||||
|
||||
## Where the newest release can be downloaded by hand.
|
||||
const RELEASES_PAGE: String = "https://gitea.sanplex.xyz/Sansan/MusicPlayer/releases/latest"
|
||||
## Added to the game's path for the downloaded new version.
|
||||
const UPDATE_SUFFIX: String = ".update"
|
||||
## Added to the game's path for the old version, on Windows.
|
||||
const OLD_SUFFIX: String = ".old"
|
||||
## rwxr-xr-x, for the new game on Linux.
|
||||
const EXECUTABLE_PERMISSIONS: int = 0b111_101_101
|
||||
## Seconds the whole download may take.
|
||||
const DOWNLOAD_TIMEOUT: float = 900.0
|
||||
|
||||
## The running game's file.
|
||||
var executable_path: String = OS.get_executable_path()
|
||||
## The platform, as [method OS.get_name] names it.
|
||||
var os_name: String = OS.get_name()
|
||||
## Whether the game runs from the Godot editor, where there is no game file to replace.
|
||||
var in_editor: bool = OS.has_feature("editor")
|
||||
## Starts a program: [code](path: String, arguments: PackedStringArray) -> int[/code].
|
||||
var start_process: Callable = OS.create_process
|
||||
## Quits the game.
|
||||
var quit: Callable
|
||||
|
||||
var _request: HTTPRequest
|
||||
var _on_progress: Callable
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
quit = func() -> void: get_tree().quit()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _request != null && _on_progress.is_valid():
|
||||
_on_progress.call(_request.get_downloaded_bytes(), _request.get_body_size())
|
||||
|
||||
|
||||
## The release file to download for [param os_name]: the game itself to update in place,
|
||||
## or, when not [param in_place], what a person downloads, which on Linux is the package
|
||||
## with an installer. Empty when there is no release for the platform.
|
||||
static func asset_for(for_os: String, in_place: bool) -> String:
|
||||
match for_os:
|
||||
"Windows":
|
||||
return "windows"
|
||||
"Linux":
|
||||
return "linux" if in_place else "linux-package"
|
||||
return ""
|
||||
|
||||
|
||||
## Why the game can't update itself here, or empty when it can.
|
||||
func can_update_in_place() -> String:
|
||||
if in_editor:
|
||||
return "The game runs from the Godot editor."
|
||||
if asset_for(os_name, true).is_empty():
|
||||
return "The game updates itself on Windows and Linux."
|
||||
var probe: String = executable_path + ".write-test"
|
||||
var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "The game's folder %s can't be written to." % executable_path.get_base_dir()
|
||||
file.close()
|
||||
DirAccess.remove_absolute(probe)
|
||||
return ""
|
||||
|
||||
|
||||
## Where the new version is downloaded to, next to the game so it can be renamed into
|
||||
## place.
|
||||
func update_path() -> String:
|
||||
return executable_path + UPDATE_SUFFIX
|
||||
|
||||
|
||||
## Downloads the new game from [param url]. Calls [param on_progress] with the bytes
|
||||
## downloaded and the total (-1 while unknown) every frame, then [param on_done] with the
|
||||
## file's path once it is checked, or [param on_failed] with the reason.
|
||||
func download(url: String, on_progress: Callable, on_done: Callable,
|
||||
on_failed: Callable) -> void:
|
||||
cancel()
|
||||
_on_progress = on_progress
|
||||
_request = HTTPRequest.new()
|
||||
_request.download_file = update_path()
|
||||
_request.timeout = DOWNLOAD_TIMEOUT
|
||||
add_child(_request)
|
||||
_request.request_completed.connect(_on_downloaded.bind(on_done, on_failed))
|
||||
var error: Error = _request.request(url)
|
||||
if error != OK:
|
||||
cancel()
|
||||
on_failed.call("The download could not start: " + error_string(error))
|
||||
|
||||
|
||||
## Stops a download and deletes what was downloaded.
|
||||
func cancel() -> void:
|
||||
if _request != null:
|
||||
_request.cancel_request()
|
||||
_request.queue_free()
|
||||
_request = null
|
||||
DirAccess.remove_absolute(update_path())
|
||||
|
||||
|
||||
## Whether [param path] looks like the game for [param for_os]: a Windows program starts
|
||||
## with "MZ", a Linux one with the ELF header. An error page saved by mistake does not.
|
||||
static func check_file(path: String, for_os: String) -> bool:
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
||||
if file == null || file.get_length() < 4:
|
||||
return false
|
||||
var start: PackedByteArray = file.get_buffer(4)
|
||||
match for_os:
|
||||
"Windows":
|
||||
return start.slice(0, 2) == "MZ".to_ascii_buffer()
|
||||
"Linux":
|
||||
return start == PackedByteArray([0x7F, 0x45, 0x4C, 0x46])
|
||||
return false
|
||||
|
||||
|
||||
## Puts the new game at [param update] in place of the game at [param exe]. On Windows
|
||||
## the running game is moved aside first, and put back if the new one can't be moved in.
|
||||
static func install(update: String, exe: String, for_os: String) -> Error:
|
||||
if for_os == "Windows":
|
||||
var old: String = exe + OLD_SUFFIX
|
||||
if FileAccess.file_exists(old):
|
||||
DirAccess.remove_absolute(old)
|
||||
var error: Error = DirAccess.rename_absolute(exe, old)
|
||||
if error != OK:
|
||||
return error
|
||||
error = DirAccess.rename_absolute(update, exe)
|
||||
if error != OK:
|
||||
DirAccess.rename_absolute(old, exe)
|
||||
return error
|
||||
FileAccess.set_unix_permissions(update, EXECUTABLE_PERMISSIONS)
|
||||
return DirAccess.rename_absolute(update, exe)
|
||||
|
||||
|
||||
## Deletes what an update left next to the game at [param exe]: the old version, and a
|
||||
## download that was not finished.
|
||||
static func remove_old_files(exe: String) -> void:
|
||||
for suffix: String in [OLD_SUFFIX, UPDATE_SUFFIX]:
|
||||
if FileAccess.file_exists(exe + suffix):
|
||||
DirAccess.remove_absolute(exe + suffix)
|
||||
|
||||
|
||||
## Starts the game again, now the new version, and quits this one.
|
||||
func restart() -> void:
|
||||
start_process.call(executable_path, OS.get_cmdline_args())
|
||||
quit.call()
|
||||
|
||||
|
||||
func _on_downloaded(result: int, response_code: int, _headers: PackedStringArray,
|
||||
_body: PackedByteArray, on_done: Callable, on_failed: Callable) -> void:
|
||||
var expected: int = _request.get_body_size()
|
||||
_request.queue_free()
|
||||
_request = null
|
||||
var reason: String = ""
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
reason = "The download failed. Check the connection and try again."
|
||||
elif response_code != 200:
|
||||
reason = "The download failed: the server answered %d." % response_code
|
||||
elif expected >= 0 && FileAccess.get_size(update_path()) != expected:
|
||||
reason = "The download stopped before the end. Try again."
|
||||
elif !check_file(update_path(), os_name):
|
||||
reason = "The downloaded file is not the game for %s." % os_name
|
||||
if !reason.is_empty():
|
||||
DirAccess.remove_absolute(update_path())
|
||||
on_failed.call(reason)
|
||||
return
|
||||
on_done.call(update_path())
|
||||
@@ -0,0 +1 @@
|
||||
uid://bu3bi7qhxr7it
|
||||
@@ -0,0 +1,183 @@
|
||||
class_name MusicFolder
|
||||
extends RefCounted
|
||||
## A folder of songs to play instead of the MusicServer's, picked in the settings.
|
||||
##
|
||||
## A folder with subfolders is a soundtrack folder: each subfolder is a soundtrack, and
|
||||
## songs are picked at random from a random soundtrack. Songs at the top are left out.
|
||||
## A folder with songs and no subfolders is a prepared quiz: every song plays once, in
|
||||
## order or at random, and only the song's name is the answer.
|
||||
##
|
||||
## Hidden files and folders, and files the game can't play (see [AudioFile]), are left
|
||||
## out. A subfolder called [code]characters[/code] holds the quiz's own characters
|
||||
## instead of songs, see [member characters_path].
|
||||
|
||||
enum Kind { EMPTY, SOUNDTRACKS, PREPARED_QUIZ }
|
||||
enum PlayOrder { IN_ORDER, RANDOM }
|
||||
|
||||
## How many files are tried before giving up when files can't be read.
|
||||
const MAX_TRIES: int = 20
|
||||
## The subfolder with the quiz's own characters. Any case, e.g. "Characters", works.
|
||||
const CHARACTERS_FOLDER: String = "characters"
|
||||
|
||||
## The folder's path.
|
||||
var path: String = ""
|
||||
var kind: Kind = Kind.EMPTY
|
||||
## How a prepared quiz is played. Change it with [method set_play_order].
|
||||
var play_order: PlayOrder = PlayOrder.IN_ORDER
|
||||
## The folder's [constant CHARACTERS_FOLDER] subfolder when it has pictures, so a themed
|
||||
## quiz can bring its own characters. Empty when there is none.
|
||||
var characters_path: String = ""
|
||||
|
||||
# Soundtrack folder: soundtrack name -> paths of its songs
|
||||
var _soundtracks: Dictionary[String, PackedStringArray] = {}
|
||||
# Prepared quiz: the songs in order, the order they are played in and how many are played
|
||||
var _quiz_songs: PackedStringArray = []
|
||||
var _quiz_queue: PackedStringArray = []
|
||||
var _quiz_next: int = 0
|
||||
|
||||
|
||||
## Reads the folder at [param folder_path]. Its [member kind] is [constant Kind.EMPTY]
|
||||
## when there is nothing to play in it.
|
||||
static func scan(folder_path: String) -> MusicFolder:
|
||||
var folder: MusicFolder = MusicFolder.new()
|
||||
folder.path = folder_path.trim_suffix("/")
|
||||
var subfolders: PackedStringArray = []
|
||||
for subfolder: String in _visible(DirAccess.get_directories_at(folder.path)):
|
||||
if subfolder.to_lower() == CHARACTERS_FOLDER:
|
||||
var characters: String = folder.path.path_join(subfolder)
|
||||
if !FolderCharacters.picture_names(characters).is_empty():
|
||||
folder.characters_path = characters
|
||||
else:
|
||||
subfolders.append(subfolder)
|
||||
if !subfolders.is_empty():
|
||||
for subfolder: String in subfolders:
|
||||
var songs: PackedStringArray = _songs_in(folder.path.path_join(subfolder), true)
|
||||
if !songs.is_empty():
|
||||
folder._soundtracks[subfolder] = songs
|
||||
if !folder._soundtracks.is_empty():
|
||||
folder.kind = Kind.SOUNDTRACKS
|
||||
else:
|
||||
folder._quiz_songs = _songs_in(folder.path, false)
|
||||
if !folder._quiz_songs.is_empty():
|
||||
folder.kind = Kind.PREPARED_QUIZ
|
||||
folder.restart()
|
||||
return folder
|
||||
|
||||
|
||||
## The folder's name, to show to the user.
|
||||
func display_name() -> String:
|
||||
return path.get_file()
|
||||
|
||||
|
||||
## The names of the soundtracks, sorted. Empty for a prepared quiz.
|
||||
func soundtrack_names() -> Array[String]:
|
||||
var names: Array[String] = []
|
||||
names.assign(_soundtracks.keys())
|
||||
names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
||||
return names
|
||||
|
||||
|
||||
## How many songs a prepared quiz has.
|
||||
func quiz_size() -> int:
|
||||
return _quiz_songs.size()
|
||||
|
||||
|
||||
## Plays a prepared quiz in [param order] from its first song.
|
||||
func set_play_order(order: PlayOrder) -> void:
|
||||
play_order = order
|
||||
restart()
|
||||
|
||||
|
||||
## Starts a prepared quiz over from its first song, shuffled again if it is played at
|
||||
## random.
|
||||
func restart() -> void:
|
||||
_quiz_queue = _quiz_songs.duplicate()
|
||||
if play_order == PlayOrder.RANDOM:
|
||||
var shuffled: Array = Array(_quiz_queue)
|
||||
shuffled.shuffle()
|
||||
_quiz_queue = PackedStringArray(shuffled)
|
||||
_quiz_next = 0
|
||||
|
||||
|
||||
## How far a prepared quiz has come, to continue it after a restart with
|
||||
## [method restore_progress].
|
||||
func progress() -> Dictionary:
|
||||
return {"play_order": play_order, "queue": Array(_quiz_queue), "next": _quiz_next}
|
||||
|
||||
|
||||
## Continues a prepared quiz from [param saved], as given by [method progress]. Songs
|
||||
## that are no longer in the folder are left out. Returns false, changing nothing, when
|
||||
## [param saved] does not fit this folder.
|
||||
func restore_progress(saved: Dictionary) -> bool:
|
||||
if kind != Kind.PREPARED_QUIZ || !(saved.get("queue") is Array) \
|
||||
|| !(saved.get("next") is float || saved.get("next") is int):
|
||||
return false
|
||||
var queue: PackedStringArray = []
|
||||
var next: int = int(saved.next)
|
||||
var known: Dictionary = {}
|
||||
for song_path: String in _quiz_songs:
|
||||
known[song_path] = true
|
||||
for i: int in saved.queue.size():
|
||||
if known.has(str(saved.queue[i])):
|
||||
queue.append(str(saved.queue[i]))
|
||||
elif i < int(saved.next):
|
||||
next -= 1
|
||||
play_order = int(saved.get("play_order", play_order)) as PlayOrder
|
||||
_quiz_queue = queue
|
||||
_quiz_next = clampi(next, 0, queue.size())
|
||||
return true
|
||||
|
||||
|
||||
## The next song to play, or null when a prepared quiz has played every song or no file
|
||||
## can be read.
|
||||
func next_song() -> SongObject:
|
||||
for i: int in MAX_TRIES:
|
||||
var song_path: String = _pick()
|
||||
if song_path.is_empty():
|
||||
return null
|
||||
if AudioFile.load(song_path) != null:
|
||||
return _song_object(song_path)
|
||||
GameLog.warning("Skipping a song that can't be played: ", song_path)
|
||||
return null
|
||||
|
||||
|
||||
func _pick() -> String:
|
||||
if kind == Kind.SOUNDTRACKS:
|
||||
var songs: PackedStringArray = _soundtracks[_soundtracks.keys().pick_random()]
|
||||
return songs[randi() % songs.size()]
|
||||
if _quiz_next >= _quiz_queue.size():
|
||||
return ""
|
||||
_quiz_next += 1
|
||||
return _quiz_queue[_quiz_next - 1]
|
||||
|
||||
|
||||
func _song_object(song_path: String) -> SongObject:
|
||||
var song_object: SongObject = SongObject.new()
|
||||
song_object.song_path = song_path
|
||||
song_object.song_title = song_path.get_file().get_basename()
|
||||
if kind == Kind.SOUNDTRACKS:
|
||||
# The soundtrack is the subfolder right under the music folder
|
||||
song_object.soundtrack_title = song_path.trim_prefix(path + "/").get_slice("/", 0)
|
||||
return song_object
|
||||
|
||||
|
||||
# The playable songs in [param dir], in natural order, also in subfolders if [param deep]
|
||||
static func _songs_in(dir: String, deep: bool) -> PackedStringArray:
|
||||
var songs: Array[String] = []
|
||||
for file_name: String in _visible(DirAccess.get_files_at(dir)):
|
||||
if AudioFile.is_playable(file_name):
|
||||
songs.append(dir.path_join(file_name))
|
||||
songs.sort_custom(func(a: String, b: String) -> bool:
|
||||
return a.get_file().naturalnocasecmp_to(b.get_file()) < 0)
|
||||
if deep:
|
||||
for subfolder: String in _visible(DirAccess.get_directories_at(dir)):
|
||||
songs.append_array(_songs_in(dir.path_join(subfolder), true))
|
||||
return PackedStringArray(songs)
|
||||
|
||||
|
||||
static func _visible(names: PackedStringArray) -> PackedStringArray:
|
||||
var shown: PackedStringArray = []
|
||||
for entry: String in names:
|
||||
if !entry.begins_with("."):
|
||||
shown.append(entry)
|
||||
return shown
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcrem1inl47ve
|
||||
@@ -0,0 +1,110 @@
|
||||
class_name PlayerObject
|
||||
extends RefCounted
|
||||
## A player in the quiz: name, score and character. The players are kept in
|
||||
## [code]Settings.player_array[/code].
|
||||
|
||||
signal first_point_triggered
|
||||
signal match_point_triggered
|
||||
signal winner_triggered
|
||||
signal point_given_sound
|
||||
signal player_point_given
|
||||
signal player_point_taken
|
||||
|
||||
## Picture shown until the player picks a character.
|
||||
const NO_CHARACTER_PATH: String = "res://noCharacter.png"
|
||||
|
||||
@export var player_name: String
|
||||
@export var player_score: int
|
||||
|
||||
## Unique id that never changes, unlike the player's position in Settings.player_array.
|
||||
var id: String = _generate_uuid()
|
||||
|
||||
var is_first_point: bool = true
|
||||
var character: Texture = load(NO_CHARACTER_PATH)
|
||||
## The name of the picked character, its picture's file name, or empty when none is
|
||||
## picked. Saved with the quiz, so the picture can be found again after a restart.
|
||||
var character_name: String = ""
|
||||
|
||||
|
||||
func _init(new_name: String) -> void:
|
||||
player_name = new_name
|
||||
player_score = 0
|
||||
|
||||
|
||||
## Shows [param texture], the character called [param new_name], for the player.
|
||||
func set_character(texture: Texture, new_name: String) -> void:
|
||||
character = texture
|
||||
character_name = new_name
|
||||
|
||||
|
||||
## Goes back to no character, e.g. when the characters come from somewhere else.
|
||||
func clear_character() -> void:
|
||||
character = load(NO_CHARACTER_PATH)
|
||||
character_name = ""
|
||||
|
||||
|
||||
## The player as a Dictionary that can be written as JSON, for [QuizSave].
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"id": id, "name": player_name, "score": player_score,
|
||||
"is_first_point": is_first_point, "character": character_name,
|
||||
}
|
||||
|
||||
|
||||
## A player made from [param data], as given by [method to_dict], or null when
|
||||
## [param data] is not a player. The character's picture is not loaded here.
|
||||
static func from_dict(data: Variant) -> PlayerObject:
|
||||
if !(data is Dictionary) || !(data.get("name") is String) || !(data.get("id") is String):
|
||||
return null
|
||||
var player: PlayerObject = PlayerObject.new(data.name)
|
||||
player.id = data.id
|
||||
player.player_score = maxi(int(data.get("score", 0)), 0)
|
||||
player.is_first_point = bool(data.get("is_first_point", player.player_score == 0))
|
||||
player.character_name = str(data.get("character", ""))
|
||||
return player
|
||||
|
||||
|
||||
## 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)]
|
||||
|
||||
|
||||
## Gives the player a point. With a winning score, the point that reaches it wins; a
|
||||
## prepared quiz has none, so points just add up until every song has played.
|
||||
func add_point() -> void:
|
||||
var has_winning_score: bool = !Settings.is_prepared_quiz()
|
||||
if has_winning_score && player_score >= Settings.winning_score:
|
||||
return
|
||||
player_score += 1
|
||||
player_point_given.emit()
|
||||
var first_point: bool = player_score == 1 && is_first_point
|
||||
if player_score == 1:
|
||||
is_first_point = false
|
||||
# With a low winning score the first point can also be match point or the win,
|
||||
# so the more important event is checked first
|
||||
if has_winning_score && player_score == Settings.winning_score:
|
||||
winner_triggered.emit()
|
||||
elif has_winning_score && player_score == Settings.winning_score - 1:
|
||||
match_point_triggered.emit()
|
||||
elif first_point:
|
||||
first_point_triggered.emit()
|
||||
else:
|
||||
point_given_sound.emit()
|
||||
|
||||
|
||||
func minus_point() -> void:
|
||||
if player_score > 0:
|
||||
player_score -= 1
|
||||
player_point_taken.emit()
|
||||
if player_score == 0:
|
||||
is_first_point = true
|
||||
|
||||
|
||||
func reset_points() -> void:
|
||||
player_score = 0
|
||||
is_first_point = true
|
||||
File renamed without changes.
@@ -0,0 +1,201 @@
|
||||
extends Node
|
||||
## The songs of the current quiz, in the order they were added, and which of them are
|
||||
## played, answered and playing.
|
||||
##
|
||||
## Registered as the [code]Playlist[/code] autoload.
|
||||
##
|
||||
## Downloaded songs are kept on disk in the [code]songs[/code] folder next to the
|
||||
## settings, not in memory. The folder only holds the current playlist: when the game
|
||||
## starts, songs the saved quiz does not use are deleted (see [QuizSave]), and the folder
|
||||
## is emptied when the playlist is reset.
|
||||
|
||||
const SONGS_FOLDER_NAME: String = "songs"
|
||||
|
||||
var song_object_array: Array[SongObject] = []
|
||||
var latest_played_song: int = 0
|
||||
var currently_playing_song: int = -1
|
||||
var first_song_downloaded: bool = false
|
||||
var first_song_played: bool = false
|
||||
var number_of_tracks_to_preload: int = 1
|
||||
var is_song_downloading: bool = false
|
||||
## The first song of the current game. Songs before it belong to earlier games, which
|
||||
## ended with Reset points.
|
||||
var game_start_song: int = 0
|
||||
|
||||
# How many song files have been written, so each gets a name of its own
|
||||
var _song_files_written: int = 0
|
||||
|
||||
|
||||
## The folder downloaded songs are kept in.
|
||||
static func songs_folder() -> String:
|
||||
return Settings.config_dir().path_join(SONGS_FOLDER_NAME)
|
||||
|
||||
|
||||
func get_next_song() -> SongObject:
|
||||
return song_object_array[latest_played_song]
|
||||
|
||||
|
||||
func set_latest_played_song() -> void:
|
||||
latest_played_song += 1
|
||||
if latest_played_song > song_object_array.size() - 1:
|
||||
latest_played_song = song_object_array.size() - 1
|
||||
|
||||
|
||||
func update_last_played_song() -> void:
|
||||
latest_played_song = song_object_array.size()
|
||||
|
||||
|
||||
func get_preloaded_songs() -> int:
|
||||
var number_of_tracks_preloaded: int = 0
|
||||
for song: SongObject in song_object_array:
|
||||
if !song.has_played:
|
||||
number_of_tracks_preloaded += 1
|
||||
return number_of_tracks_preloaded
|
||||
|
||||
|
||||
func should_preload_more_songs() -> bool:
|
||||
return number_of_tracks_to_preload > get_preloaded_songs()
|
||||
|
||||
|
||||
func song_is_answered(song_no: int = currently_playing_song) -> void:
|
||||
# Nothing is playing yet (-1); a negative index would answer the last, unplayed song
|
||||
if song_no < 0 || song_no >= song_object_array.size():
|
||||
return
|
||||
GameLog.debug("show_answer_pressed song_object: ", song_object_array[song_no]._to_string())
|
||||
song_object_array[song_no].is_answered = true
|
||||
if Settings.add_to_stats && !Settings.is_offline():
|
||||
GameLog.debug("add to stats: ", song_no)
|
||||
MusicServerClient.mark_song_played(song_no)
|
||||
|
||||
|
||||
func add_to_playlist(new_song_object: SongObject) -> void:
|
||||
song_object_array.append(new_song_object)
|
||||
|
||||
|
||||
func get_playlist() -> Array[SongObject]:
|
||||
if Settings.is_debug:
|
||||
return song_object_array
|
||||
else:
|
||||
var temp_list: Array[SongObject] = []
|
||||
for s: SongObject in song_object_array:
|
||||
if s.has_played:
|
||||
temp_list.append(s)
|
||||
return temp_list
|
||||
|
||||
|
||||
func reset_playlist() -> void:
|
||||
currently_playing_song = -1
|
||||
latest_played_song = 0
|
||||
game_start_song = 0
|
||||
clear_song_list()
|
||||
clear_song_files()
|
||||
|
||||
|
||||
func clear_song_list() -> void:
|
||||
song_object_array.clear()
|
||||
|
||||
|
||||
func unset_is_playing() -> void:
|
||||
for song_object: SongObject in song_object_array:
|
||||
song_object.is_playing = false
|
||||
|
||||
|
||||
func set_currently_playing_song(value: int) -> void:
|
||||
currently_playing_song = value
|
||||
|
||||
|
||||
func get_current_soundtrack_title() -> String:
|
||||
return song_object_array[currently_playing_song].get_soundtrack_title()
|
||||
|
||||
|
||||
func get_current_soundtrack_uuid() -> String:
|
||||
return song_object_array[currently_playing_song].get_soundtrack_uuid()
|
||||
|
||||
|
||||
func get_current_song_title() -> String:
|
||||
return song_object_array[currently_playing_song].get_song_title()
|
||||
|
||||
|
||||
func add_point(player_id: String) -> void:
|
||||
song_object_array[currently_playing_song].add_point(player_id)
|
||||
|
||||
|
||||
func remove_point(player_id: String) -> void:
|
||||
song_object_array[currently_playing_song].remove_point(player_id)
|
||||
|
||||
|
||||
## Removes every player's points from every song, for a new game.
|
||||
func clear_points() -> void:
|
||||
for song_object: SongObject in song_object_array:
|
||||
song_object.players_given_point.clear()
|
||||
|
||||
|
||||
## Removes a player's points from every song.
|
||||
func remove_player_from_list(player_id: String) -> void:
|
||||
for song_object: SongObject in song_object_array:
|
||||
song_object.remove_point(player_id)
|
||||
|
||||
|
||||
## The song of [param song_no], read from its file. Returns null if the file is gone, so
|
||||
## the song can be downloaded again.
|
||||
func get_song(song_no: int) -> AudioStream:
|
||||
var song_object: SongObject = song_object_array[song_no]
|
||||
if song_object.song_path.is_empty():
|
||||
return song_object.song
|
||||
return AudioFile.load(song_object.song_path)
|
||||
|
||||
|
||||
func add_song_to_song_object(song_no: int, mp3: PackedByteArray) -> void:
|
||||
store_song(song_object_array[song_no], mp3)
|
||||
|
||||
|
||||
## Keeps a downloaded song in a file in [method songs_folder]. If the file can't be
|
||||
## written, the song is kept in memory instead.
|
||||
func store_song(song_object: SongObject, mp3: PackedByteArray) -> void:
|
||||
DirAccess.make_dir_recursive_absolute(songs_folder())
|
||||
# Songs kept from a saved quiz already have files, which must not be written over
|
||||
var path: String = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
||||
while FileAccess.file_exists(path):
|
||||
_song_files_written += 1
|
||||
path = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
||||
_song_files_written += 1
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
GameLog.warning("Could not save a song to ", path, ", keeping it in memory: ",
|
||||
error_string(FileAccess.get_open_error()))
|
||||
var sound: AudioStreamMP3 = AudioStreamMP3.new()
|
||||
sound.data = mp3
|
||||
song_object.song = sound
|
||||
song_object.song_path = ""
|
||||
return
|
||||
file.store_buffer(mp3)
|
||||
file.close()
|
||||
song_object.song = null
|
||||
song_object.song_path = path
|
||||
|
||||
|
||||
## Deletes the files of the songs downloaded so far, except those in [param keep].
|
||||
## The downloaded song files the playlist uses, which must be kept while it does.
|
||||
func song_files_in_use() -> PackedStringArray:
|
||||
var files: PackedStringArray = []
|
||||
for song_object: SongObject in song_object_array:
|
||||
if !song_object.song_path.is_empty():
|
||||
files.append(song_object.song_path)
|
||||
return files
|
||||
|
||||
|
||||
func clear_song_files(keep: PackedStringArray = []) -> void:
|
||||
if !DirAccess.dir_exists_absolute(songs_folder()):
|
||||
return
|
||||
for file_name: String in DirAccess.get_files_at(songs_folder()):
|
||||
var path: String = songs_folder().path_join(file_name)
|
||||
if !keep.has(path):
|
||||
DirAccess.remove_absolute(path)
|
||||
|
||||
|
||||
func song_has_played(song_no: int = latest_played_song) -> void:
|
||||
song_object_array[song_no].has_played = true
|
||||
|
||||
|
||||
func song_is_playing(song_no: int = latest_played_song) -> void:
|
||||
song_object_array[song_no].is_playing = true
|
||||
File renamed without changes.
@@ -0,0 +1,174 @@
|
||||
class_name QuizSave
|
||||
extends RefCounted
|
||||
## The quiz going on, saved to a file after every change, so the game goes on where it
|
||||
## was when it is started again: the players with their points and characters, and every
|
||||
## song with who got a point for it and whether it was played, answered or is playing.
|
||||
##
|
||||
## Songs downloaded ahead keep their files, so they are not downloaded again. A new game
|
||||
## (Reset points, a sync or another server) is saved like any other change, so the file
|
||||
## always holds the quiz as it is now.
|
||||
##
|
||||
## Kept in [code]current_quiz.json[/code] next to the settings, or
|
||||
## [code]current_quiz_debug.json[/code] in debug mode, like the saved settings.
|
||||
|
||||
const FILE_NAME: String = "current_quiz.json"
|
||||
const DEBUG_FILE_NAME: String = "current_quiz_debug.json"
|
||||
## Files with another version are from a game that saved differently, and are ignored.
|
||||
const VERSION: int = 1
|
||||
|
||||
var players: Array[PlayerObject] = []
|
||||
var songs: Array[SongObject] = []
|
||||
var currently_playing_song: int = -1
|
||||
var latest_played_song: int = 0
|
||||
var game_start_song: int = 0
|
||||
var first_song_played: bool = false
|
||||
## Whether statistics were turned off because a player won, see [code]Quiz[/code].
|
||||
var stats_paused_by_win: bool = false
|
||||
## The server the songs came from. Empty when they came from a music folder.
|
||||
var server: String = ""
|
||||
## The music folder the songs came from, or empty when they came from the server.
|
||||
var music_folder_path: String = ""
|
||||
## How far a prepared quiz in [member music_folder_path] has come, see
|
||||
## [method MusicFolder.progress].
|
||||
var music_folder_progress: Dictionary = {}
|
||||
## Where the characters came from, see [method Settings.characters_dir].
|
||||
var characters_dir: String = ""
|
||||
|
||||
|
||||
## Where the quiz is saved.
|
||||
static func path() -> String:
|
||||
return Settings.config_dir().path_join(DEBUG_FILE_NAME if Settings.is_debug else FILE_NAME)
|
||||
|
||||
|
||||
## The quiz as it is now, in [code]Settings[/code] and [code]Playlist[/code].
|
||||
static func capture(paused_by_win: bool) -> QuizSave:
|
||||
var save: QuizSave = QuizSave.new()
|
||||
save.players = Settings.player_array.duplicate()
|
||||
save.songs = Playlist.song_object_array.duplicate()
|
||||
save.currently_playing_song = Playlist.currently_playing_song
|
||||
save.latest_played_song = Playlist.latest_played_song
|
||||
save.game_start_song = Playlist.game_start_song
|
||||
save.first_song_played = Playlist.first_song_played
|
||||
save.stats_paused_by_win = paused_by_win
|
||||
if Settings.is_offline():
|
||||
save.music_folder_path = Settings.music_folder.path
|
||||
save.music_folder_progress = Settings.music_folder.progress()
|
||||
else:
|
||||
save.server = Settings.default_path
|
||||
save.characters_dir = Settings.characters_dir()
|
||||
return save
|
||||
|
||||
|
||||
## Reads the saved quiz, or returns null when there is none or it can't be read.
|
||||
static func read() -> QuizSave:
|
||||
if !FileAccess.file_exists(path()):
|
||||
return null
|
||||
var save: QuizSave = from_dict(JSON.parse_string(FileAccess.get_file_as_string(path())))
|
||||
if save == null:
|
||||
GameLog.warning("Could not read the saved quiz in ", path(), ", starting a new one")
|
||||
return save
|
||||
|
||||
|
||||
## Deletes the saved quiz.
|
||||
static func delete() -> void:
|
||||
if FileAccess.file_exists(path()):
|
||||
DirAccess.remove_absolute(path())
|
||||
|
||||
|
||||
## Saves the quiz. It is written to another file first and then moved, so a crash while
|
||||
## saving never leaves half a file. Returns false when it could not be saved.
|
||||
func write() -> bool:
|
||||
var target: String = path()
|
||||
var temporary: String = target + ".tmp"
|
||||
DirAccess.make_dir_recursive_absolute(target.get_base_dir())
|
||||
var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
|
||||
if file == null:
|
||||
GameLog.warning("Could not save the quiz to ", temporary, ": ",
|
||||
error_string(FileAccess.get_open_error()))
|
||||
return false
|
||||
file.store_string(JSON.stringify(to_dict(), "\t"))
|
||||
file.close()
|
||||
var error: Error = DirAccess.rename_absolute(temporary, target)
|
||||
if error != OK:
|
||||
GameLog.warning("Could not save the quiz to ", target, ": ", error_string(error))
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## The song files the saved songs use, which must not be deleted.
|
||||
func song_files() -> PackedStringArray:
|
||||
var files: PackedStringArray = []
|
||||
for song_object: SongObject in songs:
|
||||
if !song_object.song_path.is_empty():
|
||||
files.append(song_object.song_path)
|
||||
return files
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var player_list: Array[Dictionary] = []
|
||||
for player: PlayerObject in players:
|
||||
player_list.append(player.to_dict())
|
||||
var song_list: Array[Dictionary] = []
|
||||
for song_object: SongObject in songs:
|
||||
song_list.append(song_object.to_dict())
|
||||
return {
|
||||
"version": VERSION,
|
||||
"players": player_list,
|
||||
"songs": song_list,
|
||||
"currently_playing_song": currently_playing_song,
|
||||
"latest_played_song": latest_played_song,
|
||||
"game_start_song": game_start_song,
|
||||
"first_song_played": first_song_played,
|
||||
"stats_paused_by_win": stats_paused_by_win,
|
||||
"server": server,
|
||||
"music_folder": {"path": music_folder_path, "progress": music_folder_progress},
|
||||
"characters_dir": characters_dir,
|
||||
}
|
||||
|
||||
|
||||
## A save made from [param data], as given by [method to_dict], or null when
|
||||
## [param data] is not a saved quiz of this [constant VERSION]. Players and songs that
|
||||
## can't be read are left out.
|
||||
static func from_dict(data: Variant) -> QuizSave:
|
||||
if !(data is Dictionary) || int(data.get("version", 0)) != VERSION \
|
||||
|| !(data.get("players") is Array) || !(data.get("songs") is Array):
|
||||
return null
|
||||
var save: QuizSave = QuizSave.new()
|
||||
for player_data: Variant in data.players:
|
||||
var player: PlayerObject = PlayerObject.from_dict(player_data)
|
||||
if player != null && save.players.size() < Settings.MAX_NUMBER_OF_PLAYERS:
|
||||
save.players.append(player)
|
||||
for song_data: Variant in data.songs:
|
||||
var song_object: SongObject = SongObject.from_dict(song_data)
|
||||
if song_object == null:
|
||||
# The songs' places in the list are their numbers, so none can be left out
|
||||
save.songs.clear()
|
||||
break
|
||||
save.songs.append(song_object)
|
||||
save.currently_playing_song = clampi(int(data.get("currently_playing_song", -1)), -1,
|
||||
save.songs.size() - 1)
|
||||
save.latest_played_song = clampi(int(data.get("latest_played_song", 0)), 0,
|
||||
maxi(save.songs.size() - 1, 0))
|
||||
save.game_start_song = clampi(int(data.get("game_start_song", 0)), 0, save.songs.size())
|
||||
save.first_song_played = bool(data.get("first_song_played", false))
|
||||
save.stats_paused_by_win = bool(data.get("stats_paused_by_win", false))
|
||||
save.server = str(data.get("server", ""))
|
||||
var folder: Variant = data.get("music_folder", {})
|
||||
if folder is Dictionary:
|
||||
save.music_folder_path = str(folder.get("path", ""))
|
||||
if folder.get("progress") is Dictionary:
|
||||
save.music_folder_progress = folder.progress
|
||||
save.characters_dir = str(data.get("characters_dir", ""))
|
||||
return save
|
||||
|
||||
|
||||
## Forgets the songs, e.g. when the server's playlist no longer has them. The players are
|
||||
## kept, without points.
|
||||
func drop_songs() -> void:
|
||||
songs.clear()
|
||||
currently_playing_song = -1
|
||||
latest_played_song = 0
|
||||
game_start_song = 0
|
||||
first_song_played = false
|
||||
for player: PlayerObject in players:
|
||||
player.reset_points()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfabuhf854ukp
|
||||
@@ -0,0 +1,121 @@
|
||||
class_name ScoreHistory
|
||||
extends RefCounted
|
||||
## Who got a point on which song of the current game, for the graph and the summary on
|
||||
## the Winner popup.
|
||||
##
|
||||
## Songs are counted from 1 at the start of the game, which is the start of the playlist
|
||||
## or the first song after the last Reset points.
|
||||
|
||||
## The players, in the order of [code]Settings.player_array[/code].
|
||||
var players: Array[PlayerObject] = []
|
||||
## For each song of the game, in the order they were played, the ids of the players who
|
||||
## got a point on it.
|
||||
var songs: Array[Array] = []
|
||||
|
||||
|
||||
## The current game, from [code]Playlist.game_start_song[/code] up to the last song that
|
||||
## has played. Songs downloaded ahead but not played yet are left out, and so are points
|
||||
## of players who are no longer in [param game_players].
|
||||
static func from_playlist(game_players: Array[PlayerObject]) -> ScoreHistory:
|
||||
var song_scorers: Array[Array] = []
|
||||
# Not just the song playing now: an older song may have been picked in the music list
|
||||
var last_song: int = mini(Playlist.currently_playing_song,
|
||||
Playlist.song_object_array.size() - 1)
|
||||
for song_no: int in Playlist.song_object_array.size():
|
||||
if Playlist.song_object_array[song_no].has_played:
|
||||
last_song = maxi(last_song, song_no)
|
||||
for song_no: int in range(Playlist.game_start_song, last_song + 1):
|
||||
var song: SongObject = Playlist.song_object_array[song_no]
|
||||
if song.has_played || song_no == Playlist.currently_playing_song:
|
||||
song_scorers.append(song.players_given_point)
|
||||
return from_songs(game_players, song_scorers)
|
||||
|
||||
|
||||
## A history of [param song_scorers], the ids of the players who scored on each song.
|
||||
static func from_songs(game_players: Array[PlayerObject], song_scorers: Array) -> ScoreHistory:
|
||||
var history: ScoreHistory = ScoreHistory.new()
|
||||
history.players = game_players.duplicate()
|
||||
var ids: Array[String] = []
|
||||
for player: PlayerObject in game_players:
|
||||
ids.append(player.id)
|
||||
for scorers: Array in song_scorers:
|
||||
history.songs.append(scorers.filter(func(id: String) -> bool: return ids.has(id)))
|
||||
return history
|
||||
|
||||
|
||||
## [param player_id]'s score before the first song and after each song.
|
||||
func scores_after_songs(player_id: String) -> Array[int]:
|
||||
var scores: Array[int] = [0]
|
||||
for scorers: Array in songs:
|
||||
scores.append(scores.back() + (1 if scorers.has(player_id) else 0))
|
||||
return scores
|
||||
|
||||
|
||||
## [param player_id]'s score after the last song.
|
||||
func final_score(player_id: String) -> int:
|
||||
return scores_after_songs(player_id).back()
|
||||
|
||||
|
||||
## The player with the most songs in a row with a point, e.g.
|
||||
## "Crono, 5 songs in a row (14–18)". The earliest streak wins a tie.
|
||||
func longest_streak_text() -> String:
|
||||
if songs.is_empty():
|
||||
return "No songs played"
|
||||
var best_length: int = 0
|
||||
var best_start: int = 0
|
||||
var best_player: PlayerObject = null
|
||||
for player: PlayerObject in players:
|
||||
var length: int = 0
|
||||
for song: int in songs.size():
|
||||
length = length + 1 if songs[song].has(player.id) else 0
|
||||
var start: int = song - length + 1
|
||||
if length > best_length || (length == best_length && length > 0 \
|
||||
&& start < best_start):
|
||||
best_length = length
|
||||
best_start = start
|
||||
best_player = player
|
||||
if best_length < 2:
|
||||
return "Nobody scored twice in a row"
|
||||
return "%s, %d songs in a row (%d–%d)" % [best_player.player_name, best_length,
|
||||
best_start + 1, best_start + best_length]
|
||||
|
||||
|
||||
## The most songs in a row where nobody scored, e.g. "Songs 6–10, nobody scored".
|
||||
func longest_standstill_text() -> String:
|
||||
if songs.is_empty():
|
||||
return "No songs played"
|
||||
var best_length: int = 0
|
||||
var best_start: int = 0
|
||||
var length: int = 0
|
||||
for song: int in songs.size():
|
||||
length = length + 1 if songs[song].is_empty() else 0
|
||||
if length > best_length:
|
||||
best_length = length
|
||||
best_start = song - length + 1
|
||||
if best_length == 0:
|
||||
return "Someone scored on every song"
|
||||
if best_length == 1:
|
||||
return "Song %d, nobody scored" % (best_start + 1)
|
||||
return "Songs %d–%d, nobody scored" % [best_start + 1, best_start + best_length]
|
||||
|
||||
|
||||
## The song most players scored on, e.g. "Song 11, Marle and Lucca", or
|
||||
## "Song 11, 3 players" when more than two did. The earliest song wins a tie.
|
||||
func most_shared_text() -> String:
|
||||
if songs.is_empty():
|
||||
return "No songs played"
|
||||
var best_song: int = -1
|
||||
for song: int in songs.size():
|
||||
if songs[song].size() >= 2 \
|
||||
&& (best_song < 0 || songs[song].size() > songs[best_song].size()):
|
||||
best_song = song
|
||||
if best_song < 0:
|
||||
return "No song had two scorers"
|
||||
var scorers: Array = songs[best_song]
|
||||
if scorers.size() > 2:
|
||||
return "Song %d, %d players" % [best_song + 1, scorers.size()]
|
||||
var names: PackedStringArray = []
|
||||
for player: PlayerObject in players:
|
||||
if scorers.has(player.id):
|
||||
names.append(player.player_name)
|
||||
return "Song %d, %s" % [best_song + 1, " and ".join(names)]
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcvwgwhlu80qh
|
||||
@@ -0,0 +1,299 @@
|
||||
extends Node
|
||||
## The game settings and the players, shared by the whole game.
|
||||
##
|
||||
## Registered as the [code]Settings[/code] autoload.
|
||||
|
||||
## Emitted when another theme is used, for code that reads theme colours itself.
|
||||
signal theme_changed
|
||||
|
||||
## How the server picks a random song: any soundtrack then any of its songs, preferring
|
||||
## soundtracks played less than average, or every song with the same chance.
|
||||
enum RandomMode { NORMAL, LOW_PLAYED, CLASSIC }
|
||||
|
||||
const MAX_NUMBER_OF_PLAYERS: int = 7
|
||||
## How many letters a search needs before it also finds names with the letters in order
|
||||
## but apart, e.g. "lttp" for "A Link to the Past". Shorter searches only find names
|
||||
## that contain the text, since a letter or two in order matches most of the library.
|
||||
const LETTERS_FOR_LOOSE_SEARCH: int = 3
|
||||
## The MusicServers that can always be picked in the settings.
|
||||
const SERVERS: Array[String] = [
|
||||
"https://music.sanplex.xyz", "https://tmusic.sanplex.xyz", "https://rmusic.sanplex.xyz",
|
||||
"https://old-music.sanplex.xyz", "http://localhost:8080",
|
||||
]
|
||||
## The server debug mode uses.
|
||||
const DEBUG_SERVER: String = "http://localhost:8080"
|
||||
## The winning score debug mode starts with, so a game is quick to win while testing.
|
||||
const DEBUG_WINNING_SCORE: int = 5
|
||||
## The settings the game saves by itself, by section, in [method saved_settings_path].
|
||||
## Count plays in statistics and Prefer less played soundtracks are left out on purpose.
|
||||
const SAVED_SETTINGS: Dictionary[String, Array] = {
|
||||
"quiz": [
|
||||
&"auto_repeat_song", &"stop_after_current", &"hide_next_track", &"hide_beginning",
|
||||
&"hide_for_seconds", &"hide_length", &"hide_ticks", &"winning_score",
|
||||
&"inspiration_list_speed", &"short_songs", &"short_song_seconds",
|
||||
],
|
||||
"window": [&"fullscreen"],
|
||||
"app": [&"last_seen_version", &"theme_id", &"reduce_animations"],
|
||||
}
|
||||
## The looks the game can have, picked in Settings: an id that is saved, the name shown
|
||||
## and the theme file. The first is the default. See themes/README.md.
|
||||
const THEMES: Array[Dictionary] = [
|
||||
{"id": "classic", "name": "Classic", "path": "res://themes/main_theme.tres"},
|
||||
{"id": "original_web", "name": "Original web", "path": "res://themes/original_web_theme.tres"},
|
||||
{"id": "nord", "name": "Nord", "path": "res://themes/nord_theme.tres"},
|
||||
{"id": "christmas", "name": "Christmas", "path": "res://themes/christmas_theme.tres"},
|
||||
]
|
||||
## When this environment variable is set, the settings files are kept in that folder
|
||||
## instead of the user's config folder. The tests set it so they never touch the real files.
|
||||
const CONFIG_DIR_ENV: String = "MUSICPLAYER_CONFIG_DIR"
|
||||
## The file the user edits by hand. The game writes it only when it is missing, with every
|
||||
## setting commented out.
|
||||
const CONFIG_FILE_NAME: String = "config.ini"
|
||||
const CONFIG_TEMPLATE: String = """# MusicPlayer config. The game never changes this file, so edit it as you like.
|
||||
# Lines starting with # or ; are comments. Remove the # in front of a setting to use it,
|
||||
# and put it back to go back to the default.
|
||||
|
||||
[server]
|
||||
# The MusicServer to use when the game starts. Another one can still be picked in
|
||||
# Settings for as long as the game runs. Debug mode uses http://localhost:8080.
|
||||
#url = https://music.sanplex.xyz
|
||||
|
||||
# Password for getting an API token from the server.
|
||||
#password =
|
||||
|
||||
[game]
|
||||
# true or false. Debug mode uses the local server, starts with a winning score of 5 and
|
||||
# shows DEBUG in the top bar. Without this setting, the game is in debug mode when run
|
||||
# from the Godot editor and not when exported.
|
||||
#debug = true
|
||||
"""
|
||||
|
||||
var default_path: String = "https://music.sanplex.xyz"
|
||||
## Password for getting an API token from the server. Any password works when the
|
||||
## server has none set.
|
||||
var api_password: String = ""
|
||||
## The servers that can be picked in Settings: [constant SERVERS], plus the server from
|
||||
## the config file when it is not one of them.
|
||||
var servers: Array[String] = SERVERS.duplicate()
|
||||
var selected_server: int = 0
|
||||
var is_debug: bool = false
|
||||
var stop_after_current: bool = true
|
||||
var auto_repeat_song: bool = true
|
||||
var hide_next_track: bool = true
|
||||
var add_to_stats: bool = true
|
||||
## How the server picks random songs. Not saved, like statistics counting.
|
||||
var random_mode: RandomMode = RandomMode.NORMAL
|
||||
var winning_score: int = 20
|
||||
var fullscreen: bool = false
|
||||
var inspiration_list_speed: int = 1
|
||||
var hide_for_seconds: float = 5.0
|
||||
var hide_beginning: bool = false
|
||||
var hide_length: bool = false
|
||||
var hide_ticks: bool = false
|
||||
## Plays only [member short_song_seconds] of each song, from a random point, so more
|
||||
## songs fit in a game.
|
||||
var short_songs: bool = false
|
||||
var short_song_seconds: int = 30
|
||||
var player_array: Array[PlayerObject]
|
||||
var edit_players: bool = false
|
||||
var currently_syncing: bool = false
|
||||
var character_select_open: bool = false
|
||||
## The ID of the game's play queue on each server, by server URL, set by
|
||||
## [code]MusicServerClient[/code]. Saved in the [code][queues][/code] section of
|
||||
## [method saved_settings_path], so the game goes on with the same queue after a restart.
|
||||
var queue_ids: Dictionary[String, String] = {}
|
||||
## The version the game last showed the About popup for. When it is not [member version],
|
||||
## this version is new and the main window opens the About popup to show what is new.
|
||||
var last_seen_version: String = ""
|
||||
## Turns every animation off, so nothing moves. See [Anim].
|
||||
var reduce_animations: bool = false
|
||||
## The id of the theme in use, one of [constant THEMES].
|
||||
var theme_id: String = THEMES[0].id
|
||||
## The folder songs are played from instead of the server, picked in the settings, or null
|
||||
## to use the server. Kept for this session only.
|
||||
var music_folder: MusicFolder = null
|
||||
## The folder characters are read from instead of the server, picked in the settings, or
|
||||
## empty. Kept for this session only. See [method characters_dir].
|
||||
var character_folder: String = ""
|
||||
|
||||
## The game version, set in Project Settings > Application > Config > Version.
|
||||
## Releases are made from it, see RELEASING.md.
|
||||
var version: String = ProjectSettings.get_setting("application/config/version", "")
|
||||
|
||||
|
||||
# Autoloads are ready before any scene, so the server and debug mode from the config file
|
||||
# are used from the very first request. The saved settings come after, so the debug
|
||||
# winning score is only used until another one is saved.
|
||||
func _ready() -> void:
|
||||
apply_config(load_config())
|
||||
load_saved_settings()
|
||||
_use_theme(theme_id)
|
||||
if fullscreen:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||
|
||||
|
||||
## Uses the server, password and debug mode from the hand-edited [param config]. Settings
|
||||
## left out of it get their defaults.
|
||||
func apply_config(config: IniFile) -> void:
|
||||
is_debug = config.get_bool("game", "debug", OS.has_feature("debug"))
|
||||
default_path = DEBUG_SERVER if is_debug else SERVERS[0]
|
||||
if is_debug:
|
||||
winning_score = DEBUG_WINNING_SCORE
|
||||
var url: String = config.get_string("server", "url")
|
||||
if !url.is_empty():
|
||||
default_path = url.trim_suffix("/")
|
||||
if !servers.has(default_path):
|
||||
servers.append(default_path)
|
||||
selected_server = servers.find(default_path)
|
||||
api_password = config.get_string("server", "password")
|
||||
|
||||
|
||||
## The folder with the settings files: a MusicPlayer folder in the user's config folder,
|
||||
## e.g. [code]~/.config[/code] on Linux and [code]%APPDATA%[/code] on Windows.
|
||||
func config_dir() -> String:
|
||||
var dir: String = OS.get_environment(CONFIG_DIR_ENV)
|
||||
if dir.is_empty():
|
||||
dir = OS.get_config_dir().path_join("MusicPlayer")
|
||||
return dir
|
||||
|
||||
|
||||
## Where the hand-edited config file is.
|
||||
func config_path() -> String:
|
||||
return config_dir().path_join(CONFIG_FILE_NAME)
|
||||
|
||||
|
||||
## Where the game saves its settings. Debug mode uses a file of its own, so testing never
|
||||
## changes the real settings.
|
||||
func saved_settings_path() -> String:
|
||||
var file_name: String = "settings_debug.cfg" if is_debug else "settings.cfg"
|
||||
return config_dir().path_join(file_name)
|
||||
|
||||
|
||||
## Reads the hand-edited config file. When it is missing, writes one with every setting
|
||||
## commented out, so there is a file to edit.
|
||||
func load_config() -> IniFile:
|
||||
var path: String = config_path()
|
||||
if !FileAccess.file_exists(path):
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var template: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
if template != null:
|
||||
template.store_string(CONFIG_TEMPLATE)
|
||||
return IniFile.load_file(path)
|
||||
|
||||
|
||||
## Loads the settings the game saved. Settings missing from the file, or saved with the
|
||||
## wrong type, keep their current value.
|
||||
func load_saved_settings() -> void:
|
||||
var file: ConfigFile = ConfigFile.new()
|
||||
if file.load(saved_settings_path()) != OK:
|
||||
return
|
||||
for section: String in SAVED_SETTINGS:
|
||||
for setting: StringName in SAVED_SETTINGS[section]:
|
||||
set(setting, _saved_value(file, section, setting, get(setting)))
|
||||
if file.has_section("queues"):
|
||||
queue_ids.clear()
|
||||
for server: String in file.get_section_keys("queues"):
|
||||
var id: Variant = file.get_value("queues", server)
|
||||
if id is String:
|
||||
queue_ids[server] = id
|
||||
|
||||
|
||||
## Saves the settings in [constant SAVED_SETTINGS], so they are the same the next time
|
||||
## the game starts.
|
||||
func save_settings() -> void:
|
||||
var path: String = saved_settings_path()
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var file: ConfigFile = ConfigFile.new()
|
||||
for section: String in SAVED_SETTINGS:
|
||||
for setting: StringName in SAVED_SETTINGS[section]:
|
||||
file.set_value(section, setting, get(setting))
|
||||
for server: String in queue_ids:
|
||||
file.set_value("queues", server, queue_ids[server])
|
||||
var error: Error = file.save(path)
|
||||
if error != OK:
|
||||
GameLog.warning("Could not save the settings to %s: %s" % [path, error_string(error)])
|
||||
|
||||
|
||||
## Sets the play queue ID for [param server] and saves it. Only the ID is changed in
|
||||
## the file; the other settings in it stay as they were saved.
|
||||
func set_queue_id(server: String, id: String) -> void:
|
||||
queue_ids[server] = id
|
||||
var path: String = saved_settings_path()
|
||||
var file: ConfigFile = ConfigFile.new()
|
||||
# A missing file is fine: it is written with only the queue ID
|
||||
file.load(path)
|
||||
file.set_value("queues", server, id)
|
||||
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
var error: Error = file.save(path)
|
||||
if error != OK:
|
||||
GameLog.warning("Could not save the queue ID to %s: %s" % [path, error_string(error)])
|
||||
|
||||
|
||||
## Turns fullscreen on or off and saves it.
|
||||
func set_fullscreen(on: bool) -> void:
|
||||
fullscreen = on
|
||||
if on:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||
else:
|
||||
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||
save_settings()
|
||||
|
||||
|
||||
# The saved value of [param key], or [param default] when it is missing or of another type
|
||||
func _saved_value(file: ConfigFile, section: String, key: String, default: Variant) -> Variant:
|
||||
var value: Variant = file.get_value(section, key, default)
|
||||
if typeof(value) != typeof(default):
|
||||
push_warning("%s in [%s] should be a %s, not %s" % [key, section,
|
||||
type_string(typeof(default)), var_to_str(value)])
|
||||
return default
|
||||
return value
|
||||
|
||||
|
||||
## Whether songs come from a music folder instead of the server. Nothing but the
|
||||
## characters is asked from the server then.
|
||||
func is_offline() -> bool:
|
||||
return music_folder != null
|
||||
|
||||
|
||||
## Whether a prepared quiz is played from a music folder. It has no winning score: the
|
||||
## game ends when every song has played.
|
||||
func is_prepared_quiz() -> bool:
|
||||
return music_folder != null && music_folder.kind == MusicFolder.Kind.PREPARED_QUIZ
|
||||
|
||||
|
||||
## The folder the characters come from, or empty for the server's: the folder picked in
|
||||
## the settings, otherwise the [code]characters[/code] folder of the music folder played.
|
||||
func characters_dir() -> String:
|
||||
if !character_folder.is_empty():
|
||||
return character_folder
|
||||
if music_folder != null:
|
||||
return music_folder.characters_path
|
||||
return ""
|
||||
|
||||
|
||||
## Switches the whole game to the theme [param id] from [constant THEMES] and saves the
|
||||
## choice. Controls without a theme of their own take it from the root window.
|
||||
func apply_theme(id: String) -> void:
|
||||
_use_theme(id)
|
||||
save_settings()
|
||||
|
||||
|
||||
## The theme in [constant THEMES] with [param id], or the default one if there is none.
|
||||
func theme_entry(id: String) -> Dictionary:
|
||||
for entry: Dictionary in THEMES:
|
||||
if entry.id == id:
|
||||
return entry
|
||||
return THEMES[0]
|
||||
|
||||
|
||||
func _use_theme(id: String) -> void:
|
||||
var entry: Dictionary = theme_entry(id)
|
||||
theme_id = entry.id
|
||||
get_tree().root.theme = load(entry.path)
|
||||
theme_changed.emit()
|
||||
|
||||
|
||||
func delete_children(node: Node) -> void:
|
||||
for n: Node in node.get_children():
|
||||
node.remove_child(n)
|
||||
n.queue_free()
|
||||
File renamed without changes.
@@ -0,0 +1,109 @@
|
||||
extends RefCounted
|
||||
|
||||
class_name SongObject
|
||||
|
||||
var song_number: int
|
||||
var song_title: String
|
||||
var soundtrack_title: String
|
||||
## The UUID of the song's soundtrack on the server, empty for songs from a music folder.
|
||||
var soundtrack_uuid: String = ""
|
||||
|
||||
## Ids of the players who got a point for this song.
|
||||
var players_given_point: Array[String] = []
|
||||
|
||||
var is_answered: bool = false
|
||||
var has_played: bool = false
|
||||
var is_playing: bool = false
|
||||
## The song's file: a download in [method Playlist.songs_folder], or a song in the music
|
||||
## folder. Read the song with [method Playlist.get_song].
|
||||
var song_path: String = ""
|
||||
## The song itself, only when it could not be saved to a file.
|
||||
var song: AudioStream
|
||||
## Where the part played with Short songs starts, in seconds, or -1 before it is picked.
|
||||
## Picked once, so restarting and repeating play the same part.
|
||||
var short_start: float = -1.0
|
||||
|
||||
|
||||
func get_soundtrack_title() -> String:
|
||||
if is_answered:
|
||||
return soundtrack_title
|
||||
else:
|
||||
return "????????"
|
||||
|
||||
|
||||
func get_song_title() -> String:
|
||||
if is_answered:
|
||||
return song_title
|
||||
else:
|
||||
return "??????"
|
||||
|
||||
|
||||
## The soundtrack's UUID once the answer is shown, otherwise empty.
|
||||
func get_soundtrack_uuid() -> String:
|
||||
return soundtrack_uuid if is_answered else ""
|
||||
|
||||
|
||||
func get_song_info() -> String:
|
||||
if Settings.is_debug:
|
||||
return "%d. %s | played: %s | answered: %s" % [(song_number + 1), _full_title(),
|
||||
has_played, is_answered]
|
||||
elif is_answered:
|
||||
return "%d. %s" % [(song_number + 1), _full_title()]
|
||||
else:
|
||||
return "??? - ???"
|
||||
|
||||
|
||||
# "Soundtrack - Song", or only the song in a prepared quiz, which has no soundtracks
|
||||
func _full_title() -> String:
|
||||
if soundtrack_title.is_empty():
|
||||
return song_title
|
||||
return "%s - %s" % [soundtrack_title, song_title]
|
||||
|
||||
|
||||
func add_point(player_id: String) -> void:
|
||||
GameLog.debug("add_point")
|
||||
if !players_given_point.has(player_id):
|
||||
players_given_point.append(player_id)
|
||||
|
||||
|
||||
func remove_point(player_id: String) -> void:
|
||||
GameLog.debug("remove_point")
|
||||
players_given_point.erase(player_id)
|
||||
|
||||
|
||||
## The song as a Dictionary that can be written as JSON, for [QuizSave]. A song kept only
|
||||
## in memory is saved without its file.
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"number": song_number, "soundtrack": soundtrack_title, "song": song_title,
|
||||
"points": players_given_point.duplicate(), "answered": is_answered,
|
||||
"played": has_played, "playing": is_playing, "path": song_path,
|
||||
"soundtrack_uuid": soundtrack_uuid,
|
||||
}
|
||||
|
||||
|
||||
## A song made from [param data], as given by [method to_dict], or null when [param data]
|
||||
## is not a song.
|
||||
static func from_dict(data: Variant) -> SongObject:
|
||||
if !(data is Dictionary) || !(data.get("number") is float || data.get("number") is int):
|
||||
return null
|
||||
var song_object: SongObject = SongObject.new()
|
||||
song_object.song_number = int(data.number)
|
||||
song_object.soundtrack_title = str(data.get("soundtrack", ""))
|
||||
song_object.song_title = str(data.get("song", ""))
|
||||
song_object.soundtrack_uuid = str(data.get("soundtrack_uuid", ""))
|
||||
if data.get("points") is Array:
|
||||
for player_id: Variant in data.points:
|
||||
song_object.add_point(str(player_id))
|
||||
song_object.is_answered = bool(data.get("answered", false))
|
||||
song_object.has_played = bool(data.get("played", false))
|
||||
song_object.is_playing = bool(data.get("playing", false))
|
||||
song_object.song_path = str(data.get("path", ""))
|
||||
return song_object
|
||||
|
||||
|
||||
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"
|
||||
return format_string % [song_number, soundtrack_title, song_title, is_answered, has_played,
|
||||
is_playing]
|
||||
File renamed without changes.
@@ -0,0 +1,119 @@
|
||||
class_name UploadFolder
|
||||
extends RefCounted
|
||||
## A folder picked or dropped to upload as a new soundtrack: its MP3s, ready to send, and
|
||||
## the other song files, which are left out because the server only takes MP3.
|
||||
##
|
||||
## The server wants one folder of songs, so songs in subfolders, such as disc folders,
|
||||
## are taken too, with the disc's number in front of their names: "Disc 1/01 Title.mp3"
|
||||
## is sent as "1-01 Title.mp3". With only one disc folder, its songs keep their names.
|
||||
## Hidden files and folders are left out, and names get rid of the characters the server
|
||||
## refuses.
|
||||
|
||||
## The only kind of song the server takes.
|
||||
const UPLOAD_EXTENSION: String = "mp3"
|
||||
## Other song files, listed as left out so the user knows.
|
||||
const OTHER_SONG_EXTENSIONS: Array[String] = [
|
||||
"ogg", "wav", "flac", "m4a", "aac", "opus", "wma", "ape", "aif", "aiff",
|
||||
]
|
||||
## Characters the server refuses in soundtrack and file names.
|
||||
const INVALID_NAME_CHARACTERS: String = "/\\:*?\"<>|"
|
||||
|
||||
## The folder.
|
||||
var path: String = ""
|
||||
## The soundtrack's name to suggest: the folder's name.
|
||||
var suggested_name: String = ""
|
||||
## The songs to upload, in order: [code]{"path": String, "name": String, "size": int}[/code],
|
||||
## with the name to send each one as.
|
||||
var songs: Array[Dictionary] = []
|
||||
## The song files left out, as paths in the folder, e.g. [code]"Disc 2/03 Song.flac"[/code].
|
||||
var skipped: Array[String] = []
|
||||
## The size of the songs to upload, in bytes.
|
||||
var total_size: int = 0
|
||||
|
||||
|
||||
## Reads the folder at [param folder].
|
||||
static func scan(folder: String) -> UploadFolder:
|
||||
var result: UploadFolder = UploadFolder.new()
|
||||
result.path = folder.trim_suffix("/").trim_suffix("\\")
|
||||
result.suggested_name = clean_name(result.path.get_file())
|
||||
result._add_files(result.path, "", "")
|
||||
var discs: Array[String] = []
|
||||
for disc: String in _visible(DirAccess.get_directories_at(result.path)):
|
||||
if _has_songs(result.path.path_join(disc)):
|
||||
discs.append(disc)
|
||||
for disc: int in discs.size():
|
||||
result._add_folder(result.path.path_join(discs[disc]), discs[disc],
|
||||
"%d-" % (disc + 1) if discs.size() > 1 else "")
|
||||
return result
|
||||
|
||||
|
||||
## [param name] without the characters the server refuses, a leading dot, or a trailing
|
||||
## dot or space.
|
||||
static func clean_name(name: String) -> String:
|
||||
var cleaned: String = ""
|
||||
for character: String in name:
|
||||
var control: bool = character.unicode_at(0) < 32 || character.unicode_at(0) == 127
|
||||
cleaned += "-" if INVALID_NAME_CHARACTERS.contains(character) || control \
|
||||
else character
|
||||
cleaned = cleaned.strip_edges().lstrip(".")
|
||||
while cleaned.ends_with(".") || cleaned.ends_with(" "):
|
||||
cleaned = cleaned.left(-1)
|
||||
return cleaned
|
||||
|
||||
|
||||
## Whether there is anything to upload.
|
||||
func has_songs() -> bool:
|
||||
return !songs.is_empty()
|
||||
|
||||
|
||||
# Adds the files of [param folder] and of the folders in it, all with [param prefix]
|
||||
func _add_folder(folder: String, relative: String, prefix: String) -> void:
|
||||
_add_files(folder, relative, prefix)
|
||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
||||
_add_folder(folder.path_join(inner), relative.path_join(inner), prefix)
|
||||
|
||||
|
||||
func _add_files(folder: String, relative: String, prefix: String) -> void:
|
||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
||||
var extension: String = file_name.get_extension().to_lower()
|
||||
if extension == UPLOAD_EXTENSION:
|
||||
var file_path: String = folder.path_join(file_name)
|
||||
var size: int = FileAccess.get_size(file_path)
|
||||
songs.append({"path": file_path, "name": _free_name(prefix + clean_name(file_name)),
|
||||
"size": size})
|
||||
total_size += size
|
||||
elif extension in OTHER_SONG_EXTENSIONS:
|
||||
skipped.append(relative.path_join(file_name) if !relative.is_empty() else file_name)
|
||||
|
||||
|
||||
# [param name], or with " (2)" and so on before its extension when a song already has it
|
||||
func _free_name(name: String) -> String:
|
||||
var taken: Array = songs.map(func(song: Dictionary) -> String: return song.name.to_lower())
|
||||
var candidate: String = name
|
||||
var number: int = 2
|
||||
while taken.has(candidate.to_lower()):
|
||||
candidate = "%s (%d).%s" % [name.get_basename(), number, name.get_extension()]
|
||||
number += 1
|
||||
return candidate
|
||||
|
||||
|
||||
# Whether [param folder] or a folder in it has a song, to upload or to list as left out
|
||||
static func _has_songs(folder: String) -> bool:
|
||||
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
||||
var extension: String = file_name.get_extension().to_lower()
|
||||
if extension == UPLOAD_EXTENSION || extension in OTHER_SONG_EXTENSIONS:
|
||||
return true
|
||||
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
||||
if _has_songs(folder.path_join(inner)):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# The names that are not hidden, in natural order, so "2" comes before "10"
|
||||
static func _visible(names: PackedStringArray) -> Array[String]:
|
||||
var shown: Array[String] = []
|
||||
for name: String in names:
|
||||
if !name.begins_with("."):
|
||||
shown.append(name)
|
||||
shown.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
||||
return shown
|
||||
@@ -0,0 +1 @@
|
||||
uid://3amk1cte2idb
|
||||
@@ -1,6 +0,0 @@
|
||||
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://8bllart6xn1f"]
|
||||
|
||||
[ext_resource type="StyleBox" uid="uid://bsv6yb0yir04l" path="res://style_box_flat.tres" id="1_uierc"]
|
||||
|
||||
[resource]
|
||||
PanelContainer/styles/panel = ExtResource("1_uierc")
|
||||
+3
-263
@@ -75,266 +75,6 @@ debug/export_console_script=1
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="macOS"
|
||||
platform="macOS"
|
||||
runnable=true
|
||||
advanced_options=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="tests/*, addons/gdUnit4/*, linux/*"
|
||||
export_path="./MusicPlayer_0.9_Beta.dmg"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
export/distribution_type=1
|
||||
binary_format/architecture="universal"
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
application/icon=""
|
||||
application/icon_interpolation=4
|
||||
application/bundle_identifier="tech.sanplex.musicplayer"
|
||||
application/signature=""
|
||||
application/app_category="Music-games"
|
||||
application/short_version=""
|
||||
application/version=""
|
||||
application/copyright=""
|
||||
application/copyright_localized={}
|
||||
application/min_macos_version_x86_64="10.12"
|
||||
application/min_macos_version_arm64="11.00"
|
||||
application/export_angle=0
|
||||
display/high_res=true
|
||||
application/additional_plist_content=""
|
||||
xcode/platform_build="14C18"
|
||||
xcode/sdk_version="13.1"
|
||||
xcode/sdk_build="22C55"
|
||||
xcode/sdk_name="macosx13.1"
|
||||
xcode/xcode_version="1420"
|
||||
xcode/xcode_build="14C18"
|
||||
codesign/codesign=1
|
||||
codesign/installer_identity=""
|
||||
codesign/apple_team_id=""
|
||||
codesign/identity=""
|
||||
codesign/entitlements/custom_file=""
|
||||
codesign/entitlements/allow_jit_code_execution=false
|
||||
codesign/entitlements/allow_unsigned_executable_memory=false
|
||||
codesign/entitlements/allow_dyld_environment_variables=false
|
||||
codesign/entitlements/disable_library_validation=false
|
||||
codesign/entitlements/audio_input=false
|
||||
codesign/entitlements/camera=false
|
||||
codesign/entitlements/location=false
|
||||
codesign/entitlements/address_book=false
|
||||
codesign/entitlements/calendars=false
|
||||
codesign/entitlements/photos_library=false
|
||||
codesign/entitlements/apple_events=false
|
||||
codesign/entitlements/debugging=false
|
||||
codesign/entitlements/app_sandbox/enabled=false
|
||||
codesign/entitlements/app_sandbox/network_server=false
|
||||
codesign/entitlements/app_sandbox/network_client=false
|
||||
codesign/entitlements/app_sandbox/device_usb=false
|
||||
codesign/entitlements/app_sandbox/device_bluetooth=false
|
||||
codesign/entitlements/app_sandbox/files_downloads=0
|
||||
codesign/entitlements/app_sandbox/files_pictures=0
|
||||
codesign/entitlements/app_sandbox/files_music=0
|
||||
codesign/entitlements/app_sandbox/files_movies=0
|
||||
codesign/entitlements/app_sandbox/files_user_selected=0
|
||||
codesign/entitlements/app_sandbox/helper_executables=[]
|
||||
codesign/entitlements/additional=""
|
||||
codesign/custom_options=PackedStringArray()
|
||||
notarization/notarization=0
|
||||
privacy/microphone_usage_description=""
|
||||
privacy/microphone_usage_description_localized={}
|
||||
privacy/camera_usage_description=""
|
||||
privacy/camera_usage_description_localized={}
|
||||
privacy/location_usage_description=""
|
||||
privacy/location_usage_description_localized={}
|
||||
privacy/address_book_usage_description=""
|
||||
privacy/address_book_usage_description_localized={}
|
||||
privacy/calendar_usage_description=""
|
||||
privacy/calendar_usage_description_localized={}
|
||||
privacy/photos_library_usage_description=""
|
||||
privacy/photos_library_usage_description_localized={}
|
||||
privacy/desktop_folder_usage_description=""
|
||||
privacy/desktop_folder_usage_description_localized={}
|
||||
privacy/documents_folder_usage_description=""
|
||||
privacy/documents_folder_usage_description_localized={}
|
||||
privacy/downloads_folder_usage_description=""
|
||||
privacy/downloads_folder_usage_description_localized={}
|
||||
privacy/network_volumes_usage_description=""
|
||||
privacy/network_volumes_usage_description_localized={}
|
||||
privacy/removable_volumes_usage_description=""
|
||||
privacy/removable_volumes_usage_description_localized={}
|
||||
privacy/tracking_enabled=false
|
||||
privacy/tracking_domains=PackedStringArray()
|
||||
privacy/collected_data/name/collected=false
|
||||
privacy/collected_data/name/linked_to_user=false
|
||||
privacy/collected_data/name/used_for_tracking=false
|
||||
privacy/collected_data/name/collection_purposes=0
|
||||
privacy/collected_data/email_address/collected=false
|
||||
privacy/collected_data/email_address/linked_to_user=false
|
||||
privacy/collected_data/email_address/used_for_tracking=false
|
||||
privacy/collected_data/email_address/collection_purposes=0
|
||||
privacy/collected_data/phone_number/collected=false
|
||||
privacy/collected_data/phone_number/linked_to_user=false
|
||||
privacy/collected_data/phone_number/used_for_tracking=false
|
||||
privacy/collected_data/phone_number/collection_purposes=0
|
||||
privacy/collected_data/physical_address/collected=false
|
||||
privacy/collected_data/physical_address/linked_to_user=false
|
||||
privacy/collected_data/physical_address/used_for_tracking=false
|
||||
privacy/collected_data/physical_address/collection_purposes=0
|
||||
privacy/collected_data/other_contact_info/collected=false
|
||||
privacy/collected_data/other_contact_info/linked_to_user=false
|
||||
privacy/collected_data/other_contact_info/used_for_tracking=false
|
||||
privacy/collected_data/other_contact_info/collection_purposes=0
|
||||
privacy/collected_data/health/collected=false
|
||||
privacy/collected_data/health/linked_to_user=false
|
||||
privacy/collected_data/health/used_for_tracking=false
|
||||
privacy/collected_data/health/collection_purposes=0
|
||||
privacy/collected_data/fitness/collected=false
|
||||
privacy/collected_data/fitness/linked_to_user=false
|
||||
privacy/collected_data/fitness/used_for_tracking=false
|
||||
privacy/collected_data/fitness/collection_purposes=0
|
||||
privacy/collected_data/payment_info/collected=false
|
||||
privacy/collected_data/payment_info/linked_to_user=false
|
||||
privacy/collected_data/payment_info/used_for_tracking=false
|
||||
privacy/collected_data/payment_info/collection_purposes=0
|
||||
privacy/collected_data/credit_info/collected=false
|
||||
privacy/collected_data/credit_info/linked_to_user=false
|
||||
privacy/collected_data/credit_info/used_for_tracking=false
|
||||
privacy/collected_data/credit_info/collection_purposes=0
|
||||
privacy/collected_data/other_financial_info/collected=false
|
||||
privacy/collected_data/other_financial_info/linked_to_user=false
|
||||
privacy/collected_data/other_financial_info/used_for_tracking=false
|
||||
privacy/collected_data/other_financial_info/collection_purposes=0
|
||||
privacy/collected_data/precise_location/collected=false
|
||||
privacy/collected_data/precise_location/linked_to_user=false
|
||||
privacy/collected_data/precise_location/used_for_tracking=false
|
||||
privacy/collected_data/precise_location/collection_purposes=0
|
||||
privacy/collected_data/coarse_location/collected=false
|
||||
privacy/collected_data/coarse_location/linked_to_user=false
|
||||
privacy/collected_data/coarse_location/used_for_tracking=false
|
||||
privacy/collected_data/coarse_location/collection_purposes=0
|
||||
privacy/collected_data/sensitive_info/collected=false
|
||||
privacy/collected_data/sensitive_info/linked_to_user=false
|
||||
privacy/collected_data/sensitive_info/used_for_tracking=false
|
||||
privacy/collected_data/sensitive_info/collection_purposes=0
|
||||
privacy/collected_data/contacts/collected=false
|
||||
privacy/collected_data/contacts/linked_to_user=false
|
||||
privacy/collected_data/contacts/used_for_tracking=false
|
||||
privacy/collected_data/contacts/collection_purposes=0
|
||||
privacy/collected_data/emails_or_text_messages/collected=false
|
||||
privacy/collected_data/emails_or_text_messages/linked_to_user=false
|
||||
privacy/collected_data/emails_or_text_messages/used_for_tracking=false
|
||||
privacy/collected_data/emails_or_text_messages/collection_purposes=0
|
||||
privacy/collected_data/photos_or_videos/collected=false
|
||||
privacy/collected_data/photos_or_videos/linked_to_user=false
|
||||
privacy/collected_data/photos_or_videos/used_for_tracking=false
|
||||
privacy/collected_data/photos_or_videos/collection_purposes=0
|
||||
privacy/collected_data/audio_data/collected=false
|
||||
privacy/collected_data/audio_data/linked_to_user=false
|
||||
privacy/collected_data/audio_data/used_for_tracking=false
|
||||
privacy/collected_data/audio_data/collection_purposes=0
|
||||
privacy/collected_data/gameplay_content/collected=false
|
||||
privacy/collected_data/gameplay_content/linked_to_user=false
|
||||
privacy/collected_data/gameplay_content/used_for_tracking=false
|
||||
privacy/collected_data/gameplay_content/collection_purposes=0
|
||||
privacy/collected_data/customer_support/collected=false
|
||||
privacy/collected_data/customer_support/linked_to_user=false
|
||||
privacy/collected_data/customer_support/used_for_tracking=false
|
||||
privacy/collected_data/customer_support/collection_purposes=0
|
||||
privacy/collected_data/other_user_content/collected=false
|
||||
privacy/collected_data/other_user_content/linked_to_user=false
|
||||
privacy/collected_data/other_user_content/used_for_tracking=false
|
||||
privacy/collected_data/other_user_content/collection_purposes=0
|
||||
privacy/collected_data/browsing_history/collected=false
|
||||
privacy/collected_data/browsing_history/linked_to_user=false
|
||||
privacy/collected_data/browsing_history/used_for_tracking=false
|
||||
privacy/collected_data/browsing_history/collection_purposes=0
|
||||
privacy/collected_data/search_hhistory/collected=false
|
||||
privacy/collected_data/search_hhistory/linked_to_user=false
|
||||
privacy/collected_data/search_hhistory/used_for_tracking=false
|
||||
privacy/collected_data/search_hhistory/collection_purposes=0
|
||||
privacy/collected_data/user_id/collected=false
|
||||
privacy/collected_data/user_id/linked_to_user=false
|
||||
privacy/collected_data/user_id/used_for_tracking=false
|
||||
privacy/collected_data/user_id/collection_purposes=0
|
||||
privacy/collected_data/device_id/collected=false
|
||||
privacy/collected_data/device_id/linked_to_user=false
|
||||
privacy/collected_data/device_id/used_for_tracking=false
|
||||
privacy/collected_data/device_id/collection_purposes=0
|
||||
privacy/collected_data/purchase_history/collected=false
|
||||
privacy/collected_data/purchase_history/linked_to_user=false
|
||||
privacy/collected_data/purchase_history/used_for_tracking=false
|
||||
privacy/collected_data/purchase_history/collection_purposes=0
|
||||
privacy/collected_data/product_interaction/collected=false
|
||||
privacy/collected_data/product_interaction/linked_to_user=false
|
||||
privacy/collected_data/product_interaction/used_for_tracking=false
|
||||
privacy/collected_data/product_interaction/collection_purposes=0
|
||||
privacy/collected_data/advertising_data/collected=false
|
||||
privacy/collected_data/advertising_data/linked_to_user=false
|
||||
privacy/collected_data/advertising_data/used_for_tracking=false
|
||||
privacy/collected_data/advertising_data/collection_purposes=0
|
||||
privacy/collected_data/other_usage_data/collected=false
|
||||
privacy/collected_data/other_usage_data/linked_to_user=false
|
||||
privacy/collected_data/other_usage_data/used_for_tracking=false
|
||||
privacy/collected_data/other_usage_data/collection_purposes=0
|
||||
privacy/collected_data/crash_data/collected=false
|
||||
privacy/collected_data/crash_data/linked_to_user=false
|
||||
privacy/collected_data/crash_data/used_for_tracking=false
|
||||
privacy/collected_data/crash_data/collection_purposes=0
|
||||
privacy/collected_data/performance_data/collected=false
|
||||
privacy/collected_data/performance_data/linked_to_user=false
|
||||
privacy/collected_data/performance_data/used_for_tracking=false
|
||||
privacy/collected_data/performance_data/collection_purposes=0
|
||||
privacy/collected_data/other_diagnostic_data/collected=false
|
||||
privacy/collected_data/other_diagnostic_data/linked_to_user=false
|
||||
privacy/collected_data/other_diagnostic_data/used_for_tracking=false
|
||||
privacy/collected_data/other_diagnostic_data/collection_purposes=0
|
||||
privacy/collected_data/environment_scanning/collected=false
|
||||
privacy/collected_data/environment_scanning/linked_to_user=false
|
||||
privacy/collected_data/environment_scanning/used_for_tracking=false
|
||||
privacy/collected_data/environment_scanning/collection_purposes=0
|
||||
privacy/collected_data/hands/collected=false
|
||||
privacy/collected_data/hands/linked_to_user=false
|
||||
privacy/collected_data/hands/used_for_tracking=false
|
||||
privacy/collected_data/hands/collection_purposes=0
|
||||
privacy/collected_data/head/collected=false
|
||||
privacy/collected_data/head/linked_to_user=false
|
||||
privacy/collected_data/head/used_for_tracking=false
|
||||
privacy/collected_data/head/collection_purposes=0
|
||||
privacy/collected_data/other_data_types/collected=false
|
||||
privacy/collected_data/other_data_types/linked_to_user=false
|
||||
privacy/collected_data/other_data_types/used_for_tracking=false
|
||||
privacy/collected_data/other_data_types/collection_purposes=0
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||
open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
|
||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||
kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
|
||||
rm -rf \"{temp_dir}\""
|
||||
dotnet/include_scripts_content=false
|
||||
dotnet/include_debug_symbols=true
|
||||
dotnet/embed_build_outputs=false
|
||||
application/min_macos_version="10.12"
|
||||
debug/export_console_script=1
|
||||
notarization/apple_team_id=""
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="Web"
|
||||
platform="Web"
|
||||
runnable=false
|
||||
@@ -353,7 +93,7 @@ encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.2.options]
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
@@ -380,7 +120,7 @@ dotnet/include_scripts_content=false
|
||||
dotnet/include_debug_symbols=true
|
||||
dotnet/embed_build_outputs=false
|
||||
|
||||
[preset.3]
|
||||
[preset.2]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
@@ -400,7 +140,7 @@ encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.3.options]
|
||||
[preset.2.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://c325cbuhy0n3d"
|
||||
path="res://.godot/imported/NotoSansJP-Medium.otf-48029a81cfbb1e3a1e09d84067c2726b.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/NotoSansJP-Medium.otf"
|
||||
dest_files=["res://.godot/imported/NotoSansJP-Medium.otf-48029a81cfbb1e3a1e09d84067c2726b.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
@@ -0,0 +1,88 @@
|
||||
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://bu247t5hvmk2w"
|
||||
path="res://.godot/imported/OpenSans-SemiBold.woff2-035244bfb0cae573e35cba9e3c76e652.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://fonts/OpenSans-SemiBold.woff2"
|
||||
dest_files=["res://.godot/imported/OpenSans-SemiBold.woff2-035244bfb0cae573e35cba9e3c76e652.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[Resource("uid://c325cbuhy0n3d", "res://fonts/NotoSansJP-Medium.otf")]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Fonts
|
||||
|
||||
The theme's default font (`themes/main_theme.tres`) is `OpenSans-SemiBold.woff2`, the
|
||||
font Godot uses when a project sets none, saved from Godot so the game looks the same.
|
||||
Text it has no letters for, such as Japanese, falls back to `NotoSansJP-Medium.otf`.
|
||||
|
||||
Both are under the SIL Open Font License, see `OpenSans-LICENSE.txt` and
|
||||
`NotoSansJP-LICENSE.txt`.
|
||||
|
||||
`NotoSansJP-Medium.otf` is the Medium weight from
|
||||
[noto-cjk](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP), changed in
|
||||
one way: its line metrics (`hhea` ascent and descent) are set to Open Sans's. Godot makes
|
||||
every line as tall as the tallest font in the fallback chain, so with Noto's own, taller
|
||||
metrics all text in the game would move down and rows would grow, even without any
|
||||
Japanese on screen. The glyphs are unchanged. To update the font, download it again and
|
||||
set the metrics with [fontTools](https://github.com/fonttools/fonttools):
|
||||
|
||||
```python
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
reference = TTFont("OpenSans-SemiBold.woff2") # needs the brotli package
|
||||
noto = TTFont("NotoSansJP-Medium.otf")
|
||||
scale = noto["head"].unitsPerEm / reference["head"].unitsPerEm
|
||||
noto["hhea"].ascent = round(reference["hhea"].ascent * scale)
|
||||
noto["hhea"].descent = round(reference["hhea"].descent * scale)
|
||||
noto["hhea"].lineGap = 0
|
||||
noto.save("NotoSansJP-Medium.otf")
|
||||
```
|
||||
|
||||
`tests/integration/test_theme.gd` checks that Japanese has a font and that the line
|
||||
height is still Godot's.
|
||||
@@ -1,42 +0,0 @@
|
||||
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.
|
||||
|
||||
## Seconds before a request is given up and reported as failed. This covers the whole
|
||||
## download, so it is generous enough for long songs on a slow connection.
|
||||
var timeout_seconds: float = 120.0
|
||||
|
||||
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 and
|
||||
## timeouts are reported with response code 0.
|
||||
func request(url: String, method: int, on_complete: Callable) -> void:
|
||||
var http_request: HTTPRequest = HTTPRequest.new()
|
||||
http_request.timeout = timeout_seconds
|
||||
_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())
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="48 48 416 416">
|
||||
<!-- The pause shape of pause_icon_light.svg in one colour -->
|
||||
<path fill="#f9f9f9" d="M224,435.8V76.1c0-6.7-5.4-12.1-12.2-12.1h-71.6c-6.8,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6 C218.6,448,224,442.6,224,435.8z"/>
|
||||
<path fill="#f9f9f9" d="M371.8,64h-71.6c-6.7,0-12.2,5.4-12.2,12.1v359.7c0,6.7,5.4,12.2,12.2,12.2h71.6c6.7,0,12.2-5.4,12.2-12.2V76.1 C384,69.4,378.6,64,371.8,64z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://djdsgbhw8gpj"
|
||||
path="res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/pause_button_icon.svg"
|
||||
dest_files=["res://.godot/imported/pause_button_icon.svg-cf86876de39506a769c5610b329fdb75.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="416" height="416" viewBox="64 48 416 416">
|
||||
<!-- The play shape of play_icon_light.svg in one colour, shifted right so it looks centred -->
|
||||
<path fill="#f9f9f9" transform="translate(16 0)" d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20 c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 454 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dkweun1f426gl"
|
||||
path="res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/play_button_icon.svg"
|
||||
dest_files=["res://.godot/imported/play_button_icon.svg-76b19c7150b176f215ad401b4dd9488d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
||||
<!-- reload_light_icon.svg mirrored, so the arrow turns back like a restart -->
|
||||
<path fill="#f9f9f9" transform="translate(28 0) scale(-1 1)" d="M 12.273759,27.821814 C 8.301606,27.305128 4.6414458,25.025364 2.4172439,21.682595 -0.6740156,17.036719 -0.6987372,11.050866 2.3541959,6.4167276 6.4831292,0.1492961 14.7875,-1.7884148 21.095349,2.0437368 l 0.883303,0.5366251 0.09577,-0.6963111 c 0.07593,-0.5520493 0.198918,-0.7994615 0.593645,-1.1941885 0.682393,-0.6823936 1.29683,-0.7876518 2.135162,-0.3657721 1.11162,0.5594083 1.142977,0.6968977 1.099029,4.8188242 C 25.866598,8.48735 25.848168,8.714727 25.583585,9.075118 24.993194,9.879284 24.711316,9.931761 20.980939,9.931993 17.03905,9.932238 16.871859,9.891713 16.324094,8.803226 15.94065,8.041272 16.00249,7.4396205 16.528498,6.8144942 17.041282,6.2050862 17.302265,6.1106614 18.732442,6.0170974 l 1.29765,-0.084894 -0.711421,-0.4557502 C 16.407302,3.611373 12.42501,3.4754273 9.250891,5.1327637 6.9185825,6.3505563 5.186242,8.47902 4.3514391,11.152542 c -0.4596099,1.471937 -0.4596099,4.222979 0,5.694916 1.0532717,3.373185 3.4279179,5.747831 6.8011029,6.801103 0.874696,0.273122 1.291973,0.316826 2.983051,0.31243 1.791555,-0.0047 2.062404,-0.03855 3.050848,-0.381822 2.095349,-0.727675 4.004905,-2.225187 5.276499,-4.137935 0.712133,-1.071201 1.028577,-1.77101 1.500599,-3.318525 0.234713,-0.769505 0.466198,-1.242244 0.733848,-1.49867 0.768275,-0.736054 1.953395,-0.694987 2.67526,0.0927 0.56162,0.612834 0.678351,1.318164 0.398766,2.409492 -0.7664,2.991551 -3.210883,6.444937 -5.916304,8.35812 -2.688606,1.901292 -6.251776,2.77056 -9.58135,2.337459 z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://4t7vk634k0v4"
|
||||
path="res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/restart_light_icon.svg"
|
||||
dest_files=["res://.godot/imported/restart_light_icon.svg-3822a30546bff23ba1adb74c1723ba80.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
||||
<g fill="#f9f9f9">
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(45 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(90 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(135 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(180 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(225 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(270 14 14)"/>
|
||||
<rect x="11.75" y="2.5" width="4.5" height="5" rx="1" transform="rotate(315 14 14)"/>
|
||||
</g>
|
||||
<circle cx="14" cy="14" r="6.5" fill="none" stroke="#f9f9f9" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 889 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bylmore1gmmnr"
|
||||
path="res://.godot/imported/settings_light_icon.svg-2b0876b2265a32eb81d9266290ab6204.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/settings_light_icon.svg"
|
||||
dest_files=["res://.godot/imported/settings_light_icon.svg-2b0876b2265a32eb81d9266290ab6204.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="#f9f9f9" d="M3 9.5v5c0 .55.45 1 1 1h3.2l4.1 3.6c.65.57 1.7.11 1.7-.76V5.66c0-.87-1.05-1.33-1.7-.76L7.2 8.5H4c-.55 0-1 .45-1 1z"/>
|
||||
<path fill="none" stroke="#f9f9f9" stroke-width="2" stroke-linecap="round" d="M16 8.5a5 5 0 0 1 0 7M18.8 5.8a9 9 0 0 1 0 12.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,43 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d0uyxivsv58uh"
|
||||
path="res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icons/volume_light_icon.svg"
|
||||
dest_files=["res://.godot/imported/volume_light_icon.svg-a1b08f489924a01123c9c4f7fa5e16a0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
class_name ListRow
|
||||
extends PanelContainer
|
||||
## One item in a list of names, such as the inspiration list or a sync result column.
|
||||
##
|
||||
## Every row has its own background, alternating between two shades, so it is clear
|
||||
## where an item starts even when its text wraps over several lines.
|
||||
|
||||
const EVEN_BACKGROUND: Color = Color(1, 1, 1, 0.05)
|
||||
const ODD_BACKGROUND: Color = Color(1, 1, 1, 0.11)
|
||||
|
||||
static var _even_style: StyleBoxFlat
|
||||
static var _odd_style: StyleBoxFlat
|
||||
|
||||
## The label showing the item's text.
|
||||
var label: Label
|
||||
|
||||
## The item's text.
|
||||
var text: String:
|
||||
get:
|
||||
return label.text
|
||||
|
||||
|
||||
## Creates the row for item number [param row] (counting from 0) in a list.
|
||||
static func create(
|
||||
item_text: String,
|
||||
row: int,
|
||||
font_size: int,
|
||||
alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_LEFT,
|
||||
) -> ListRow:
|
||||
var list_row: ListRow = ListRow.new()
|
||||
list_row.add_theme_stylebox_override("panel", _style(row % 2 != 0))
|
||||
list_row.label = Label.new()
|
||||
list_row.label.add_theme_font_size_override("font_size", font_size)
|
||||
list_row.label.text = item_text
|
||||
list_row.label.horizontal_alignment = alignment
|
||||
list_row.label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
list_row.add_child(list_row.label)
|
||||
return list_row
|
||||
|
||||
|
||||
# Rows share two styles instead of each creating its own
|
||||
static func _style(odd: bool) -> StyleBoxFlat:
|
||||
if _even_style == null:
|
||||
_even_style = _make_style(EVEN_BACKGROUND)
|
||||
_odd_style = _make_style(ODD_BACKGROUND)
|
||||
return _odd_style if odd else _even_style
|
||||
|
||||
|
||||
static func _make_style(background: Color) -> StyleBoxFlat:
|
||||
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
style.bg_color = background
|
||||
style.set_corner_radius_all(4)
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
style.content_margin_top = 2
|
||||
style.content_margin_bottom = 2
|
||||
return style
|
||||
@@ -1,25 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var log_panel: Panel = %LogPanel
|
||||
|
||||
@onready
|
||||
var log_box: VBoxContainer = %LogVBoxContainer
|
||||
|
||||
var log_rows: Array
|
||||
|
||||
func add_log_row(text: String) -> void:
|
||||
var log_row: String = get_time() + text
|
||||
log_rows.append(log_row)
|
||||
log_box.add_child(make_log_label(log_row))
|
||||
|
||||
func make_log_label(row: String) -> Label:
|
||||
var log_label: Label = Label.new()
|
||||
log_label.add_theme_font_size_override("font_size", 20)
|
||||
log_label.text = row
|
||||
log_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
return log_label
|
||||
|
||||
func get_time() -> String:
|
||||
var time: Dictionary = Time.get_time_dict_from_system()
|
||||
return "%02d:%02d:%02d " % [time.hour, time.minute, time.second]
|
||||
@@ -0,0 +1,131 @@
|
||||
[gd_scene format=3 uid="uid://xwq863o6uvsu"]
|
||||
|
||||
[ext_resource type="PackedScene" path="res://main/TopBar.tscn" id="top_bar_scene"]
|
||||
[ext_resource type="PackedScene" path="res://quiz/Quiz.tscn" id="quiz_scene"]
|
||||
[ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://main/main_window.gd" id="1_eu0t5"]
|
||||
[ext_resource type="PackedScene" uid="uid://bxydgil1yifps" path="res://popups/SearchPopup.tscn" id="9_5ijvr"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpdem7pdxweb5" path="res://popups/SyncPopup.tscn" id="10_yxw0b"]
|
||||
[ext_resource type="PackedScene" uid="uid://bd1by80q1v27e" path="res://popups/ShortcutsPopup.tscn" id="13_8eebo"]
|
||||
[ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://popups/QRPopup.tscn" id="15_qr_window"]
|
||||
[ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://popups/AboutPopup.tscn" id="16_about_window"]
|
||||
[ext_resource type="PackedScene" uid="uid://bijh5h5yrivm3" path="res://popups/LogPopup.tscn" id="14_26rwn"]
|
||||
[ext_resource type="PackedScene" path="res://statistics/StatisticsView.tscn" id="statistics_view_scene"]
|
||||
[ext_resource type="PackedScene" path="res://popups/SoundtrackStatsPopup.tscn" id="soundtrack_stats_popup_scene"]
|
||||
[ext_resource type="PackedScene" uid="uid://dldpeo5y3l5hq" path="res://popups/SettingsPopup.tscn" id="settings_popup_scene"]
|
||||
[ext_resource type="PackedScene" path="res://popups/UpdatePopup.tscn" id="update_popup_scene"]
|
||||
[ext_resource type="PackedScene" path="res://popups/UploadPopup.tscn" id="upload_popup_scene"]
|
||||
|
||||
[node name="MainWindow" type="Control" unique_id=407149696]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_eu0t5")
|
||||
|
||||
[node name="Background" type="Panel" parent="." unique_id=1093825566]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_type_variation = &"WindowBackground"
|
||||
|
||||
[node name="TopBar" parent="." unique_id=1734261695 instance=ExtResource("top_bar_scene")]
|
||||
|
||||
[node name="Quiz" parent="." unique_id=1357908642 instance=ExtResource("quiz_scene")]
|
||||
|
||||
[node name="StatisticsView" parent="." unique_id=1212999001 instance=ExtResource("statistics_view_scene")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
|
||||
[node name="SearchPopup" parent="." unique_id=1661250375 instance=ExtResource("9_5ijvr")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 384.0
|
||||
offset_top = 100.0
|
||||
offset_right = 384.0
|
||||
offset_bottom = 100.0
|
||||
|
||||
[node name="SyncPopup" parent="." unique_id=182499699 instance=ExtResource("10_yxw0b")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 210.0
|
||||
offset_top = 220.0
|
||||
offset_right = 210.0
|
||||
offset_bottom = 220.0
|
||||
|
||||
[node name="QRPopup" parent="." unique_id=589733821 instance=ExtResource("15_qr_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 735.0
|
||||
offset_top = 270.0
|
||||
offset_right = 735.0
|
||||
offset_bottom = 270.0
|
||||
|
||||
[node name="ShortcutsPopup" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 610.0
|
||||
offset_top = 187.0
|
||||
offset_right = 610.0
|
||||
offset_bottom = 187.0
|
||||
|
||||
[node name="AboutPopup" parent="." unique_id=141261008 instance=ExtResource("16_about_window")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 360.0
|
||||
offset_top = 220.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 220.0
|
||||
|
||||
[node name="LogPopup" parent="." unique_id=1289881388 instance=ExtResource("14_26rwn")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 610.0
|
||||
offset_top = 190.0
|
||||
offset_right = 1310.0
|
||||
offset_bottom = 890.0
|
||||
|
||||
[node name="SettingsPopup" parent="." unique_id=1323246978 instance=ExtResource("settings_popup_scene")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 700.0
|
||||
offset_top = 405.0
|
||||
offset_right = 700.0
|
||||
offset_bottom = 405.0
|
||||
|
||||
[node name="SoundtrackStatsPopup" parent="." unique_id=1213999001 instance=ExtResource("soundtrack_stats_popup_scene")]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 610.0
|
||||
offset_top = 140.0
|
||||
offset_right = 610.0
|
||||
offset_bottom = 140.0
|
||||
|
||||
[node name="UpdatePopup" parent="." instance=ExtResource("update_popup_scene")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 640.0
|
||||
offset_top = 390.0
|
||||
offset_right = 640.0
|
||||
offset_bottom = 390.0
|
||||
|
||||
[node name="UploadPopup" parent="." instance=ExtResource("upload_popup_scene")]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
offset_left = 580.0
|
||||
offset_top = 200.0
|
||||
offset_right = 580.0
|
||||
offset_bottom = 200.0
|
||||
@@ -0,0 +1,151 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://main/top_bar.gd" id="top_bar_script"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_tabs"]
|
||||
|
||||
[node name="TopBar" type="Control" unique_id=1489162753]
|
||||
layout_mode = 3
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 56.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("top_bar_script")
|
||||
|
||||
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
|
||||
layout_mode = 0
|
||||
offset_right = 1920.0
|
||||
offset_bottom = 56.0
|
||||
theme_type_variation = &"TopBarBackground"
|
||||
|
||||
[node name="Title" type="Label" parent="." unique_id=1187907861]
|
||||
layout_mode = 0
|
||||
offset_left = 64.0
|
||||
offset_top = 12.0
|
||||
offset_right = 369.0
|
||||
offset_bottom = 50.0
|
||||
size_flags_horizontal = 0
|
||||
theme_type_variation = &"Title"
|
||||
text = "Music Player Randomizer"
|
||||
|
||||
[node name="DebugLabel" type="Label" parent="." unique_id=1959221257]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 384.0
|
||||
offset_top = 12.0
|
||||
offset_right = 470.0
|
||||
offset_bottom = 50.0
|
||||
theme_type_variation = &"HighlightLarge"
|
||||
text = "DEBUG"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="UpdateBox" type="HBoxContainer" parent="." unique_id=1959221258]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 1096.0
|
||||
offset_top = 12.0
|
||||
offset_right = 1432.0
|
||||
offset_bottom = 50.0
|
||||
mouse_filter = 2
|
||||
alignment = 2
|
||||
|
||||
[node name="UpdateButton" type="Button" parent="UpdateBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
focus_mode = 0
|
||||
mouse_default_cursor_shape = 2
|
||||
theme_type_variation = &"UpdateButton"
|
||||
text = "Version 0.0.0 is available"
|
||||
text_overrun_behavior = 3
|
||||
clip_text = true
|
||||
alignment = 2
|
||||
|
||||
[node name="OfflineLabel" type="Label" parent="." unique_id=1959221259]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 1096.0
|
||||
offset_top = 12.0
|
||||
offset_right = 1432.0
|
||||
offset_bottom = 50.0
|
||||
mouse_filter = 1
|
||||
theme_type_variation = &"HighlightMedium"
|
||||
text = "Offline: Music"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 1200.0
|
||||
offset_top = 10.0
|
||||
offset_right = 1856.0
|
||||
offset_bottom = 46.0
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 12
|
||||
alignment = 2
|
||||
|
||||
[node name="SyncButton" type="Button" parent="Buttons" unique_id=1228313556]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Sync"
|
||||
|
||||
[node name="SettingsButton" type="Button" parent="Buttons" unique_id=1228313557]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Settings"
|
||||
|
||||
[node name="ShortcutsButton" type="Button" parent="Buttons" unique_id=2066498226]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "Shortcuts"
|
||||
|
||||
[node name="AboutButton" type="Button" parent="Buttons" unique_id=1057035253]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "About"
|
||||
|
||||
[node name="QRButton" type="Button" parent="Buttons" unique_id=2021908559]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
focus_mode = 0
|
||||
action_mode = 0
|
||||
text = "QR"
|
||||
|
||||
[node name="QuizTab" type="Button" parent="." unique_id=1903412671]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 839.0
|
||||
offset_top = 10.0
|
||||
offset_right = 954.0
|
||||
offset_bottom = 46.0
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"TabButton"
|
||||
toggle_mode = true
|
||||
button_pressed = true
|
||||
action_mode = 0
|
||||
button_group = SubResource("ButtonGroup_tabs")
|
||||
text = "Quiz"
|
||||
|
||||
[node name="StatisticsTab" type="Button" parent="." unique_id=1903412672]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 966.0
|
||||
offset_top = 10.0
|
||||
offset_right = 1081.0
|
||||
offset_bottom = 46.0
|
||||
focus_mode = 0
|
||||
theme_type_variation = &"TabButton"
|
||||
toggle_mode = true
|
||||
action_mode = 0
|
||||
button_group = SubResource("ButtonGroup_tabs")
|
||||
text = "Statistics"
|
||||
@@ -0,0 +1,209 @@
|
||||
class_name MainWindow
|
||||
extends Control
|
||||
## The main window: the top bar, the popups opened from it, and below it the quiz or the
|
||||
## statistics, depending on the chosen tab.
|
||||
##
|
||||
## Everything that is not part of a quiz lives here.
|
||||
|
||||
## How far a tab slides in from the side when it is chosen.
|
||||
const TAB_SLIDE: float = 60.0
|
||||
|
||||
## Dims the window behind an open popup and keeps clicks from reaching the buttons under it.
|
||||
var popup_backdrop: PopupBackdrop
|
||||
|
||||
@onready var top_bar: TopBar = $TopBar
|
||||
@onready var quiz: Quiz = $Quiz
|
||||
@onready var search_popup: SearchPopup = $SearchPopup
|
||||
@onready var sync_popup: SyncPopup = %SyncPopup
|
||||
@onready var shortcuts_popup: ShortcutsPopup = %ShortcutsPopup
|
||||
@onready var qr_popup: QRPopup = %QRPopup
|
||||
@onready var about_popup: AboutPopup = %AboutPopup
|
||||
@onready var log_popup: LogPopup = %LogPopup
|
||||
@onready var settings_popup: SettingsPopup = %SettingsPopup
|
||||
@onready var update_popup: UpdatePopup = %UpdatePopup
|
||||
@onready var upload_popup: UploadPopup = %UploadPopup
|
||||
@onready var statistics_view: StatisticsView = $StatisticsView
|
||||
@onready var soundtrack_stats_popup: SoundtrackStatsPopup = $SoundtrackStatsPopup
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
popup_backdrop = PopupBackdrop.add_to(self, _popups())
|
||||
# Every button grows a little under the mouse and shrinks when clicked
|
||||
ButtonBounce.add_to_every_button(get_tree())
|
||||
if OS.has_feature("macos"):
|
||||
GameLog.debug("is_mac")
|
||||
quiz.search_pressed.connect(show_search)
|
||||
top_bar.sync_pressed.connect(sync_soundtracks)
|
||||
top_bar.tab_selected.connect(show_tab)
|
||||
top_bar.shortcuts_pressed.connect(show_shortcuts)
|
||||
top_bar.about_pressed.connect(show_about)
|
||||
top_bar.qr_pressed.connect(show_qr)
|
||||
top_bar.settings_pressed.connect(settings_popup.open)
|
||||
top_bar.update_pressed.connect(update_popup.show_update)
|
||||
update_popup.restarting.connect(quiz.save_now)
|
||||
statistics_view.add_soundtrack_pressed.connect(upload_popup.choose_folder)
|
||||
upload_popup.soundtrack_added.connect(_on_soundtrack_added)
|
||||
get_window().files_dropped.connect(on_files_dropped)
|
||||
settings_popup.server_changed.connect(quiz.on_server_changed)
|
||||
settings_popup.characters_changed.connect(quiz.character_select_popup.load_characters)
|
||||
settings_popup.clear_cache_requested.connect(clear_cache)
|
||||
settings_popup.visibility_changed.connect(_on_settings_popup_visibility_changed)
|
||||
|
||||
sync_popup.sync_started.connect(_on_sync_started)
|
||||
sync_popup.sync_finished.connect(_on_sync_finished)
|
||||
quiz.server_changed.connect(search_popup.get_list_of_soundtracks)
|
||||
quiz.server_changed.connect(_on_music_source_changed)
|
||||
quiz.event_logged.connect(log_popup.add_log_row)
|
||||
statistics_view.soundtrack_selected.connect(soundtrack_stats_popup.show_soundtrack)
|
||||
soundtrack_stats_popup.covers = statistics_view.covers
|
||||
quiz.now_playing.covers = statistics_view.covers
|
||||
search_popup.soundtracks_loaded.connect(_remember_cover_times)
|
||||
# New alternative names are found by the statistics search and the Search popup
|
||||
soundtrack_stats_popup.alternative_names_changed.connect(
|
||||
statistics_view.update_alternative_names)
|
||||
soundtrack_stats_popup.alternative_names_changed.connect(
|
||||
func(_soundtrack: Soundtrack) -> void: search_popup.get_list_of_soundtracks())
|
||||
soundtrack_stats_popup.cover_changed.connect(statistics_view.update_cover)
|
||||
|
||||
# An update leaves the old version next to the game, to delete once the new one runs
|
||||
if !OS.has_feature("editor"):
|
||||
Updater.remove_old_files(OS.get_executable_path())
|
||||
# A saved quiz may have been played from a music folder
|
||||
top_bar.update_offline_state()
|
||||
search_popup.get_list_of_soundtracks()
|
||||
show_about_if_new_version()
|
||||
|
||||
|
||||
## Deletes the cached character pictures and covers. The characters are loaded again at
|
||||
## once, so the character select has its pictures; covers come when they are shown.
|
||||
func clear_cache() -> void:
|
||||
quiz.character_select_popup.character_cache.clear()
|
||||
statistics_view.covers.clear()
|
||||
quiz.character_select_popup.load_characters()
|
||||
|
||||
|
||||
## Offers to upload a folder dropped on the window as a new soundtrack. Dropped songs
|
||||
## stand for the folder they are in.
|
||||
func on_files_dropped(paths: PackedStringArray) -> void:
|
||||
if paths.is_empty():
|
||||
return
|
||||
var folder: String = paths[0]
|
||||
for path: String in paths:
|
||||
if DirAccess.dir_exists_absolute(path):
|
||||
folder = path
|
||||
break
|
||||
if !DirAccess.dir_exists_absolute(folder):
|
||||
folder = folder.get_base_dir()
|
||||
upload_popup.show_folder(folder)
|
||||
|
||||
|
||||
# A new soundtrack is found by search, listed for inspiration and in the statistics
|
||||
func _on_soundtrack_added(_soundtrack: SoundtrackWithSongList) -> void:
|
||||
search_popup.get_list_of_soundtracks()
|
||||
quiz.inspiration_list.load_soundtracks()
|
||||
if statistics_view.visible:
|
||||
statistics_view.refresh()
|
||||
|
||||
|
||||
# The quiz finds a soundtrack's cover by the cover times in the soundtrack list
|
||||
func _remember_cover_times(soundtracks: Array[Soundtrack]) -> void:
|
||||
var times: Dictionary = {}
|
||||
for soundtrack: Soundtrack in soundtracks:
|
||||
times[soundtrack.uuid] = soundtrack.cover_modified
|
||||
statistics_view.covers.set_cover_times(times)
|
||||
|
||||
|
||||
# After another server or a music folder was picked. Statistics need the server.
|
||||
func _on_music_source_changed() -> void:
|
||||
top_bar.update_offline_state()
|
||||
top_bar.check_for_update()
|
||||
if Settings.is_offline() && statistics_view.visible:
|
||||
show_tab(&"quiz")
|
||||
|
||||
|
||||
## Opens the About popup, with what is new, the first time this version starts.
|
||||
func show_about_if_new_version() -> void:
|
||||
if Settings.last_seen_version == Settings.version:
|
||||
return
|
||||
Settings.last_seen_version = Settings.version
|
||||
Settings.save_settings()
|
||||
show_about()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.alt_pressed && event.keycode == KEY_ENTER:
|
||||
GameLog.debug("Alt + Enter pressed")
|
||||
Settings.set_fullscreen(!Settings.fullscreen)
|
||||
if event.alt_pressed && event.keycode == KEY_K:
|
||||
shortcuts_popup.toggle()
|
||||
if event.alt_pressed && event.keycode == KEY_L:
|
||||
log_popup.toggle()
|
||||
|
||||
|
||||
func _popups() -> Array[BasePopup]:
|
||||
return [search_popup, sync_popup, qr_popup, shortcuts_popup, about_popup, log_popup,
|
||||
soundtrack_stats_popup, settings_popup, update_popup, upload_popup]
|
||||
|
||||
|
||||
# How many songs to download ahead can be changed in the settings. Waiting until the
|
||||
# settings are closed avoids starting downloads while the number is clicked up and down.
|
||||
func _on_settings_popup_visibility_changed() -> void:
|
||||
if !settings_popup.visible:
|
||||
quiz.song_loader.fill_cache()
|
||||
|
||||
|
||||
func show_search() -> void:
|
||||
GameLog.debug("show_search")
|
||||
search_popup.toggle()
|
||||
|
||||
|
||||
func show_about() -> void:
|
||||
GameLog.debug("show_about")
|
||||
about_popup.open()
|
||||
|
||||
|
||||
func show_shortcuts() -> void:
|
||||
GameLog.debug("show_shortcuts")
|
||||
shortcuts_popup.open()
|
||||
|
||||
|
||||
func show_qr() -> void:
|
||||
GameLog.debug("show_qr")
|
||||
qr_popup.open()
|
||||
|
||||
|
||||
## Shows the quiz for [code]&"quiz"[/code] or the statistics for [code]&"statistics"[/code].
|
||||
## A hidden quiz keeps playing its song.
|
||||
func show_tab(tab: StringName) -> void:
|
||||
GameLog.debug("show_tab ", tab)
|
||||
top_bar.select_tab(tab)
|
||||
var was_statistics: bool = statistics_view.visible
|
||||
statistics_view.visible = tab == &"statistics"
|
||||
quiz.visible = !statistics_view.visible
|
||||
# The tab that comes in slides from its side: statistics from the right
|
||||
if statistics_view.visible && !was_statistics:
|
||||
Anim.slide_in(statistics_view, Vector2(TAB_SLIDE, 0))
|
||||
elif quiz.visible && was_statistics:
|
||||
Anim.slide_in(quiz, Vector2(-TAB_SLIDE, 0))
|
||||
if statistics_view.visible:
|
||||
statistics_view.refresh()
|
||||
|
||||
|
||||
func sync_soundtracks() -> void:
|
||||
GameLog.debug("sync_soundtracks")
|
||||
sync_popup.start_sync()
|
||||
|
||||
|
||||
func _on_sync_started() -> void:
|
||||
GameLog.debug("_on_sync_started")
|
||||
top_bar.sync_button.disabled = false
|
||||
quiz.lock_for_sync()
|
||||
|
||||
|
||||
func _on_sync_finished() -> void:
|
||||
GameLog.debug("_on_sync_finished")
|
||||
search_popup.get_list_of_soundtracks()
|
||||
quiz.on_sync_finished()
|
||||
if statistics_view.visible:
|
||||
statistics_view.refresh()
|
||||
File renamed without changes.
+117
@@ -0,0 +1,117 @@
|
||||
class_name TopBar
|
||||
extends Control
|
||||
## The bar along the top of the main window, with the title, the Quiz and Statistics tabs
|
||||
## and the buttons for things that are not part of a quiz.
|
||||
##
|
||||
## With a music folder picked, the buttons that need the server are turned off and the
|
||||
## folder's name is shown.
|
||||
|
||||
## Shown on the buttons that need the server while a music folder is used.
|
||||
const OFFLINE_TOOLTIP: String = "Not available with a music folder"
|
||||
|
||||
signal sync_pressed
|
||||
signal settings_pressed
|
||||
signal shortcuts_pressed
|
||||
signal about_pressed
|
||||
signal qr_pressed
|
||||
## Emitted when a tab is chosen: [code]&"quiz"[/code] or [code]&"statistics"[/code].
|
||||
signal tab_selected(tab: StringName)
|
||||
## Emitted when "Version X is available" is clicked, with the newest version.
|
||||
signal update_pressed(latest_version: String)
|
||||
|
||||
## Space between the message in the middle and the tabs and buttons around it.
|
||||
const MIDDLE_GAP: float = 16.0
|
||||
|
||||
## The newest release the server knows of, once asked, or empty.
|
||||
var latest_version: String = ""
|
||||
|
||||
@onready var sync_button: Button = %SyncButton
|
||||
@onready var debug_label: Label = $DebugLabel
|
||||
@onready var update_box: HBoxContainer = $UpdateBox
|
||||
@onready var update_button: Button = %UpdateButton
|
||||
@onready var offline_label: Label = $OfflineLabel
|
||||
@onready var qr_button: Button = %QRButton
|
||||
@onready var quiz_tab: Button = %QuizTab
|
||||
@onready var statistics_tab: Button = %StatisticsTab
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Debug builds use the local server, see Settings
|
||||
debug_label.visible = Settings.is_debug
|
||||
sync_button.pressed.connect(sync_pressed.emit)
|
||||
%SettingsButton.pressed.connect(settings_pressed.emit)
|
||||
%ShortcutsButton.pressed.connect(shortcuts_pressed.emit)
|
||||
%AboutButton.pressed.connect(about_pressed.emit)
|
||||
qr_button.pressed.connect(qr_pressed.emit)
|
||||
quiz_tab.pressed.connect(tab_selected.emit.bind(&"quiz"))
|
||||
statistics_tab.pressed.connect(tab_selected.emit.bind(&"statistics"))
|
||||
update_button.pressed.connect(func() -> void: update_pressed.emit(latest_version))
|
||||
# The buttons are as wide as the theme makes them, so the message fits between
|
||||
statistics_tab.item_rect_changed.connect(_fit_middle)
|
||||
sync_button.item_rect_changed.connect(_fit_middle)
|
||||
_fit_middle()
|
||||
update_offline_state()
|
||||
check_for_update()
|
||||
|
||||
|
||||
## Turns the buttons that need the server off or on, after a music folder was picked or
|
||||
## the server is used again.
|
||||
func update_offline_state() -> void:
|
||||
var offline: bool = Settings.is_offline()
|
||||
for button: Button in [sync_button, qr_button, statistics_tab]:
|
||||
button.disabled = offline
|
||||
button.tooltip_text = OFFLINE_TOOLTIP if offline else ""
|
||||
offline_label.visible = offline
|
||||
if offline:
|
||||
offline_label.text = "Offline: " + Settings.music_folder.display_name()
|
||||
offline_label.tooltip_text = Settings.music_folder.path
|
||||
update_box.visible = false
|
||||
|
||||
|
||||
## Asks the server for the newest MusicPlayer release and tells the user when it is newer
|
||||
## than this one. Not asked while a music folder is used.
|
||||
func check_for_update() -> void:
|
||||
if Settings.is_offline():
|
||||
update_box.visible = false
|
||||
return
|
||||
MusicServerClient.fetch_latest_client_version(show_latest_version)
|
||||
|
||||
|
||||
## Shows that [param latest] is available when it is newer than this version, and hides
|
||||
## the message otherwise.
|
||||
func show_latest_version(latest: Variant) -> void:
|
||||
var newer: bool = latest is String && is_newer_version(latest, Settings.version) \
|
||||
&& !Settings.is_offline()
|
||||
update_box.visible = newer
|
||||
if newer:
|
||||
latest_version = latest
|
||||
update_button.text = "Version %s is available" % latest
|
||||
update_button.tooltip_text = "You have %s. Click to update." % Settings.version
|
||||
|
||||
|
||||
# Fits "Version X is available" and the offline label between the tabs and the buttons
|
||||
func _fit_middle() -> void:
|
||||
var left: float = statistics_tab.position.x + statistics_tab.size.x + MIDDLE_GAP
|
||||
var right: float = sync_button.global_position.x - global_position.x - MIDDLE_GAP
|
||||
for middle: Control in [update_box, offline_label]:
|
||||
middle.position.x = left
|
||||
middle.size.x = maxf(right - left, 0.0)
|
||||
|
||||
|
||||
## Whether version [param latest] is newer than [param current], comparing numbers such as
|
||||
## [code]1.10.0[/code] part by part. A leading "v" is ignored, and missing parts count as 0.
|
||||
static func is_newer_version(latest: String, current: String) -> bool:
|
||||
var latest_parts: PackedStringArray = latest.strip_edges().trim_prefix("v").split(".")
|
||||
var current_parts: PackedStringArray = current.strip_edges().trim_prefix("v").split(".")
|
||||
for i: int in maxi(latest_parts.size(), current_parts.size()):
|
||||
var a: int = latest_parts[i].to_int() if i < latest_parts.size() else 0
|
||||
var b: int = current_parts[i].to_int() if i < current_parts.size() else 0
|
||||
if a != b:
|
||||
return a > b
|
||||
return false
|
||||
|
||||
|
||||
## Shows [param tab] as the chosen tab, without emitting [signal tab_selected].
|
||||
func select_tab(tab: StringName) -> void:
|
||||
quiz_tab.set_pressed_no_signal(tab == &"quiz")
|
||||
statistics_tab.set_pressed_no_signal(tab == &"statistics")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb2ukabiec1vv
|
||||
@@ -1,139 +0,0 @@
|
||||
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, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/music/list", on_done, on_failed)
|
||||
|
||||
|
||||
## 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, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/music/rand/low" if low_played else "/music/rand", on_done, on_failed)
|
||||
|
||||
|
||||
## Adds the most recently fetched random song to the server playlist.
|
||||
func add_to_queue(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
send("/music/addQue", on_done, on_failed)
|
||||
|
||||
|
||||
## Fetches [code]{SongNo, Soundtrack, Song}[/code] for the most recently queued song.
|
||||
func fetch_song_info(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/music/info", on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads a song from the server playlist as MP3 bytes.
|
||||
func fetch_song(song_no: int, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/music?song=" + str(song_no), on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads the sound test song as MP3 bytes.
|
||||
func fetch_sound_test_song(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/music/soundTest", on_done, on_failed)
|
||||
|
||||
|
||||
## Clears the server playlist.
|
||||
func reset_playlist(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
send("/music/reset", on_done, on_failed)
|
||||
|
||||
|
||||
## Fetches every soundtrack name as an Array of Strings.
|
||||
func fetch_all_soundtracks(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/music/all", on_done, on_failed)
|
||||
|
||||
|
||||
## Fetches every soundtrack name, sorted, as an Array of Strings.
|
||||
func fetch_all_soundtracks_ordered(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/music/all/order", on_done, on_failed)
|
||||
|
||||
|
||||
## 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(), Callable(), HTTPClient.METHOD_PUT)
|
||||
|
||||
|
||||
## Fetches the names of all character images.
|
||||
func fetch_character_list(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/characters", on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads a character image as PNG bytes.
|
||||
func fetch_character_image(
|
||||
character_name: String, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/character?name=" + character_name.uri_encode(), on_done, on_failed)
|
||||
|
||||
|
||||
## Starts syncing the server's soundtrack library with its music folder.
|
||||
func start_sync(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
send("/sync", on_done, on_failed)
|
||||
|
||||
|
||||
## Fetches the sync state: [code]{progress, time_spent}[/code] while running, or the
|
||||
## sync result once finished.
|
||||
func fetch_sync_progress(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json("/sync/progress", on_done, on_failed)
|
||||
|
||||
|
||||
## Requests [param path] and calls [param on_done] with the parsed JSON.
|
||||
## [param on_failed] is called with no arguments if the request fails or the body is
|
||||
## not valid JSON.
|
||||
func get_json(path: String, on_done: Callable, on_failed: Callable = 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()])
|
||||
_call_if_valid(on_failed)
|
||||
return
|
||||
on_done.call(json.data)
|
||||
, on_failed)
|
||||
|
||||
|
||||
## Requests [param path] and calls [param on_done] with the raw response body, or
|
||||
## [param on_failed] with no arguments if the request fails.
|
||||
func get_bytes(path: String, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
_request(path, HTTPClient.METHOD_GET, on_done, on_failed)
|
||||
|
||||
|
||||
## Requests [param path] and calls [param on_done] with no arguments when it succeeds,
|
||||
## or [param on_failed] when it fails. Either may be an empty Callable.
|
||||
func send(
|
||||
path: String,
|
||||
on_done: Callable,
|
||||
on_failed: Callable = Callable(),
|
||||
method: int = HTTPClient.METHOD_GET,
|
||||
) -> void:
|
||||
_request(path, method, func(_body: PackedByteArray) -> void:
|
||||
_call_if_valid(on_done)
|
||||
, on_failed)
|
||||
|
||||
|
||||
func _request(path: String, method: int, on_success: Callable, on_failed: 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])
|
||||
_call_if_valid(on_failed)
|
||||
return
|
||||
on_success.call(body)
|
||||
)
|
||||
|
||||
|
||||
static func _call_if_valid(callback: Callable) -> void:
|
||||
if callback.is_valid():
|
||||
callback.call()
|
||||
@@ -1,4 +0,0 @@
|
||||
[gd_resource type="StyleBoxFlat" format=3 uid="uid://blf3g88mugovr"]
|
||||
|
||||
[resource]
|
||||
bg_color = Color(0.257813, 0.257813, 0.257813, 1)
|
||||
@@ -1,65 +0,0 @@
|
||||
extends PopupWindow
|
||||
|
||||
@onready
|
||||
var character_panel: Panel = %CharacterPanel
|
||||
|
||||
@onready
|
||||
var title_label: Label = %TitleLabel
|
||||
|
||||
@onready
|
||||
var character_grid: GridContainer = %CharacterGridContainer
|
||||
|
||||
@onready
|
||||
var close_timer: Timer = %CloseTimer
|
||||
|
||||
var can_select_character: bool = false
|
||||
|
||||
signal new_new_character_selected(file_name: String)
|
||||
|
||||
var character: PackedScene = preload("res://character.tscn")
|
||||
|
||||
func _ready() -> void:
|
||||
close_timer.timeout.connect(_on_close_timeout)
|
||||
|
||||
func _on_close_timeout() -> void:
|
||||
print("_on_close_timeout!")
|
||||
Settings.character_select_open = false
|
||||
close_timer.stop()
|
||||
|
||||
func load_characters() -> void:
|
||||
print("load_characters")
|
||||
var fetch_character_list: Callable = func fetch_character_list(list) -> void:
|
||||
print("fetch_character_list")
|
||||
Settings.delete_children(character_grid)
|
||||
if typeof(list) == TYPE_ARRAY:
|
||||
for character_name: String in list:
|
||||
print("character_name ", character_name)
|
||||
var character_box: Character = character.instantiate()
|
||||
character_grid.add_child(character_box)
|
||||
character_box.custom_minimum_size = Vector2(235, 145)
|
||||
character_box.size = Vector2(235, 145)
|
||||
character_box.set_character_name(character_name)
|
||||
character_box.connect("new_character_selected", _on_character_selected)
|
||||
MusicServerClient.fetch_character_list(fetch_character_list)
|
||||
|
||||
func show_grid(player_name: String) -> void:
|
||||
print("show_grid")
|
||||
title_label.text = "Select character for " + player_name
|
||||
close_timer.stop()
|
||||
open()
|
||||
can_select_character = true
|
||||
Settings.character_select_open = true
|
||||
|
||||
func close() -> void:
|
||||
super()
|
||||
Settings.character_select_open = false
|
||||
|
||||
func _on_character_selected(texture: ImageTexture) -> void:
|
||||
print("_on_character_selected")
|
||||
# Not close(): character_select_open stays true until close_timer fires, so the click
|
||||
# that picked the character does not also select a song behind it
|
||||
self.visible = false
|
||||
if can_select_character:
|
||||
new_new_character_selected.emit(texture)
|
||||
close_timer.start()
|
||||
can_select_character = false
|
||||
@@ -1,59 +0,0 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cdy4kvemwaiom"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cvffg87wr6r3y" path="res://new_character_select.gd" id="1_h4tdp"]
|
||||
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_p4iny"]
|
||||
font_size = 25
|
||||
|
||||
[node name="Control" type="Control" node_paths=PackedStringArray("panel")]
|
||||
theme = ExtResource("popup_theme")
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
script = ExtResource("1_h4tdp")
|
||||
panel = NodePath("CharacterPanel")
|
||||
|
||||
[node name="CharacterPanel" type="Panel" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 1290.0
|
||||
offset_bottom = 665.0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CharacterPanel"]
|
||||
layout_mode = 0
|
||||
offset_right = 1290.0
|
||||
offset_bottom = 659.0
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="CharacterPanel/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
text = "Character Select"
|
||||
label_settings = SubResource("LabelSettings_p4iny")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="CharacterPanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 20
|
||||
theme_override_constants/margin_top = 10
|
||||
theme_override_constants/margin_right = 20
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="CharacterPanel/VBoxContainer/MarginContainer"]
|
||||
custom_minimum_size = Vector2(0, 600)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="CharacterGridContainer" type="GridContainer" parent="CharacterPanel/VBoxContainer/MarginContainer/ScrollContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(1150, 600)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 10
|
||||
theme_override_constants/v_separation = 10
|
||||
columns = 5
|
||||
|
||||
[node name="CloseTimer" type="Timer" parent="CharacterPanel"]
|
||||
unique_name_in_owner = true
|
||||
Loaded 100 of 395 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user