495 lines
16 KiB
GDScript
495 lines
16 KiB
GDScript
class_name SfxConfigPanel
|
|
extends Control
|
|
|
|
# signal config_changed(node_name: String, property: String, value)
|
|
|
|
# 面板相关
|
|
@onready var scroll_container: ScrollContainer = %ScrollContainer
|
|
@onready var vbox_container: VBoxContainer = %VBoxContainer
|
|
@onready var reset_button: Button = %ResetButton
|
|
@onready var import_button: Button = %ImportButton
|
|
@onready var export_button: Button = %ExportButton
|
|
# 音频预览播放器
|
|
@onready var preview_player: AudioStreamPlayer = %AudioStreamPlayer
|
|
# 文件对话框
|
|
@onready var file_dialog: FileDialog = %FileDialog
|
|
|
|
# 数据存储
|
|
var sfx_nodes: Array[Node] = []
|
|
var config_data: Dictionary = {}
|
|
var current_scene_name: String = ""
|
|
|
|
|
|
func _ready():
|
|
file_dialog.file_selected.connect(_on_file_selected)
|
|
reset_button.pressed.connect(_on_reset_pressed)
|
|
import_button.pressed.connect(_on_import_pressed)
|
|
export_button.pressed.connect(_on_export_pressed)
|
|
# 初始加载
|
|
refresh_sfx_list(SceneManager.get_ground())
|
|
|
|
|
|
# 1. 检测 current_scene 下 Sfx 节点
|
|
func refresh_sfx_list(ground: Ground2D, headless := false):
|
|
sfx_nodes.clear()
|
|
current_scene_name = GroundLoader.get_ground_scene_readable_name(ground.scene_name)
|
|
find_sfx_nodes(ground)
|
|
load_config()
|
|
if not headless:
|
|
refresh_ui()
|
|
|
|
var ignore_class_list = ["Portal2D", "Interactable2D", "Note2D", "Inspectable2D", "Pickable2D", "Npc2D", "Ambush2D"]
|
|
|
|
func find_sfx_nodes(node: Node):
|
|
var script = node.get_script()
|
|
if script:
|
|
if script.get_global_name() in ignore_class_list:
|
|
return
|
|
# if node is Sfx or node is VibeSfx:
|
|
if node is Sfx:
|
|
if node.has_method("play") and node.get("volume_db") != null and node.get("stream") != null:
|
|
sfx_nodes.append(node)
|
|
# 递归检查子节点
|
|
for child in node.get_children():
|
|
find_sfx_nodes(child)
|
|
|
|
func clear_ui():
|
|
for child in vbox_container.get_children():
|
|
child.queue_free()
|
|
|
|
# 2. 在面板生成对应条目,双向绑定
|
|
func create_ui_items():
|
|
for sfx_node in sfx_nodes:
|
|
create_sfx_item(sfx_node)
|
|
|
|
func refresh_ui():
|
|
clear_ui()
|
|
create_ui_items()
|
|
|
|
|
|
func create_sfx_item(sfx_node: Node):
|
|
var item_container = VBoxContainer.new()
|
|
item_container.add_theme_constant_override("separation", 8)
|
|
vbox_container.add_child(item_container)
|
|
|
|
# 节点名称标签
|
|
var name_label = Label.new()
|
|
name_label.text = sfx_node.name
|
|
name_label.add_theme_font_size_override("font_size", 14)
|
|
item_container.add_child(name_label)
|
|
|
|
var controls_hbox = HBoxContainer.new()
|
|
controls_hbox.add_theme_constant_override("separation", 10)
|
|
item_container.add_child(controls_hbox)
|
|
|
|
# 音量滑块
|
|
var volume_vbox = VBoxContainer.new()
|
|
controls_hbox.add_child(volume_vbox)
|
|
|
|
var volume_label = Label.new()
|
|
volume_label.text = "音量 (dB)"
|
|
volume_vbox.add_child(volume_label)
|
|
|
|
var volume_hbox = HBoxContainer.new()
|
|
volume_vbox.add_child(volume_hbox)
|
|
|
|
var volume_slider = HSlider.new()
|
|
volume_slider.min_value = -60.0
|
|
volume_slider.max_value = 20.0
|
|
volume_slider.step = 0.1
|
|
volume_slider.value = sfx_node.volume_db
|
|
volume_slider.custom_minimum_size.x = 200
|
|
volume_hbox.add_child(volume_slider)
|
|
|
|
var volume_value_label = Label.new()
|
|
volume_value_label.text = str(snapped(sfx_node.volume_db, 0.1))
|
|
volume_value_label.custom_minimum_size.x = 50
|
|
volume_hbox.add_child(volume_value_label)
|
|
|
|
# 双向绑定音量
|
|
volume_slider.value_changed.connect(func(value):
|
|
sfx_node.volume_db = value
|
|
volume_value_label.text = str(snapped(value, 0.1))
|
|
save_node_config(sfx_node.name, "volume_db", value)
|
|
)
|
|
|
|
# Stream 信息和控制
|
|
var stream_vbox = VBoxContainer.new()
|
|
controls_hbox.add_child(stream_vbox)
|
|
|
|
var stream_label = Label.new()
|
|
stream_label.text = "音频文件"
|
|
stream_vbox.add_child(stream_label)
|
|
|
|
var stream_hbox = HBoxContainer.new()
|
|
stream_vbox.add_child(stream_hbox)
|
|
|
|
var stream_name_label = Label.new()
|
|
stream_name_label.custom_minimum_size.x = 150
|
|
_set_stream_name_label(sfx_node, stream_name_label)
|
|
stream_hbox.add_child(stream_name_label)
|
|
|
|
# 预览播放按钮
|
|
var preview_button = Button.new()
|
|
preview_button.text = "▶"
|
|
preview_button.custom_minimum_size = Vector2(30, 30)
|
|
preview_button.pressed.connect(preview_audio.bind(sfx_node))
|
|
stream_hbox.add_child(preview_button)
|
|
|
|
# 恢复默认按钮
|
|
if sfx_node.stream_was_replaced():
|
|
var reset_button = Button.new()
|
|
reset_button.text = "🗑️"
|
|
reset_button.custom_minimum_size = Vector2(30, 30)
|
|
reset_button.pressed.connect(reset_audio.bind(sfx_node))
|
|
stream_hbox.add_child(reset_button)
|
|
|
|
# 3. 上传文件按钮
|
|
var upload_button = Button.new()
|
|
upload_button.text = "上传音频"
|
|
upload_button.pressed.connect(open_file_dialog.bind(sfx_node, stream_name_label))
|
|
stream_hbox.add_child(upload_button)
|
|
|
|
# 分割线
|
|
var separator = HSeparator.new()
|
|
item_container.add_child(separator)
|
|
|
|
# 存储节点引用用于后续操作
|
|
item_container.set_meta("sfx_node", sfx_node)
|
|
item_container.set_meta("stream_label", stream_name_label)
|
|
|
|
|
|
func _set_stream_name_label(sfx_node:Sfx, stream_name_label:Label) -> void:
|
|
if sfx_node.stream:
|
|
stream_name_label.text = sfx_node.stream.resource_path.get_file()
|
|
else:
|
|
stream_name_label.text = "无音频文件"
|
|
|
|
|
|
func preview_audio(sfx_node: Sfx):
|
|
if sfx_node.stream:
|
|
preview_player.stream = sfx_node.stream
|
|
preview_player.volume_db = sfx_node.volume_db
|
|
preview_player.play()
|
|
|
|
|
|
func reset_audio(sfx_node: Sfx):
|
|
sfx_node.reset_original_stream()
|
|
config_data.erase(sfx_node.name)
|
|
save_config()
|
|
refresh_ui()
|
|
|
|
|
|
var current_upload_node: Node
|
|
var current_stream_label: Label
|
|
|
|
func open_file_dialog(sfx_node: Node, stream_label: Label):
|
|
current_upload_node = sfx_node
|
|
current_stream_label = stream_label
|
|
file_dialog.popup_centered()
|
|
|
|
|
|
func _on_file_selected(path: String):
|
|
if not current_upload_node:
|
|
return
|
|
copy_and_load_audio_file(path, current_upload_node, current_stream_label)
|
|
|
|
|
|
# 3. 复制文件并加载
|
|
func copy_and_load_audio_file(source_path: String, sfx_node: Node, stream_label: Label):
|
|
var file_name = source_path.get_file()
|
|
var audio_dir = "user://audio/" + current_scene_name + "/"
|
|
var target_path = audio_dir + file_name
|
|
# 确保目录存在
|
|
DirAccess.open("user://").make_dir_recursive("audio/" + current_scene_name)
|
|
# 复制文件
|
|
var source_file = FileAccess.open(source_path, FileAccess.READ)
|
|
if not source_file:
|
|
push_error("无法读取源文件: " + source_path)
|
|
return
|
|
var target_file = FileAccess.open(target_path, FileAccess.WRITE)
|
|
if not target_file:
|
|
push_error("无法创建目标文件: " + target_path)
|
|
source_file.close()
|
|
return
|
|
target_file.store_buffer(source_file.get_buffer(source_file.get_length()))
|
|
source_file.close()
|
|
target_file.close()
|
|
# 加载新的音频流
|
|
var new_stream = AudioLoader.new().loadfile(target_path)
|
|
if new_stream:
|
|
sfx_node.replace_stream(new_stream)
|
|
stream_label.text = file_name
|
|
save_node_config(sfx_node.name, "stream", target_path)
|
|
print("音频文件已更新: ", sfx_node.name, " -> ", file_name)
|
|
else:
|
|
push_error("无法加载音频文件: " + target_path)
|
|
refresh_ui()
|
|
|
|
|
|
# 4. 配置保存与加载
|
|
func save_node_config(node_name: String, property: String, value):
|
|
if not config_data.has(node_name):
|
|
config_data[node_name] = {}
|
|
config_data[node_name][property] = value
|
|
save_config()
|
|
|
|
func save_config():
|
|
var config_dir = "user://audio/" + current_scene_name + "/"
|
|
DirAccess.open("user://").make_dir_recursive("audio/" + current_scene_name)
|
|
|
|
var config_path = config_dir + "audio_config.dat"
|
|
var file = FileAccess.open(config_path, FileAccess.WRITE)
|
|
if file:
|
|
file.store_string(var_to_str(config_data))
|
|
file.close()
|
|
|
|
func load_config():
|
|
var config_path = "user://audio/" + current_scene_name + "/audio_config.dat"
|
|
var file = FileAccess.open(config_path, FileAccess.READ)
|
|
if file:
|
|
var config_str = file.get_as_text()
|
|
file.close()
|
|
config_data = str_to_var(config_str)
|
|
if not config_data:
|
|
config_data = {}
|
|
apply_config()
|
|
else:
|
|
config_data = {}
|
|
|
|
|
|
func apply_config():
|
|
for sfx_node in sfx_nodes:
|
|
if config_data.has(sfx_node.name):
|
|
var node_config = config_data[sfx_node.name]
|
|
if node_config.has("volume_db"):
|
|
sfx_node.volume_db = node_config["volume_db"]
|
|
if node_config.has("stream"):
|
|
var stream_path = node_config["stream"]
|
|
if FileAccess.file_exists(stream_path):
|
|
var new_stream = AudioLoader.new().loadfile(stream_path)
|
|
# var new_stream = load(stream_path)
|
|
if new_stream:
|
|
sfx_node.replace_stream(new_stream)
|
|
|
|
|
|
func _on_reset_pressed() -> void:
|
|
for sfx_node in sfx_nodes:
|
|
sfx_node.reset_original_stream()
|
|
var config_file_path = "user://audio/" + current_scene_name + "/" + "/audio_config.dat"
|
|
if FileAccess.file_exists(config_file_path):
|
|
DirAccess.remove_absolute(config_file_path)
|
|
SceneManager.enter_main_scene()
|
|
|
|
|
|
# 5. Import/Export 功能
|
|
func _on_import_pressed():
|
|
# 打开文件对话框选择导入文件夹或配置文件
|
|
var import_dialog = FileDialog.new()
|
|
import_dialog.size = Vector2(1400, 800)
|
|
import_dialog.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
|
|
import_dialog.content_scale_factor = 3
|
|
import_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
|
import_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
|
import_dialog.dir_selected.connect(_on_import_folder_selected)
|
|
add_child(import_dialog)
|
|
import_dialog.popup_centered()
|
|
|
|
func _on_import_folder_selected(folder_path: String):
|
|
var config_file_path = folder_path + "/audio_config.dat"
|
|
if not FileAccess.file_exists(config_file_path):
|
|
push_error("所选文件夹中没有找到 audio_config.dat 配置文件")
|
|
return
|
|
# 确保目标目录存在
|
|
var target_dir = "user://audio/" + current_scene_name + "/"
|
|
DirAccess.open("user://").make_dir_recursive("audio/" + current_scene_name)
|
|
# 复制配置文件
|
|
copy_file(config_file_path, target_dir + "audio_config.dat")
|
|
# 复制所有音频文件
|
|
var dir = DirAccess.open(folder_path)
|
|
if dir:
|
|
dir.list_dir_begin()
|
|
var file_name = dir.get_next()
|
|
while file_name != "":
|
|
if not dir.current_is_dir():
|
|
var extension = file_name.get_extension().to_lower()
|
|
if extension in ["wav", "ogg", "mp3"]:
|
|
var source_path = folder_path + "/" + file_name
|
|
var target_path = target_dir + file_name
|
|
copy_file(source_path, target_path)
|
|
file_name = dir.get_next()
|
|
dir.list_dir_end()
|
|
# 重新加载配置
|
|
refresh_sfx_list(SceneManager.get_ground())
|
|
print("配置和音频文件已导入")
|
|
|
|
func copy_file(source_path: String, target_path: String) -> bool:
|
|
var source_file = FileAccess.open(source_path, FileAccess.READ)
|
|
if not source_file:
|
|
push_error("无法读取文件: " + source_path)
|
|
return false
|
|
var target_file = FileAccess.open(target_path, FileAccess.WRITE)
|
|
if not target_file:
|
|
push_error("无法创建文件: " + target_path)
|
|
source_file.close()
|
|
return false
|
|
target_file.store_buffer(source_file.get_buffer(source_file.get_length()))
|
|
source_file.close()
|
|
target_file.close()
|
|
return true
|
|
|
|
func _on_export_pressed():
|
|
var desktop_path = OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
|
|
var export_folder = desktop_path + "/" + current_scene_name + "_audio_export"
|
|
var config_path = "user://audio/" + current_scene_name + "/audio_config.dat"
|
|
|
|
# 创建导出文件夹
|
|
var dir = DirAccess.open(desktop_path)
|
|
if not dir:
|
|
push_error("无法访问桌面目录")
|
|
return
|
|
|
|
# 如果文件夹已存在,先删除
|
|
if dir.dir_exists(export_folder):
|
|
remove_directory_recursive(export_folder)
|
|
|
|
dir.make_dir(current_scene_name + "_audio_export")
|
|
|
|
var exported_files = []
|
|
var failed_files = []
|
|
|
|
# 1. 导出配置文件
|
|
if FileAccess.file_exists(config_path):
|
|
var source_file = FileAccess.open(config_path, FileAccess.READ)
|
|
var target_file = FileAccess.open(export_folder + "/audio_config.dat", FileAccess.WRITE)
|
|
|
|
if source_file and target_file:
|
|
target_file.store_buffer(source_file.get_buffer(source_file.get_length()))
|
|
source_file.close()
|
|
target_file.close()
|
|
exported_files.append("audio_config.dat")
|
|
else:
|
|
failed_files.append("audio_config.dat")
|
|
push_error("配置文件导出失败")
|
|
else:
|
|
push_error("没有找到配置文件")
|
|
return
|
|
|
|
# 2. 导出所有关联的音频文件
|
|
var audio_dir = "user://audio/" + current_scene_name + "/"
|
|
|
|
# 从配置数据中收集所有音频文件路径
|
|
var audio_files_to_export = []
|
|
|
|
# 收集配置文件中引用的音频文件
|
|
for node_name in config_data.keys():
|
|
var node_config = config_data[node_name]
|
|
if node_config.has("stream"):
|
|
var stream_path = node_config["stream"]
|
|
if stream_path.begins_with("user://audio/" + current_scene_name + "/"):
|
|
audio_files_to_export.append(stream_path)
|
|
|
|
# 收集当前目录下的所有音频文件
|
|
var audio_dir_access = DirAccess.open(audio_dir)
|
|
if audio_dir_access:
|
|
audio_dir_access.list_dir_begin()
|
|
var file_name = audio_dir_access.get_next()
|
|
|
|
while file_name != "":
|
|
if not audio_dir_access.current_is_dir():
|
|
var full_path = audio_dir + file_name
|
|
var extension = file_name.get_extension().to_lower()
|
|
|
|
# 检查是否是音频文件
|
|
if extension in ["wav", "ogg", "mp3"]:
|
|
if not full_path in audio_files_to_export:
|
|
audio_files_to_export.append(full_path)
|
|
|
|
file_name = audio_dir_access.get_next()
|
|
audio_dir_access.list_dir_end()
|
|
|
|
# 导出音频文件
|
|
for audio_path in audio_files_to_export:
|
|
if FileAccess.file_exists(audio_path):
|
|
var file_name = audio_path.get_file()
|
|
var source_file = FileAccess.open(audio_path, FileAccess.READ)
|
|
var target_file = FileAccess.open(export_folder + "/" + file_name, FileAccess.WRITE)
|
|
|
|
if source_file and target_file:
|
|
target_file.store_buffer(source_file.get_buffer(source_file.get_length()))
|
|
source_file.close()
|
|
target_file.close()
|
|
exported_files.append(file_name)
|
|
else:
|
|
failed_files.append(file_name)
|
|
if source_file:
|
|
source_file.close()
|
|
if target_file:
|
|
target_file.close()
|
|
else:
|
|
failed_files.append(audio_path.get_file() + " (文件不存在)")
|
|
|
|
# 3. 创建导出说明文件
|
|
create_export_readme(export_folder, exported_files, failed_files)
|
|
|
|
# 显示导出结果
|
|
var message = "导出完成!\n导出位置: " + export_folder + "\n"
|
|
message += "成功导出 " + str(exported_files.size()) + " 个文件"
|
|
|
|
if failed_files.size() > 0:
|
|
message += "\n失败 " + str(failed_files.size()) + " 个文件: " + str(failed_files)
|
|
|
|
print(message)
|
|
|
|
# 可选:打开导出文件夹
|
|
OS.shell_open(export_folder)
|
|
|
|
func create_export_readme(export_folder: String, exported_files: Array, failed_files: Array):
|
|
var readme_path = export_folder + "/README.txt"
|
|
var readme_file = FileAccess.open(readme_path, FileAccess.WRITE)
|
|
|
|
if readme_file:
|
|
var content = "音效配置导出说明\n"
|
|
content += "===================\n\n"
|
|
content += "场景名称: " + current_scene_name + "\n"
|
|
content += "导出时间: " + Time.get_datetime_string_from_system() + "\n\n"
|
|
|
|
content += "导出文件清单:\n"
|
|
content += "-----------------\n"
|
|
for file_name in exported_files:
|
|
content += "✓ " + file_name + "\n"
|
|
|
|
if failed_files.size() > 0:
|
|
content += "\n失败文件:\n"
|
|
content += "-----------------\n"
|
|
for file_name in failed_files:
|
|
content += "✗ " + file_name + "\n"
|
|
|
|
content += "\n使用说明:\n"
|
|
content += "-----------------\n"
|
|
content += "1. audio_config.dat 是配置文件,包含音量和文件关联信息\n"
|
|
content += "2. 其他 .wav/.ogg/.mp3 文件是对应的音频资源\n"
|
|
content += "3. 导入时请将所有文件放在同一目录下\n"
|
|
|
|
readme_file.store_string(content)
|
|
readme_file.close()
|
|
|
|
func remove_directory_recursive(path: String):
|
|
var dir = DirAccess.open(path)
|
|
if dir:
|
|
dir.list_dir_begin()
|
|
var file_name = dir.get_next()
|
|
|
|
while file_name != "":
|
|
var full_path = path + "/" + file_name
|
|
|
|
if dir.current_is_dir():
|
|
remove_directory_recursive(full_path)
|
|
else:
|
|
dir.remove(file_name)
|
|
|
|
file_name = dir.get_next()
|
|
|
|
dir.list_dir_end()
|
|
dir.remove(path)
|