diff --git a/addons/global_asset_manager/asset_manager.gd b/addons/global_asset_manager/asset_manager.gd deleted file mode 100644 index a0a0ba0..0000000 --- a/addons/global_asset_manager/asset_manager.gd +++ /dev/null @@ -1,1520 +0,0 @@ -@tool -@icon("res://icon.svg") -@static_unload -class_name AssetManager -extends Control - -signal asset_selected(path: String) -signal db_updated - -enum AssetType { MODEL_3D, IMAGE_2D, AUDIO, SHADER, UNKNOWN } - -const ALL_3D_FORMATS: PackedStringArray = ["glb", "gltf", "fbx"] -const ALL_2D_FORMATS: PackedStringArray = ["png", "jpg", "jpeg", "webp"] -const ALL_AUDIO_FORMATS: PackedStringArray = ["ogg", "mp3", "wav"] -const DB_FILE_PATH: String = "user://asset_database.dat" -# Image formats Godot can import, used to find textures a glTF references under a different extension -const GLTF_TEXTURE_FORMATS: PackedStringArray = ["png", "jpg", "jpeg", "webp", "tga", "bmp", "exr", "hdr", "dds", "ktx", "svg"] -# How far above a glTF's folder to look for dependencies that aren't where the file says they are -const GLTF_SEARCH_PARENT_LEVELS: int = 2 -const GLTF_SEARCH_MAX_FILES: int = 5000 -# Folder (inside the model import folder) that holds dependencies shared between models -const GLTF_SHARED_DIR_NAME: String = "_shared" -# How many frames to keep retrying the post-export filesystem scan while the editor is busy -const SCAN_RETRY_MAX_FRAMES: int = 3600 - -var all_known_tags: Array[String] = [] -var current_selected_path: String = "" -var db: Dictionary = { - "assets": {}, - "folders": [], - "settings": {} -} -var folders_root: TreeItem -var tags_root: TreeItem - -var _active_filter_tags: Array[String] = [] -var _blank_placeholder_tex: ImageTexture -var _current_asset_type: AssetType = AssetType.UNKNOWN -var _current_filter_folder: String = "" -var _current_page: int = 0 -var _folder_context_path: String = "" -var _grid_icon_size: int = 40 -var _grid_population_version: int = 0 -var _is_grid_view: bool = false -var _items_per_page: int = 100 -var _light_mode_idx: int = 0 -var _loaded_3d_node: Node3D = null -var _preview_controller: PreviewController = PreviewController.new() -var _search_query: String = "" -var _selection_update_pending: bool = false -var _tag_display_limit: int = 20 -var _tag_to_delete: String = "" -var _thumbnail_cache: Dictionary = {} -var _max_button_text_length: int = 30 -var _thumbnail_queue: Array[Dictionary] = [] -var _is_processing_thumbnails: bool = false - -@onready var asset_grid: ItemList = $MarginContainer/MainSplit/ContentSplit/CenterPanel/AssetGrid -@onready var audio_player: AudioStreamPlayer = $AudioStreamPlayer -@onready var available_tags_flow_container: HFlowContainer = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/AvailableTagsScroll/AvailableTagsFlowContainer -@onready var file_name_label: Label = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/FileNameLabel -@onready var folder_dialog: FileDialog = $FolderDialog -@onready var light_mode_btn: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox/LightModeBtn -@onready var nav_tree: Tree = $MarginContainer/MainSplit/Sidebar/NavigationTree -@onready var next_page_button: Button = $MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer/NextPageButton -@onready var open_external_button: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons/OpenExternalButton -@onready var open_location_button: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons/OpenLocationButton -@onready var page_label: Label = $MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer/PageLabel -@onready var prev_page_button: Button = $MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer/PrevPageButton -@onready var preview_2d_rect: TextureRect = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/TextureRect -@onready var preview_3d_pivot: Node3D = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer/SubViewport/ModelPivot -@onready var preview_3d_toolbar: MarginContainer = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar -@onready var preview_3d_viewport: SubViewport = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer/SubViewport -@onready var preview_panel_container: PanelContainer = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer -@onready var replay_button: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons/ReplayButton -@onready var reset_origin_btn: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox/ResetOriginBtn -@onready var reset_zoom_btn: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox/ResetZoomBtn -@onready var scan_button: Button = $MarginContainer/MainSplit/Sidebar/ScanButton -@onready var search_input: LineEdit = $MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox/SearchInput -@onready var send_to_project_button: Button = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons/SendToProjectButton -@onready var settings_button: Button = $MarginContainer/MainSplit/Sidebar/SettingsButton -@onready var settings_dialog: AcceptDialog = $SettingsDialog -@onready var tag_context_menu: PopupMenu = $TagContextMenu -@onready var tag_flow_container: HFlowContainer = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/CurrentTagsScroll/TagFlowContainer -@onready var tag_input_field: LineEdit = $MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/TagInputField -@onready var view_mode_button: Button = $MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox/ViewModeButton -@onready var zoom_in_button: Button = $MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox/ZoomInButton -@onready var zoom_out_button: Button = $MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox/ZoomOutButton - -func _ready() -> void: - if Engine.is_editor_hint() and get_parent() == get_tree().root: - return - - var clean_env := Environment.new() - clean_env.background_mode = Environment.BG_COLOR - clean_env.background_color = Color(0.18, 0.18, 0.18) - clean_env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR - clean_env.ambient_light_color = Color(1.0, 1.0, 1.0) - clean_env.ambient_light_energy = 0.4 - clean_env.tonemap_mode = Environment.TONE_MAPPER_LINEAR - clean_env.glow_enabled = false - - var isolated_world := World3D.new() - isolated_world.environment = clean_env - preview_3d_viewport.world_3d = isolated_world - - reset_origin_btn.pressed.connect(func() -> void: _preview_controller.reset_origin()) - reset_zoom_btn.pressed.connect(func() -> void: _preview_controller.reset_zoom()) - light_mode_btn.pressed.connect(_on_light_mode_pressed) - - preview_2d_rect.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - asset_grid.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - - preview_panel_container.clip_contents = true - preview_panel_container.gui_input.connect(_on_preview_gui_input) - preview_3d_viewport.get_parent().mouse_filter = Control.MOUSE_FILTER_IGNORE - preview_2d_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE - - _preview_controller.pivot = preview_3d_pivot - _preview_controller.camera = preview_3d_viewport.get_node("Camera3D") - _preview_controller.texture_rect = preview_2d_rect - - scan_button.pressed.connect(_on_scan_button_pressed) - settings_button.pressed.connect(_on_settings_button_pressed) - folder_dialog.dir_selected.connect(_on_folder_selected) - nav_tree.item_selected.connect(_on_nav_tree_item_selected) - nav_tree.item_edited.connect(_on_nav_tree_item_edited) - nav_tree.item_mouse_selected.connect(_on_nav_tree_item_mouse_selected) - search_input.text_changed.connect(_on_search_text_changed) - asset_grid.multi_selected.connect(_on_asset_grid_multi_selected) - asset_grid.item_selected.connect(_on_asset_grid_item_selected) - prev_page_button.pressed.connect(_on_prev_page_pressed) - next_page_button.pressed.connect(_on_next_page_pressed) - tag_input_field.text_changed.connect(_on_tag_input_text_changed) - tag_input_field.text_submitted.connect(_on_tag_input_submitted) - replay_button.pressed.connect(_on_replay_button_pressed) - open_external_button.pressed.connect(_on_open_external_pressed) - open_location_button.pressed.connect(_on_open_location_pressed) - send_to_project_button.pressed.connect(_on_send_to_project_pressed) - tag_context_menu.id_pressed.connect(_on_tag_context_menu_id_pressed) - audio_player.finished.connect(_on_audio_finished) - - settings_dialog.settings_changed.connect(_on_settings_changed) - settings_dialog.database_cleared.connect(_on_database_cleared) - - zoom_out_button.pressed.connect(_on_zoom_out_pressed) - zoom_in_button.pressed.connect(_on_zoom_in_pressed) - - var blank_img := Image.create_empty(1, 1, false, Image.FORMAT_RGBA8) - _blank_placeholder_tex = ImageTexture.create_from_image(blank_img) - - view_mode_button.pressed.connect(_on_view_mode_toggled) - _apply_view_mode_settings() - - _load_database() - settings_dialog.setup(db) - _update_audio_volume() - _rebuild_nav_tree() - -func add_tag_to_selected_assets(tag_text: String) -> void: - var clean_tag: String = tag_text.strip_edges().to_lower().replace(" ", "_") - if clean_tag.is_empty(): - return - - var selected_items := asset_grid.get_selected_items() - if selected_items.is_empty(): - return - - var modified := false - for idx in selected_items: - var path: String = asset_grid.get_item_metadata(idx) - if not db["assets"].has(path): - db["assets"][path] = {"tags": []} - - var tags: Array = db["assets"][path]["tags"] - if not tags.has(clean_tag): - tags.append(clean_tag) - modified = true - - if modified: - _save_database() - call_deferred("_update_tags_tree") - _update_tag_ui() - -func load_asset_preview(file_path: String) -> void: - current_selected_path = file_path - _current_asset_type = _determine_asset_type(file_path.get_extension()) - file_name_label.text = file_path.get_file() - - _display_preview() - _update_tag_ui() - -func remove_tag_from_selected_assets(tag_text: String) -> void: - var selected_items := asset_grid.get_selected_items() - if selected_items.is_empty(): - return - - var modified := false - for idx in selected_items: - var path: String = asset_grid.get_item_metadata(idx) - if db["assets"].has(path): - var tags: Array = db["assets"][path].get("tags", []) - if tags.has(tag_text): - tags.erase(tag_text) - modified = true - - if modified: - _save_database() - call_deferred("_update_tags_tree") - _update_tag_ui() - - if _active_filter_tags.has(tag_text): - _populate_asset_grid() - _handle_selection_change() - -func scan_directory(path: String) -> void: - if not db["folders"].has(path): - db["folders"].append(path) - - var dir := DirAccess.open(path) - if dir: - _recursive_scan(dir, path, path) - _save_database() - _rebuild_nav_tree() - db_updated.emit() - _select_folder_in_tree(path) - else: - push_error("Failed to open directory: ", path) - -func _apply_view_mode_settings() -> void: - zoom_out_button.visible = _is_grid_view - zoom_in_button.visible = _is_grid_view - - if _is_grid_view: - asset_grid.max_columns = 0 - asset_grid.same_column_width = true - asset_grid.fixed_column_width = _grid_icon_size + 16 - asset_grid.icon_mode = ItemList.ICON_MODE_TOP - asset_grid.fixed_icon_size = Vector2i(_grid_icon_size, _grid_icon_size) - asset_grid.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - asset_grid.add_theme_constant_override("h_separation", 12) - asset_grid.add_theme_constant_override("v_separation", 12) - else: - asset_grid.max_columns = 1 - asset_grid.same_column_width = false - asset_grid.fixed_column_width = 0 - asset_grid.icon_mode = ItemList.ICON_MODE_LEFT - asset_grid.fixed_icon_size = Vector2i(24, 24) - asset_grid.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - asset_grid.add_theme_constant_override("h_separation", 8) - asset_grid.add_theme_constant_override("v_separation", 4) - -func _center_and_scale_3d_model(node: Node3D) -> void: - var meshes: Array[MeshInstance3D] = [] - _find_meshes_recursive(node, meshes) - - if meshes.is_empty(): - return - - var aabb := AABB() - var first := true - - for mi in meshes: - var mi_aabb := mi.get_aabb() - var xform := node.global_transform.affine_inverse() * mi.global_transform - var xformed_aabb := xform * mi_aabb - - if first: - aabb = xformed_aabb - first = false - else: - aabb = aabb.merge(xformed_aabb) - - var max_size: float = maxf(aabb.size.x, maxf(aabb.size.y, aabb.size.z)) - if max_size > 0: - var target_scale: float = 2.0 / max_size - node.scale = Vector3(target_scale, target_scale, target_scale) - - var center_offset: Vector3 = aabb.get_center() * node.scale - node.position = -center_offset - -func _clear_current_preview() -> void: - preview_2d_rect.texture = null - audio_player.stop() - audio_player.stream = null - replay_button.text = "Replay" - - _preview_controller.reset_views() - - if is_instance_valid(_loaded_3d_node): - _loaded_3d_node.queue_free() - _loaded_3d_node = null - -func _convert_24bit_to_16bit(data24: PackedByteArray) -> PackedByteArray: - var num_samples: int = data24.size() / 3 - var data16 := PackedByteArray() - data16.resize(num_samples * 2) - var j: int = 0 - for i in range(0, num_samples * 3, 3): - data16[j] = data24[i + 1] - data16[j + 1] = data24[i + 2] - j += 2 - return data16 - -func _convert_32bit_float_to_16bit(data32: PackedByteArray) -> PackedByteArray: - var num_samples: int = data32.size() / 4 - var data16 := PackedByteArray() - data16.resize(num_samples * 2) - var j: int = 0 - var offset: int = 0 - for i in range(num_samples): - var f: float = data32.decode_float(offset) - offset += 4 - var int_val: int = int(clampf(f, -1.0, 1.0) * 32767.0) - data16[j] = int_val & 0xFF - data16[j + 1] = (int_val >> 8) & 0xFF - j += 2 - return data16 - -func _convert_importer_meshes(node: Node) -> Node: - if not is_instance_valid(node): - return node - - var new_node: Node = node - - if node.get_class() == "ImporterMeshInstance3D": - var mi := MeshInstance3D.new() - mi.name = node.name - if node is Node3D: - mi.transform = node.transform - - var importer_mesh: Variant = node.get("mesh") - if importer_mesh != null and importer_mesh.has_method("get_mesh"): - mi.mesh = importer_mesh.get_mesh() - - var skin: Variant = node.get("skin") - if skin != null: - mi.skin = skin - - var skeleton_path: Variant = node.get("skeleton_path") - if skeleton_path != null: - mi.skeleton = skeleton_path - - var children := node.get_children() - for child in children: - node.remove_child(child) - mi.add_child(child) - - var parent := node.get_parent() - if parent != null: - parent.add_child(mi) - node.queue_free() - else: - node.free() - - new_node = mi - - var current_children := new_node.get_children() - for child in current_children: - _convert_importer_meshes(child) - - return new_node - -func _deferred_selection_update() -> void: - _selection_update_pending = false - var selected := asset_grid.get_selected_items() - - var has_selection := selected.size() > 0 - var is_single := selected.size() == 1 - open_location_button.disabled = not is_single - send_to_project_button.disabled = not has_selection - open_external_button.disabled = not is_single - - if selected.size() == 0: - current_selected_path = "" - file_name_label.text = "No asset selected" - _clear_current_preview() - tag_input_field.editable = false - _update_tag_ui() - - elif selected.size() == 1: - var path: String = asset_grid.get_item_metadata(selected[0]) - tag_input_field.editable = true - load_asset_preview(path) - - else: - current_selected_path = "" - file_name_label.text = str(selected.size()) + " assets selected" - _clear_current_preview() - preview_2d_rect.visible = false - preview_3d_viewport.get_parent().visible = false - replay_button.visible = false - tag_input_field.editable = true - _update_tag_ui() - -func _delete_tag_globally(tag_text: String) -> void: - var modified := false - for path: String in db["assets"].keys(): - var tags: Array = db["assets"][path].get("tags", []) - if tags.has(tag_text): - tags.erase(tag_text) - modified = true - - if _active_filter_tags.has(tag_text): - _active_filter_tags.erase(tag_text) - - if modified: - _save_database() - call_deferred("_update_tags_tree") - _populate_asset_grid() - _handle_selection_change() - _update_tag_ui() - -func _determine_asset_type(extension: String) -> AssetType: - var ext: String = extension.to_lower() - var active_formats: Array = db["settings"].get("active_formats", []) - - if not active_formats.has(ext): - return AssetType.UNKNOWN - - if ext in ALL_3D_FORMATS: - return AssetType.MODEL_3D - elif ext in ALL_2D_FORMATS: - return AssetType.IMAGE_2D - elif ext in ALL_AUDIO_FORMATS: - return AssetType.AUDIO - - return AssetType.UNKNOWN - -func _display_preview() -> void: - preview_3d_viewport.get_parent().visible = _current_asset_type == AssetType.MODEL_3D - preview_3d_toolbar.visible = _current_asset_type == AssetType.MODEL_3D - preview_2d_rect.visible = _current_asset_type == AssetType.IMAGE_2D - replay_button.visible = _current_asset_type == AssetType.AUDIO - - _clear_current_preview() - - if _current_asset_type == AssetType.MODEL_3D: - _load_3d_model(current_selected_path) - elif _current_asset_type == AssetType.IMAGE_2D: - var img: Image = Image.load_from_file(current_selected_path) - if img: - preview_2d_rect.texture = ImageTexture.create_from_image(img) - elif _current_asset_type == AssetType.AUDIO: - _load_audio(current_selected_path) - -# Copies a .gltf and the external buffers and textures it references into dest_dir. -# Dependencies are written before the .gltf, and the .gltf is written once with its final uris, -# so an editor scan that is already running never imports it while its textures are missing. -func _export_gltf(gltf_path: String, dest_dir: String) -> Error: - var dest_gltf := dest_dir.path_join(gltf_path.get_file()) - var json_text := FileAccess.get_file_as_string(gltf_path) - var data: Variant = JSON.parse_string(json_text) - if not data is Dictionary: - push_warning("Could not parse glTF file for dependencies: ", gltf_path) - return DirAccess.copy_absolute(gltf_path, dest_gltf) - - var src_dir := gltf_path.get_base_dir() - # Raw (still URI-encoded) uri strings, as written in the file - var raw_uris: Array[String] = [] - var image_uris: Array[String] = [] - for key in ["buffers", "images"]: - for entry in data.get(key, []): - if entry is Dictionary and entry.has("uri"): - var raw_uri: String = entry["uri"] - # Embedded data needs no copying - if raw_uri.begins_with("data:"): - continue - if not raw_uris.has(raw_uri): - raw_uris.append(raw_uri) - if key == "images": - image_uris.append(raw_uri) - - var uri_replacements := {} - var search_index := {} - - for raw_uri in raw_uris: - var rel_path := raw_uri.uri_decode().simplify_path() - var src_file := rel_path if rel_path.is_absolute_path() else src_dir.path_join(rel_path).simplify_path() - var substituted := false - if not FileAccess.file_exists(src_file): - # Exported asset packs often reference textures that live in another folder or use another format - var found_rel_path := _find_gltf_dependency(src_dir, rel_path.get_file(), image_uris.has(raw_uri), search_index) - if found_rel_path.is_empty(): - push_warning("Missing glTF dependency: ", src_file) - continue - rel_path = found_rel_path - var found_file := src_dir.path_join(rel_path).simplify_path() - print("Global Asset Manager: ", gltf_path.get_file(), " references missing file ", src_file, ", using ", found_file, " instead.") - src_file = found_file - substituted = true - - var is_external := rel_path.is_absolute_path() or rel_path.begins_with("..") - - var dest_rel_path := rel_path - if is_external: - # Files outside the glTF's folder are usually shared by other models in the same pack - dest_rel_path = _get_shared_dependency_path(src_file, dest_dir) - - var dest_file := dest_dir.path_join(dest_rel_path) - # An identical shared file was already imported with another model - var already_shared := is_external and FileAccess.file_exists(dest_file) - if not already_shared: - if not DirAccess.dir_exists_absolute(dest_file.get_base_dir()): - DirAccess.make_dir_recursive_absolute(dest_file.get_base_dir()) - if DirAccess.copy_absolute(src_file, dest_file) != OK: - push_error("Failed to copy glTF dependency: ", src_file) - continue - - if is_external or substituted: - var encoded_segments := PackedStringArray() - for segment in dest_rel_path.split("/"): - encoded_segments.append(segment.uri_encode()) - uri_replacements[raw_uri] = "/".join(encoded_segments) - - if uri_replacements.is_empty(): - return DirAccess.copy_absolute(gltf_path, dest_gltf) - - # Rewrite the uris so the .gltf points at the relocated files - var dest_text := json_text - var unresolved: Array[String] = [] - for raw_uri in uri_replacements: - var new_uri: String = uri_replacements[raw_uri] - var quoted := JSON.stringify(raw_uri) - # JSON writers may escape forward slashes - var quoted_escaped := quoted.replace("/", "\\/") - if dest_text.contains(quoted): - dest_text = dest_text.replace(quoted, JSON.stringify(new_uri)) - elif dest_text.contains(quoted_escaped): - dest_text = dest_text.replace(quoted_escaped, JSON.stringify(new_uri)) - else: - unresolved.append(raw_uri) - - var file := FileAccess.open(dest_gltf, FileAccess.WRITE) - if not file: - return FileAccess.get_open_error() - file.store_string(dest_text) - file.close() - - var shared_path := dest_dir.path_join(GLTF_SHARED_DIR_NAME) - if unresolved.is_empty(): - print("Global Asset Manager: files referenced outside the folder of ", gltf_path.get_file(), " are in ", shared_path, " and the glTF was updated to use them.") - else: - push_warning("Global Asset Manager: files referenced outside the folder of ", gltf_path.get_file(), " are in ", shared_path, - ", but these references in the glTF could not be updated: ", ", ".join(unresolved), - ". You may need to reconnect them manually in Godot.") - return OK - -# Returns where src_file belongs in the shared folder, relative to dest_dir. -# Reuses an identical file already there; a different file with the same name gets a content-hash suffix. -func _get_shared_dependency_path(src_file: String, dest_dir: String) -> String: - var file_name := src_file.get_file() - var shared_path := GLTF_SHARED_DIR_NAME.path_join(file_name) - var existing_file := dest_dir.path_join(shared_path) - if not FileAccess.file_exists(existing_file): - return shared_path - - var src_md5 := FileAccess.get_md5(src_file) - if FileAccess.get_md5(existing_file) == src_md5: - return shared_path - - var hashed_name := file_name.get_basename() + "_" + src_md5.left(8) - if not file_name.get_extension().is_empty(): - hashed_name += "." + file_name.get_extension() - return GLTF_SHARED_DIR_NAME.path_join(hashed_name) - -# Returns the path of a file matching file_name, relative to src_dir, or "" if none is found. -# Searches src_dir and then its parents; image references also match other image extensions. -func _find_gltf_dependency(src_dir: String, file_name: String, is_image: bool, search_index: Dictionary) -> String: - var wanted_name := file_name.get_basename().to_lower() - var wanted_ext := file_name.get_extension().to_lower() - var root := src_dir - - for level in GLTF_SEARCH_PARENT_LEVELS + 1: - if not search_index.has(level): - var files: Array[String] = [] - _collect_files_recursive(root, "", files) - search_index[level] = files - - var match_path := "" - for rel_file: String in search_index[level]: - if rel_file.get_file().get_basename().to_lower() != wanted_name: - continue - var ext := rel_file.get_extension().to_lower() - if ext == wanted_ext: - match_path = rel_file - break - if is_image and match_path.is_empty() and GLTF_TEXTURE_FORMATS.has(ext): - match_path = rel_file - - if not match_path.is_empty(): - return "../".repeat(level) + match_path - - var parent := root.get_base_dir() - if parent == root: - break - root = parent - - return "" - -func _collect_files_recursive(dir_path: String, rel_dir: String, result: Array[String]) -> void: - var dir := DirAccess.open(dir_path.path_join(rel_dir)) - if not dir: - return - for file_name in dir.get_files(): - if result.size() >= GLTF_SEARCH_MAX_FILES: - return - if file_name.get_extension() != "import": - result.append(rel_dir.path_join(file_name)) - for sub_dir in dir.get_directories(): - if not sub_dir.begins_with("."): - _collect_files_recursive(dir_path, rel_dir.path_join(sub_dir), result) - -func _export_selected_to_project() -> void: - var selected_items := asset_grid.get_selected_items() - var export_count := 0 - var tags_modified := false - - for idx in selected_items: - var path: String = asset_grid.get_item_metadata(idx) - if db["assets"].has(path): - var asset_type: int = db["assets"][path].get("type", AssetType.UNKNOWN) - var dest_dir := "res://assets/misc" - - match asset_type: - AssetType.MODEL_3D: dest_dir = db["settings"].get("import_path_3d", "res://assets/models") - AssetType.IMAGE_2D: dest_dir = db["settings"].get("import_path_2d", "res://assets/images") - AssetType.AUDIO: dest_dir = db["settings"].get("import_path_audio", "res://assets/audio") - AssetType.SHADER: dest_dir = db["settings"].get("import_path_shader", "res://assets/shaders") - - if not DirAccess.dir_exists_absolute(dest_dir): - DirAccess.make_dir_recursive_absolute(dest_dir) - - var err: Error - # .gltf files reference external buffers (.bin) and textures that must be copied too - if path.get_extension().to_lower() == "gltf": - err = _export_gltf(path, dest_dir) - else: - err = DirAccess.copy_absolute(path, dest_dir + "/" + path.get_file()) - if err == OK: - export_count += 1 - - # Automatically apply the "imported" tag - var tags: Array = db["assets"][path].get("tags", []) - if not tags.has("imported"): - tags.append("imported") - db["assets"][path]["tags"] = tags - tags_modified = true - else: - push_error("Failed to copy file: ", path) - - if export_count > 0: - if tags_modified: - _save_database() - call_deferred("_update_tags_tree") - _update_tag_ui() - - if Engine.is_editor_hint(): - _scan_when_idle() - var original_text := send_to_project_button.text - send_to_project_button.text = "Exported " + str(export_count) + " file(s)!" - get_tree().create_timer(2.0).timeout.connect(func() -> void: send_to_project_button.text = original_text) - -# scan() is silently ignored while the editor is already scanning (including the frames after a -# scan's thread ends but before its results are applied), and that scan may have started before the -# exported files were written. Retry until a scan of our own actually starts. -func _scan_when_idle() -> void: - var fs := EditorInterface.get_resource_filesystem() - for attempt in SCAN_RETRY_MAX_FRAMES: - if not fs.is_scanning() and not fs.is_importing(): - fs.scan() - if fs.is_scanning(): - return - await get_tree().process_frame - push_warning("Global Asset Manager: the editor stayed busy, so exported files will be imported on its next filesystem scan.") - -func _filter_by_tag(tag_text: String) -> void: - if not _active_filter_tags.has(tag_text): - _active_filter_tags.append(tag_text) - _current_page = 0 - _populate_asset_grid() - _handle_selection_change() - call_deferred("_update_tags_tree") - -func _find_meshes_recursive(node: Node, result: Array[MeshInstance3D]) -> void: - if node is MeshInstance3D: - result.append(node) - for child in node.get_children(): - _find_meshes_recursive(child, result) - -func _get_placeholder_icon(asset_type: int) -> Texture2D: - if not Engine.is_editor_hint(): - return _blank_placeholder_tex - - var icon_name := "File" - match asset_type: - AssetType.MODEL_3D: icon_name = "MeshInstance3D" - AssetType.AUDIO: icon_name = "AudioStreamPlayer" - AssetType.SHADER: icon_name = "Shader" - - var theme := EditorInterface.get_editor_theme() - if theme and theme.has_icon(icon_name, "EditorIcons"): - return theme.get_icon(icon_name, "EditorIcons") - - return _blank_placeholder_tex - -func _get_shared_tags() -> Array[String]: - var selected := asset_grid.get_selected_items() - if selected.is_empty(): - return [] - - var path0: String = asset_grid.get_item_metadata(selected[0]) - if not db["assets"].has(path0): - return [] - - var shared_tags: Array = db["assets"][path0].get("tags", []).duplicate() - - for i in range(1, selected.size()): - var path: String = asset_grid.get_item_metadata(selected[i]) - if db["assets"].has(path): - var current_tags: Array = db["assets"][path].get("tags", []) - var intersection: Array = [] - for tag in shared_tags: - if current_tags.has(tag): - intersection.append(tag) - shared_tags = intersection - else: - return [] - - var result: Array[String] = [] - result.assign(shared_tags) - return result - -func _queue_thumbnail(path: String, item_idx: int, version: int) -> void: - _thumbnail_queue.append({"path": path, "idx": item_idx, "version": version}) - if not _is_processing_thumbnails: - _process_thumbnail_queue() - -func _process_thumbnail_queue() -> void: - _is_processing_thumbnails = true - while _thumbnail_queue.size() > 0: - var req: Dictionary = _thumbnail_queue.pop_front() - - # Abort if the user typed a new search or changed folders while this was loading - if req["version"] != _grid_population_version: - continue - - var path: String = req["path"] - var item_idx: int = req["idx"] - - if _thumbnail_cache.has(path): - if item_idx < asset_grid.item_count and asset_grid.get_item_metadata(item_idx) == path: - asset_grid.set_item_icon(item_idx, _thumbnail_cache[path]) - continue - - var img := Image.load_from_file(path) - if img: - var size := img.get_size() - var max_dim := maxf(size.x, size.y) - if max_dim > 64: - var scale := 64.0 / max_dim - img.resize(int(size.x * scale), int(size.y * scale), Image.INTERPOLATE_NEAREST) - - var tex := ImageTexture.create_from_image(img) - _thumbnail_cache[path] = tex - - if req["version"] == _grid_population_version and item_idx < asset_grid.item_count and asset_grid.get_item_metadata(item_idx) == path: - asset_grid.set_item_icon(item_idx, tex) - - await get_tree().process_frame - - _is_processing_thumbnails = false - -func _get_type_tag(type: AssetType) -> String: - match type: - AssetType.MODEL_3D: return "3d_model" - AssetType.IMAGE_2D: return "2d_image" - AssetType.AUDIO: return "audio" - AssetType.SHADER: return "shader" - _: return "unknown" - -func _handle_selection_change() -> void: - if _selection_update_pending: - return - _selection_update_pending = true - call_deferred("_deferred_selection_update") - -func _load_3d_model(path: String) -> void: - var ext := path.get_extension().to_lower() - var state := GLTFState.new() - # The editor default extracts embedded textures next to the source file and imports them, - # which writes into the asset library; keep them in memory for the preview instead - state.handle_binary_image = GLTFState.HANDLE_BINARY_EMBED_AS_UNCOMPRESSED - var err: int = FAILED - - if ext == "fbx": - var fbx := FBXDocument.new() - err = fbx.append_from_file(path, state) - if err == OK: - _loaded_3d_node = fbx.generate_scene(state) - else: - var gltf := GLTFDocument.new() - err = gltf.append_from_file(path, state) - if err == OK: - _loaded_3d_node = gltf.generate_scene(state) - - if err == OK and _loaded_3d_node: - preview_3d_pivot.add_child(_loaded_3d_node) - _loaded_3d_node = _convert_importer_meshes(_loaded_3d_node) - _center_and_scale_3d_model(_loaded_3d_node) - else: - file_name_label.text += " (Failed to Parse 3D Model)" - -func _load_audio(path: String) -> void: - var ext := path.get_extension().to_lower() - - if ext == "wav": - audio_player.stream = _load_wav_from_file(path) - if audio_player.stream: - audio_player.play() - replay_button.text = "Stop" - return - - var file := FileAccess.open(path, FileAccess.READ) - if not file: - return - - var buffer := file.get_buffer(file.get_length()) - - if ext == "ogg": - var stream := AudioStreamOggVorbis.load_from_buffer(buffer) - audio_player.stream = stream - audio_player.play() - replay_button.text = "Stop" - elif ext == "mp3": - var stream := AudioStreamMP3.new() - stream.data = buffer - audio_player.stream = stream - audio_player.play() - replay_button.text = "Stop" - -func _load_database() -> void: - if FileAccess.file_exists(DB_FILE_PATH): - # Load the fast binary format - var file := FileAccess.open(DB_FILE_PATH, FileAccess.READ) - if file: - var data: Variant = file.get_var() - if data is Dictionary: - if data.has("assets"): db["assets"] = data["assets"] - if data.has("settings"): db["settings"] = data["settings"] - if data.has("folders"): - var parsed_folders: Variant = data["folders"] - if parsed_folders is Array: - db["folders"] = parsed_folders - elif parsed_folders is Dictionary: - db["folders"] = parsed_folders.keys() - elif FileAccess.file_exists("user://asset_database.json"): - # Legacy Migration: If the old JSON exists, load it and upgrade it to binary - var file := FileAccess.open("user://asset_database.json", FileAccess.READ) - var json_string := file.get_as_text() - var parsed: Variant = JSON.parse_string(json_string) - if parsed and parsed is Dictionary: - if parsed.has("assets"): db["assets"] = parsed["assets"] - if parsed.has("settings"): db["settings"] = parsed["settings"] - if parsed.has("folders"): - var parsed_folders: Variant = parsed["folders"] - if parsed_folders is Array: - db["folders"] = parsed_folders - elif parsed_folders is Dictionary: - db["folders"] = parsed_folders.keys() - _save_database() # Instantly saves the data into the new .dat format - - # Default Settings Checks - if not db["settings"].has("grid_icon_size"): - db["settings"]["grid_icon_size"] = 40 - if not db["settings"].has("wav_preview_length"): - db["settings"]["wav_preview_length"] = 10 - - _grid_icon_size = db["settings"]["grid_icon_size"] - -func _load_wav_from_file(path: String) -> AudioStreamWAV: - var file := FileAccess.open(path, FileAccess.READ) - if not file: return null - - var stream := AudioStreamWAV.new() - - var riff := file.get_buffer(4).get_string_from_ascii() - if riff != "RIFF": return null - - file.get_32() - - var wave := file.get_buffer(4).get_string_from_ascii() - if wave != "WAVE": return null - - var format_found := false - var data_found := false - - var format_tag: int = 0 - var bits_per_sample: int = 0 - var sample_rate: int = 0 - var channels: int = 0 - - while file.get_position() < file.get_length() and not data_found: - var chunk_id := file.get_buffer(4).get_string_from_ascii() - if chunk_id.length() < 4: - break - - var chunk_size := file.get_32() - var next_chunk_pos := file.get_position() + chunk_size + (chunk_size % 2) - - if chunk_id == "fmt ": - format_tag = file.get_16() - channels = file.get_16() - sample_rate = file.get_32() - file.get_32() - file.get_16() - bits_per_sample = file.get_16() - - stream.mix_rate = sample_rate - stream.stereo = (channels == 2) - - if format_tag == 1: - if bits_per_sample == 8: - stream.format = AudioStreamWAV.FORMAT_8_BITS - elif bits_per_sample == 16 or bits_per_sample == 24: - stream.format = AudioStreamWAV.FORMAT_16_BITS - else: - push_error("Unsupported WAV bit depth: ", bits_per_sample, " in ", path) - return null - elif format_tag == 3 and bits_per_sample == 32: - stream.format = AudioStreamWAV.FORMAT_16_BITS - else: - push_error("Unsupported WAV format tag: ", format_tag, " in ", path) - return null - - format_found = true - - elif chunk_id == "data": - var preview_length: int = db["settings"].get("wav_preview_length", 10) - var bytes_per_sec: int = sample_rate * channels * (bits_per_sample / 8) - var max_preview_bytes: int = bytes_per_sec * preview_length - var bytes_to_read: int = mini(chunk_size, max_preview_bytes) - - var raw_data := file.get_buffer(bytes_to_read) - if format_tag == 1 and bits_per_sample == 24: - stream.data = _convert_24bit_to_16bit(raw_data) - elif format_tag == 3 and bits_per_sample == 32: - stream.data = _convert_32bit_float_to_16bit(raw_data) - else: - stream.data = raw_data - data_found = true - - file.seek(next_chunk_pos) - - if not format_found or not data_found: - push_error("Could not find valid fmt/data chunks in WAV: ", path) - return null - - return stream - -func _on_asset_grid_item_selected(_index: int) -> void: - _handle_selection_change() - -func _on_asset_grid_multi_selected(_index: int, _selected: bool) -> void: - _handle_selection_change() - -func _on_audio_finished() -> void: - replay_button.text = "Replay" - -func _on_database_cleared() -> void: - db["assets"].clear() - db["folders"].clear() - _save_database() - - all_known_tags.clear() - _active_filter_tags.clear() - _current_filter_folder = "" - _search_query = "" - search_input.clear() - - _rebuild_nav_tree() - _populate_asset_grid() - _handle_selection_change() - -func _on_folder_selected(path: String) -> void: - scan_directory(path) - -func _on_light_mode_pressed() -> void: - _light_mode_idx = (_light_mode_idx + 1) % 3 - match _light_mode_idx: - 0: - light_mode_btn.text = "Lit" - preview_3d_viewport.debug_draw = Viewport.DEBUG_DRAW_DISABLED - 1: - light_mode_btn.text = "Unlit" - preview_3d_viewport.debug_draw = Viewport.DEBUG_DRAW_UNSHADED - 2: - light_mode_btn.text = "Wireframe" - preview_3d_viewport.debug_draw = Viewport.DEBUG_DRAW_WIREFRAME - -func _on_nav_tree_item_edited() -> void: - var item := nav_tree.get_edited() - var meta: Variant = item.get_metadata(0) - if meta and meta is Dictionary and meta.get("type") == "tag": - var tag_val: String = meta.get("value", "") - var is_checked: bool = item.is_checked(0) - if is_checked and not _active_filter_tags.has(tag_val): - _active_filter_tags.append(tag_val) - elif not is_checked and _active_filter_tags.has(tag_val): - _active_filter_tags.erase(tag_val) - - _current_page = 0 - _populate_asset_grid() - _handle_selection_change() - call_deferred("_update_tags_tree") - -func _on_nav_tree_item_mouse_selected(position: Vector2, mouse_button_index: int) -> void: - if mouse_button_index == MOUSE_BUTTON_RIGHT: - var item := nav_tree.get_item_at_position(position) - if item: - var meta: Variant = item.get_metadata(0) - if meta and meta is Dictionary: - tag_context_menu.clear() - if meta.get("type") == "tag": - item.deselect(0) - _select_folder_in_tree(_current_filter_folder) - _tag_to_delete = meta.get("value", "") - tag_context_menu.add_item("Delete Tag Globally", 0) - tag_context_menu.position = DisplayServer.mouse_get_position() - tag_context_menu.popup() - elif meta.get("type") == "folder": - item.select(0) - _folder_context_path = meta.get("path", "") - tag_context_menu.add_item("Rescan Folder", 1) - tag_context_menu.add_item("Remove Folder", 2) - tag_context_menu.position = DisplayServer.mouse_get_position() - tag_context_menu.popup() - -func _on_nav_tree_item_selected() -> void: - var selected := nav_tree.get_selected() - if not selected: - return - - var meta: Variant = selected.get_metadata(0) - if meta and meta is Dictionary: - if meta.get("type") == "folder": - var new_path: String = meta.get("path", "") - if _current_filter_folder == new_path: - return - - _current_filter_folder = new_path - _current_page = 0 - _tag_display_limit = 20 - _populate_asset_grid() - _handle_selection_change() - call_deferred("_update_tags_tree") - - elif meta.get("type") == "folder_root": - if _current_filter_folder == "": - return - - _current_filter_folder = "" - _current_page = 0 - _tag_display_limit = 20 - _populate_asset_grid() - _handle_selection_change() - call_deferred("_update_tags_tree") - - elif meta.get("type") == "load_more": - _tag_display_limit += 20 - selected.deselect(0) - _select_folder_in_tree(_current_filter_folder) - call_deferred("_update_tags_tree") - - elif meta.get("type") == "tag": - selected.deselect(0) - _select_folder_in_tree(_current_filter_folder) - else: - _select_folder_in_tree(_current_filter_folder) - -func _on_next_page_pressed() -> void: - _current_page += 1 - _populate_asset_grid() - _handle_selection_change() - -func _on_open_external_pressed() -> void: - if not current_selected_path.is_empty(): - OS.shell_open(current_selected_path) - -func _on_open_location_pressed() -> void: - var selected_items := asset_grid.get_selected_items() - for idx in selected_items: - var path: String = asset_grid.get_item_metadata(idx) - OS.shell_show_in_file_manager(path) - -func _on_prev_page_pressed() -> void: - _current_page -= 1 - _populate_asset_grid() - _handle_selection_change() - -func _on_preview_gui_input(event: InputEvent) -> void: - if _current_asset_type == AssetType.MODEL_3D: - _preview_controller.handle_3d_input(event) - elif _current_asset_type == AssetType.IMAGE_2D: - _preview_controller.handle_2d_input(event) - -func _on_replay_button_pressed() -> void: - if audio_player.playing: - audio_player.stop() - replay_button.text = "Replay" - elif audio_player.stream: - audio_player.play() - replay_button.text = "Stop" - -func _on_scan_button_pressed() -> void: - folder_dialog.popup_centered() - -func _on_search_text_changed(new_text: String) -> void: - _search_query = new_text - _current_page = 0 - _populate_asset_grid() - _handle_selection_change() - -func _on_send_to_project_pressed() -> void: - _export_selected_to_project() - -func _on_settings_button_pressed() -> void: - settings_dialog.popup_centered() - -func _on_settings_changed() -> void: - _update_audio_volume() - _populate_asset_grid() - -func _on_tag_context_menu_id_pressed(id: int) -> void: - if id == 0 and not _tag_to_delete.is_empty(): - _delete_tag_globally(_tag_to_delete) - elif id == 1 and not _folder_context_path.is_empty(): - _rescan_folder(_folder_context_path) - elif id == 2 and not _folder_context_path.is_empty(): - _remove_folder(_folder_context_path) - -func _on_tag_input_submitted(new_text: String) -> void: - tag_input_field.clear() - add_tag_to_selected_assets(new_text) - _update_tag_ui() - -func _on_tag_input_text_changed(_new_text: String) -> void: - _update_tag_ui() - -func _on_view_mode_toggled() -> void: - _is_grid_view = not _is_grid_view - view_mode_button.text = "View: Grid" if _is_grid_view else "View: List" - _apply_view_mode_settings() - _populate_asset_grid() - -func _on_zoom_in_pressed() -> void: - _grid_icon_size = mini(256, _grid_icon_size + 8) - db["settings"]["grid_icon_size"] = _grid_icon_size - _save_database() - _apply_view_mode_settings() - -func _on_zoom_out_pressed() -> void: - _grid_icon_size = maxi(16, _grid_icon_size - 8) - db["settings"]["grid_icon_size"] = _grid_icon_size - _save_database() - _apply_view_mode_settings() - -func _populate_asset_grid() -> void: - asset_grid.clear() - _grid_population_version += 1 - var current_version := _grid_population_version - - var matching_paths: Array[String] = [] - - var is_exact_search := false - var actual_search_query := _search_query - if _search_query.length() >= 2 and _search_query.begins_with("\"") and _search_query.ends_with("\""): - is_exact_search = true - actual_search_query = _search_query.substr(1, _search_query.length() - 2) - - for path: String in db["assets"].keys(): - var data: Dictionary = db["assets"][path] - var should_add := true - - if not _current_filter_folder.is_empty() and not path.begins_with(_current_filter_folder): - should_add = false - - if should_add and not _active_filter_tags.is_empty(): - var asset_tags: Array = data.get("tags", []) - for t in _active_filter_tags: - if not asset_tags.has(t): - should_add = false - break - - if should_add and not actual_search_query.is_empty(): - var filename := path.get_file() - if is_exact_search: - if filename.findn(actual_search_query) == -1: - should_add = false - else: - if not actual_search_query.is_subsequence_ofn(filename): - should_add = false - - if should_add: - matching_paths.append(path) - - if not actual_search_query.is_empty(): - matching_paths.sort_custom(func(a: String, b: String) -> bool: - var file_a := a.get_file() - var file_b := b.get_file() - var exact_a := file_a.findn(actual_search_query) != -1 - var exact_b := file_b.findn(actual_search_query) != -1 - - if exact_a != exact_b: - return exact_a - return file_a.nocasecmp_to(file_b) < 0 - ) - else: - matching_paths.sort_custom(func(a: String, b: String) -> bool: - return a.get_file().nocasecmp_to(b.get_file()) < 0 - ) - - var total_pages: int = maxi(1, ceili(matching_paths.size() / float(_items_per_page))) - _current_page = clampi(_current_page, 0, total_pages - 1) - - page_label.text = "Page " + str(_current_page + 1) + " of " + str(total_pages) - prev_page_button.disabled = _current_page == 0 - next_page_button.disabled = _current_page >= total_pages - 1 - - var start_idx: int = _current_page * _items_per_page - var end_idx: int = mini(start_idx + _items_per_page, matching_paths.size()) - - var items_added := 0 - - for i in range(start_idx, end_idx): - if current_version != _grid_population_version: - return - - var path := matching_paths[i] - var filename := path.get_file() - var asset_type: int = db["assets"][path].get("type", AssetType.UNKNOWN) - - var item_icon := _get_placeholder_icon(asset_type) - var idx := asset_grid.add_item(filename, item_icon) - asset_grid.set_item_metadata(idx, path) - asset_grid.set_item_tooltip(idx, path) - - if asset_type == AssetType.IMAGE_2D: - _queue_thumbnail(path, idx, current_version) - - items_added += 1 - if items_added % 200 == 0: - await get_tree().process_frame - -func _rebuild_nav_tree() -> void: - nav_tree.clear() - var root := nav_tree.create_item() - - folders_root = nav_tree.create_item(root) - folders_root.set_text(0, "Scanned Folders") - folders_root.set_metadata(0, {"type": "folder_root"}) - for folder: String in db["folders"]: - var f_item := nav_tree.create_item(folders_root) - f_item.set_text(0, folder.get_file()) - f_item.set_metadata(0, {"type": "folder", "path": folder}) - - tags_root = nav_tree.create_item(root) - tags_root.set_text(0, "Tags") - - _update_tags_tree() - _select_folder_in_tree(_current_filter_folder) - -func _recursive_scan(dir: DirAccess, current_path: String, root_path: String) -> void: - dir.list_dir_begin() - var file_name := dir.get_next() - while file_name != "": - if dir.current_is_dir(): - if file_name != "." and file_name != "..": - var next_dir_path := current_path + "/" + file_name - var next_dir := DirAccess.open(next_dir_path) - if next_dir: - _recursive_scan(next_dir, next_dir_path, root_path) - else: - var ext := file_name.get_extension().to_lower() - var full_path := current_path + "/" + file_name - var type := _determine_asset_type(ext) - - if type != AssetType.UNKNOWN: - if not db["assets"].has(full_path): - var base_tag := _get_type_tag(type) - var initial_tags: Array[String] = [] - initial_tags.append(base_tag) - - var root_folder_name := root_path.get_file().strip_edges().to_lower().replace(" ", "_") - if not root_folder_name.is_empty() and not initial_tags.has(root_folder_name): - initial_tags.append(root_folder_name) - - var relative_dir := current_path.trim_prefix(root_path) - if relative_dir.begins_with("/"): - relative_dir = relative_dir.substr(1) - - if not relative_dir.is_empty(): - var folder_parts := relative_dir.split("/") - for part in folder_parts: - var clean_part := part.strip_edges().to_lower().replace(" ", "_") - if not clean_part.is_empty() and not initial_tags.has(clean_part): - initial_tags.append(clean_part) - - db["assets"][full_path] = { - "tags": initial_tags, - "type": type - } - file_name = dir.get_next() - dir.list_dir_end() - -func _remove_folder(folder_path: String) -> void: - if db["folders"].has(folder_path): - db["folders"].erase(folder_path) - - var paths_to_remove: Array[String] = [] - for asset_path in db["assets"].keys(): - if asset_path.begins_with(folder_path): - paths_to_remove.append(asset_path) - - for p in paths_to_remove: - db["assets"].erase(p) - - if _current_filter_folder == folder_path: - _current_filter_folder = "" - - _save_database() - _rebuild_nav_tree() - _populate_asset_grid() - _handle_selection_change() - -func _rescan_folder(folder_path: String) -> void: - var dir := DirAccess.open(folder_path) - if dir: - _recursive_scan(dir, folder_path, folder_path) - - var paths_to_remove: Array[String] = [] - for asset_path in db["assets"].keys(): - if asset_path.begins_with(folder_path): - if not FileAccess.file_exists(asset_path): - paths_to_remove.append(asset_path) - - for p in paths_to_remove: - db["assets"].erase(p) - - _save_database() - _rebuild_nav_tree() - _populate_asset_grid() - db_updated.emit() - -func _save_database() -> void: - var file := FileAccess.open(DB_FILE_PATH, FileAccess.WRITE) - if file: - file.store_var(db) - -func _select_folder_in_tree(folder_path: String) -> void: - var root := nav_tree.get_root() - if not root or root.get_child_count() == 0: - return - - var target_root := root.get_child(0) - if folder_path.is_empty(): - target_root.select(0) - return - - for i in range(target_root.get_child_count()): - var item := target_root.get_child(i) - var meta: Variant = item.get_metadata(0) - if meta and meta is Dictionary and meta.get("path") == folder_path: - item.select(0) - nav_tree.scroll_to_item(item) - return - - target_root.select(0) - _current_filter_folder = "" - -func _update_audio_volume() -> void: - var val: float = db["settings"].get("volume", 50.0) - if val <= 0: - audio_player.volume_db = -80.0 - else: - audio_player.volume_db = linear_to_db(val / 100.0) - -func _update_tag_ui() -> void: - for child in tag_flow_container.get_children(): - child.queue_free() - for child in available_tags_flow_container.get_children(): - child.queue_free() - - var selected_items := asset_grid.get_selected_items() - if selected_items.is_empty(): - return - - var shared_tags: Array[String] = _get_shared_tags() - - for tag in shared_tags: - var btn := Button.new() - if tag.length() > _max_button_text_length: - btn.text = tag.substr(0, _max_button_text_length) + "..." + " (x)" - else: - btn.text = tag + " (x)" - var captured_tag := tag - btn.pressed.connect(func() -> void: remove_tag_from_selected_assets(captured_tag)) - btn.gui_input.connect(func(event: InputEvent) -> void: - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: - _filter_by_tag(captured_tag) - ) - btn.tooltip_text = tag + "\nLeft-Click: Remove tag\nRight-Click: Filter by tag" - tag_flow_container.add_child(btn) - - var search_text := tag_input_field.text.strip_edges().to_lower().replace(" ", "_") - var available_tags: Array[String] = [] - - for tag in all_known_tags: - if not shared_tags.has(tag): - if search_text.is_empty() or search_text.is_subsequence_ofn(tag): - available_tags.append(tag) - - if not search_text.is_empty(): - available_tags.sort_custom(func(a: String, b: String) -> bool: - var exact_a := a.findn(search_text) != -1 - var exact_b := b.findn(search_text) != -1 - if exact_a != exact_b: - return exact_a - return a.nocasecmp_to(b) < 0 - ) - - for tag in available_tags: - var btn := Button.new() - if tag.length() > _max_button_text_length: - btn.text = tag.substr(0, _max_button_text_length) + "..." + " (+)" - else: - btn.text = tag + " (+)" - var captured_tag := tag - btn.pressed.connect(func() -> void: add_tag_to_selected_assets(captured_tag)) - btn.tooltip_text = tag - available_tags_flow_container.add_child(btn) - -func _update_tags_tree() -> void: - if not is_instance_valid(tags_root): - return - - var child := tags_root.get_first_child() - while child: - var next := child.get_next() - child.free() - child = next - - all_known_tags.clear() - var tag_counts := {} - - for path: String in db["assets"].keys(): - var data: Dictionary = db["assets"][path] - var should_add := true - - if not _current_filter_folder.is_empty() and not path.begins_with(_current_filter_folder): - should_add = false - - if should_add and not _active_filter_tags.is_empty(): - var asset_tags: Array = data.get("tags", []) - for t in _active_filter_tags: - if not asset_tags.has(t): - should_add = false - break - - if should_add: - var asset_tags: Array = data.get("tags", []) - for tag: String in asset_tags: - tag_counts[tag] = tag_counts.get(tag, 0) + 1 - - for asset_data: Dictionary in db["assets"].values(): - for tag: String in asset_data.get("tags", []): - if not all_known_tags.has(tag): - all_known_tags.append(tag) - - var display_tags: Array[String] = [] - for tag in tag_counts.keys(): - display_tags.append(tag) - - for active_tag in _active_filter_tags: - if not display_tags.has(active_tag): - display_tags.append(active_tag) - tag_counts[active_tag] = 0 - - var unselected_tags: Array[String] = [] - for tag in display_tags: - if not _active_filter_tags.has(tag): - unselected_tags.append(tag) - - unselected_tags.sort_custom(func(a: String, b: String) -> bool: - if tag_counts.get(a, 0) == tag_counts.get(b, 0): - return a < b - return tag_counts.get(a, 0) > tag_counts.get(b, 0) - ) - - var final_ordered_tags: Array[String] = [] - final_ordered_tags.append_array(_active_filter_tags) - final_ordered_tags.append_array(unselected_tags) - - var count := 0 - for tag in final_ordered_tags: - if count >= _tag_display_limit: - var more_item := nav_tree.create_item(tags_root) - more_item.set_text(0, "Load More (+20)...") - more_item.set_metadata(0, {"type": "load_more"}) - break - - var t_item := nav_tree.create_item(tags_root) - t_item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK) - var t_count: int = tag_counts.get(tag, 0) - t_item.set_text(0, "#" + tag + " (" + str(t_count) + ")") - t_item.set_editable(0, true) - t_item.set_checked(0, _active_filter_tags.has(tag)) - t_item.set_metadata(0, {"type": "tag", "value": tag}) - - count += 1 diff --git a/addons/global_asset_manager/asset_manager.gd.uid b/addons/global_asset_manager/asset_manager.gd.uid deleted file mode 100644 index ed254ca..0000000 --- a/addons/global_asset_manager/asset_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bhxhjvggmpfr6 diff --git a/addons/global_asset_manager/asset_manager.tscn b/addons/global_asset_manager/asset_manager.tscn deleted file mode 100644 index 97ff09b..0000000 --- a/addons/global_asset_manager/asset_manager.tscn +++ /dev/null @@ -1,446 +0,0 @@ -[gd_scene format=3 uid="uid://dspvtw055wu4s"] - -[ext_resource type="Script" uid="uid://bhxhjvggmpfr6" path="res://addons/global_asset_manager/asset_manager.gd" id="1_abcde"] -[ext_resource type="Script" uid="uid://bqil1tlkmnbxj" path="res://addons/global_asset_manager/settings_manager.gd" id="2_fghij"] - -[sub_resource type="Environment" id="Environment_f1y5n"] -background_mode = 1 -background_color = Color(0.18, 0.18, 0.18, 1) -ambient_light_source = 2 -ambient_light_color = Color(1, 1, 1, 1) -ambient_light_energy = 0.4 - -[sub_resource type="World3D" id="World3D_ul3vj"] -environment = SubResource("Environment_f1y5n") - -[node name="AssetManager" type="Control" unique_id=1572556146] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -script = ExtResource("1_abcde") - -[node name="MarginContainer" type="MarginContainer" parent="." unique_id=1495794556] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/margin_left = 16 -theme_override_constants/margin_top = 16 -theme_override_constants/margin_right = 16 -theme_override_constants/margin_bottom = 16 - -[node name="MainSplit" type="HSplitContainer" parent="MarginContainer" unique_id=838962311] -layout_mode = 2 -split_offsets = PackedInt32Array(250) -split_offset = 250 - -[node name="Sidebar" type="VBoxContainer" parent="MarginContainer/MainSplit" unique_id=2026459748] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="ScanButton" type="Button" parent="MarginContainer/MainSplit/Sidebar" unique_id=1200203880] -layout_mode = 2 -text = "Scan Folder..." - -[node name="SettingsButton" type="Button" parent="MarginContainer/MainSplit/Sidebar" unique_id=1029532372] -layout_mode = 2 -text = "Settings" - -[node name="NavigationTree" type="Tree" parent="MarginContainer/MainSplit/Sidebar" unique_id=1210938222] -layout_mode = 2 -size_flags_vertical = 3 -allow_rmb_select = true -hide_root = true - -[node name="ContentSplit" type="HSplitContainer" parent="MarginContainer/MainSplit" unique_id=1996806136] -layout_mode = 2 -split_offsets = PackedInt32Array(400) -split_offset = 400 - -[node name="CenterPanel" type="VBoxContainer" parent="MarginContainer/MainSplit/ContentSplit" unique_id=769933576] -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="SearchHBox" type="HBoxContainer" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel" unique_id=1595355325] -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="SearchInput" type="LineEdit" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox" unique_id=1650205098] -layout_mode = 2 -size_flags_horizontal = 3 -placeholder_text = "Fuzzy search assets..." - -[node name="ZoomOutButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox" unique_id=839575094] -visible = false -layout_mode = 2 -text = " - " - -[node name="ZoomInButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox" unique_id=1622756085] -visible = false -layout_mode = 2 -text = " + " - -[node name="ViewModeButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/SearchHBox" unique_id=1186203920] -layout_mode = 2 -text = "View: List" - -[node name="AssetGrid" type="ItemList" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel" unique_id=1739278632] -texture_filter = 1 -layout_mode = 2 -size_flags_vertical = 3 -theme_override_constants/h_separation = 8 -theme_override_constants/v_separation = 4 -select_mode = 1 -fixed_icon_size = Vector2i(24, 24) - -[node name="PaginationContainer" type="HBoxContainer" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel" unique_id=751157778] -layout_mode = 2 -alignment = 1 - -[node name="PrevPageButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer" unique_id=546984628] -layout_mode = 2 -disabled = true -text = "< Prev" - -[node name="PageLabel" type="Label" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer" unique_id=1158757073] -layout_mode = 2 -size_flags_horizontal = 3 -text = "Page 1 of 608" -horizontal_alignment = 1 - -[node name="NextPageButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/CenterPanel/PaginationContainer" unique_id=1287330111] -layout_mode = 2 -text = "Next >" - -[node name="PreviewPanel" type="VBoxContainer" parent="MarginContainer/MainSplit/ContentSplit" unique_id=1928029523] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="PreviewContainer" type="PanelContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel" unique_id=1573284764] -clip_contents = true -layout_mode = 2 -size_flags_vertical = 3 - -[node name="SubViewportContainer" type="SubViewportContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer" unique_id=1729812474] -layout_mode = 2 -mouse_filter = 2 -stretch = true - -[node name="SubViewport" type="SubViewport" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer" unique_id=140946012] -world_3d = SubResource("World3D_ul3vj") -handle_input_locally = false -size = Vector2i(446, 241) -size_2d_override = Vector2i(223, 120) -render_target_update_mode = 4 - -[node name="Camera3D" type="Camera3D" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer/SubViewport" unique_id=1167006985] -transform = Transform3D(0.866025, -0.25, 0.433013, 0, 0.866025, 0.5, -0.5, -0.433013, 0.75, 2, 2, 3) -current = true - -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer/SubViewport" unique_id=813099430] -transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 5, 0) -light_energy = 0.5 - -[node name="ModelPivot" type="Node3D" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/SubViewportContainer/SubViewport" unique_id=182794437] - -[node name="TextureRect" type="TextureRect" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer" unique_id=1268634186] -visible = false -texture_filter = 1 -layout_mode = 2 -mouse_filter = 2 -expand_mode = 1 -stretch_mode = 5 - -[node name="ViewportToolbar" type="MarginContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer" unique_id=1133259305] -visible = false -layout_mode = 2 -size_flags_vertical = 0 -mouse_filter = 2 -theme_override_constants/margin_left = 8 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 8 - -[node name="HBox" type="HBoxContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar" unique_id=2323868] -layout_mode = 2 -alignment = 2 - -[node name="ResetOriginBtn" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox" unique_id=1867599949] -layout_mode = 2 -text = "Origin" - -[node name="ResetZoomBtn" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox" unique_id=1872291065] -layout_mode = 2 -text = "100%" - -[node name="LightModeBtn" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/PreviewContainer/ViewportToolbar/HBox" unique_id=1690862519] -custom_minimum_size = Vector2(80, 0) -layout_mode = 2 -text = "Lit" - -[node name="DetailsPanel" type="VBoxContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel" unique_id=1870626175] -layout_mode = 2 -size_flags_vertical = 3 -size_flags_stretch_ratio = 0.6 -theme_override_constants/separation = 8 - -[node name="FileNameLabel" type="Label" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=1945897261] -custom_minimum_size = Vector2(150, 0) -layout_mode = 2 -text = "No asset selected" -horizontal_alignment = 1 -autowrap_mode = 3 - -[node name="ActionButtons" type="HBoxContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=203888111] -layout_mode = 2 -alignment = 1 - -[node name="ReplayButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons" unique_id=705458203] -visible = false -layout_mode = 2 -text = "Replay" - -[node name="OpenExternalButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons" unique_id=2085029535] -layout_mode = 2 -disabled = true -text = "Open External" - -[node name="OpenLocationButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons" unique_id=1933600867] -layout_mode = 2 -disabled = true -text = "Open Location" - -[node name="SendToProjectButton" type="Button" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/ActionButtons" unique_id=1506299809] -layout_mode = 2 -disabled = true -text = "Add to Project" - -[node name="HSeparator1" type="HSeparator" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=1957271692] -layout_mode = 2 - -[node name="CurrentTagsLabel" type="Label" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=1625403212] -layout_mode = 2 -text = "Current Tags (Click to remove):" - -[node name="CurrentTagsScroll" type="ScrollContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=325549676] -custom_minimum_size = Vector2(0, 80) -layout_mode = 2 -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="TagFlowContainer" type="HFlowContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/CurrentTagsScroll" unique_id=1644314182] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 - -[node name="TagInputField" type="LineEdit" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=189668564] -layout_mode = 2 -placeholder_text = "Type new tag and press Enter..." - -[node name="HSeparator2" type="HSeparator" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=2046671363] -layout_mode = 2 - -[node name="AvailableTagsLabel" type="Label" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=796453656] -layout_mode = 2 -text = "Quick Add Known Tags:" - -[node name="AvailableTagsScroll" type="ScrollContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel" unique_id=82847435] -custom_minimum_size = Vector2(0, 80) -layout_mode = 2 -size_flags_vertical = 3 -horizontal_scroll_mode = 0 - -[node name="AvailableTagsFlowContainer" type="HFlowContainer" parent="MarginContainer/MainSplit/ContentSplit/PreviewPanel/DetailsPanel/AvailableTagsScroll" unique_id=1147206232] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 - -[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=447829074] -volume_db = -12.0412 - -[node name="FolderDialog" type="FileDialog" parent="." unique_id=751017296] -title = "Open a Directory" -size = Vector2i(800, 600) -ok_button_text = "Select Current Folder" -file_mode = 2 -access = 2 - -[node name="SettingsDialog" type="AcceptDialog" parent="." unique_id=1835870348] -oversampling_override = 1.0 -title = "Settings" -position = Vector2i(0, 36) -size = Vector2i(788, 650) -script = ExtResource("2_fghij") - -[node name="ScrollContainer" type="ScrollContainer" parent="SettingsDialog" unique_id=1304404260] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 8.0 -offset_top = 8.0 -offset_right = -8.0 -offset_bottom = -49.0 -horizontal_scroll_mode = 0 - -[node name="VBoxContainer" type="VBoxContainer" parent="SettingsDialog/ScrollContainer" unique_id=1102077803] -layout_mode = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -theme_override_constants/separation = 12 - -[node name="VolumeLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=597664857] -layout_mode = 2 -text = "Max Audio Volume: 25%" - -[node name="VolumeSlider" type="HSlider" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=337623076] -layout_mode = 2 -value = 25.0 - -[node name="WavLengthLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1860502584] -layout_mode = 2 -text = "WAV Preview Length: 20s" - -[node name="WavLengthSlider" type="HSlider" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1204250806] -layout_mode = 2 -min_value = 5.0 -max_value = 60.0 -value = 20.0 - -[node name="WavDisclaimerLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1950677763] -layout_mode = 2 -theme_override_colors/font_color = Color(1, 0.43529412, 0.43529412, 1) -theme_override_font_sizes/font_size = 14 -text = "* Higher values may cause performance drops or high RAM usage when previewing large WAV files." - -[node name="HSeparator1" type="HSeparator" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=898958503] -layout_mode = 2 - -[node name="PathsLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=79087959] -layout_mode = 2 -text = "Default Project Import Paths" - -[node name="PathsVBox" type="VBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=212524826] -layout_mode = 2 -theme_override_constants/separation = 8 - -[node name="Path3DBox" type="HBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox" unique_id=533637137] -layout_mode = 2 - -[node name="Label" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path3DBox" unique_id=482903522] -custom_minimum_size = Vector2(90, 0) -layout_mode = 2 -text = "3D Models:" - -[node name="Path3DEdit" type="LineEdit" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path3DBox" unique_id=1729944401] -layout_mode = 2 -size_flags_horizontal = 3 -text = "res://assets/models" -placeholder_text = "res://assets/models" - -[node name="Path3DBtn" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path3DBox" unique_id=1270886819] -layout_mode = 2 -text = "Browse" - -[node name="Path2DBox" type="HBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox" unique_id=1285561563] -layout_mode = 2 - -[node name="Label" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path2DBox" unique_id=79932626] -custom_minimum_size = Vector2(90, 0) -layout_mode = 2 -text = "2D Images:" - -[node name="Path2DEdit" type="LineEdit" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path2DBox" unique_id=771401102] -layout_mode = 2 -size_flags_horizontal = 3 -text = "res://assets/images" -placeholder_text = "res://assets/images" - -[node name="Path2DBtn" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/Path2DBox" unique_id=1491796409] -layout_mode = 2 -text = "Browse" - -[node name="PathAudioBox" type="HBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox" unique_id=183657517] -layout_mode = 2 - -[node name="Label" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathAudioBox" unique_id=1193668696] -custom_minimum_size = Vector2(90, 0) -layout_mode = 2 -text = "Audio:" - -[node name="PathAudioEdit" type="LineEdit" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathAudioBox" unique_id=559033777] -layout_mode = 2 -size_flags_horizontal = 3 -text = "res://assets/audio" -placeholder_text = "res://assets/audio" - -[node name="PathAudioBtn" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathAudioBox" unique_id=1049358774] -layout_mode = 2 -text = "Browse" - -[node name="PathShaderBox" type="HBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox" unique_id=1789204463] -layout_mode = 2 - -[node name="Label" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathShaderBox" unique_id=290911804] -custom_minimum_size = Vector2(90, 0) -layout_mode = 2 -text = "Shaders:" - -[node name="PathShaderEdit" type="LineEdit" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathShaderBox" unique_id=2109857326] -layout_mode = 2 -size_flags_horizontal = 3 -text = "res://assets/shaders" -placeholder_text = "res://assets/shaders" - -[node name="PathShaderBtn" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/PathsVBox/PathShaderBox" unique_id=866708351] -layout_mode = 2 -text = "Browse" - -[node name="HSeparator2" type="HSeparator" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=576437035] -layout_mode = 2 - -[node name="FormatsLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=474270375] -layout_mode = 2 -text = "Active Formats" - -[node name="SettingsTree" type="Tree" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1374659203] -custom_minimum_size = Vector2(0, 150) -layout_mode = 2 -hide_root = true -scroll_vertical_enabled = false - -[node name="HSeparator3" type="HSeparator" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=582192234] -layout_mode = 2 - -[node name="DangerZoneLabel" type="Label" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1373151021] -layout_mode = 2 -theme_override_colors/font_color = Color(1, 0.435294, 0.435294, 1) -text = "Danger Zone" - -[node name="DangerZoneBox" type="HBoxContainer" parent="SettingsDialog/ScrollContainer/VBoxContainer" unique_id=1347846678] -layout_mode = 2 -theme_override_constants/separation = 16 - -[node name="ResetButton" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/DangerZoneBox" unique_id=379439961] -layout_mode = 2 -text = "Reset Settings to Default" - -[node name="ClearButton" type="Button" parent="SettingsDialog/ScrollContainer/VBoxContainer/DangerZoneBox" unique_id=980056231] -layout_mode = 2 -text = "Clear Entire Asset Database" - -[node name="DirDialog" type="FileDialog" parent="SettingsDialog" unique_id=1398567986] -title = "Open a Directory" -size = Vector2i(600, 400) -ok_button_text = "Select Current Folder" -file_mode = 2 - -[node name="TagContextMenu" type="PopupMenu" parent="." unique_id=1890144552] -item_count = 1 -item_0/text = "Delete Tag Globally" -item_0/id = 0 diff --git a/addons/global_asset_manager/plugin.cfg b/addons/global_asset_manager/plugin.cfg deleted file mode 100644 index 3eb8fcc..0000000 --- a/addons/global_asset_manager/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Global Asset Manager" -description="A central hub for scanning, tagging, and importing local game assets." -author="sn1ks0h" -version="2.1.4" -script="plugin.gd" diff --git a/addons/global_asset_manager/plugin.gd b/addons/global_asset_manager/plugin.gd deleted file mode 100644 index 5c09eb1..0000000 --- a/addons/global_asset_manager/plugin.gd +++ /dev/null @@ -1,27 +0,0 @@ -@tool -extends EditorPlugin - -var main_panel_instance: Control - - -func _enter_tree() -> void: - main_panel_instance = preload("res://addons/global_asset_manager/asset_manager.tscn").instantiate() - EditorInterface.get_editor_main_screen().add_child(main_panel_instance) - _make_visible(false) - -func _exit_tree() -> void: - if main_panel_instance: - main_panel_instance.queue_free() - -func _has_main_screen() -> bool: - return true - -func _make_visible(visible: bool) -> void: - if main_panel_instance: - main_panel_instance.visible = visible - -func _get_plugin_name() -> String: - return "AssetHub" - -func _get_plugin_icon() -> Texture2D: - return EditorInterface.get_editor_theme().get_icon("Load", "EditorIcons") diff --git a/addons/global_asset_manager/plugin.gd.uid b/addons/global_asset_manager/plugin.gd.uid deleted file mode 100644 index 30d9f6d..0000000 --- a/addons/global_asset_manager/plugin.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cop233bihhwbe diff --git a/addons/global_asset_manager/preview_controller.gd b/addons/global_asset_manager/preview_controller.gd deleted file mode 100644 index 7dcc126..0000000 --- a/addons/global_asset_manager/preview_controller.gd +++ /dev/null @@ -1,116 +0,0 @@ -@tool -class_name PreviewController -extends RefCounted -## Handles 2D and 3D preview manipulations (orbit, pan, zoom). - -var pivot: Node3D -var camera: Camera3D -var texture_rect: TextureRect - -var _is_orbiting: bool = false -var _is_panning_3d: bool = false -var _is_panning_2d: bool = false -var _last_mouse_pos: Vector2 = Vector2.ZERO - -var _yaw: float = 0.0 -var _pitch: float = 0.0 -var _initial_cam_transform: Transform3D - -func reset_views() -> void: - _yaw = 0.0 - _pitch = 0.0 - if is_instance_valid(pivot): - pivot.transform.basis = Basis.IDENTITY - pivot.position = Vector3.ZERO - if is_instance_valid(camera): - if _initial_cam_transform == Transform3D(): - _initial_cam_transform = camera.transform - camera.transform = _initial_cam_transform - - if is_instance_valid(texture_rect): - texture_rect.scale = Vector2.ONE - texture_rect.position = Vector2.ZERO - -func reset_origin() -> void: - _yaw = 0.0 - _pitch = 0.0 - if is_instance_valid(pivot): - pivot.transform.basis = Basis.IDENTITY - pivot.position = Vector3.ZERO - -func reset_zoom() -> void: - if is_instance_valid(camera) and _initial_cam_transform != Transform3D(): - # Preserve the camera's angle, but snap its distance back to default - camera.global_position = _initial_cam_transform.origin - -func handle_3d_input(event: InputEvent) -> void: - if event is InputEventMouseButton: - if event.button_index == MOUSE_BUTTON_LEFT: - _is_orbiting = event.pressed - _last_mouse_pos = event.position - elif event.button_index == MOUSE_BUTTON_RIGHT or event.button_index == MOUSE_BUTTON_MIDDLE: - _is_panning_3d = event.pressed - _last_mouse_pos = event.position - elif event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed: - # Zoom in along the camera's actual line of sight - camera.global_position -= camera.global_transform.basis.z * 0.5 - elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed: - # Zoom out along the camera's actual line of sight - camera.global_position += camera.global_transform.basis.z * 0.5 - - elif event is InputEventMouseMotion: - var motion := event as InputEventMouseMotion - var delta: Vector2 = motion.position - _last_mouse_pos - _last_mouse_pos = motion.position - - if _is_orbiting: - _yaw += delta.x * 0.01 - _pitch += delta.y * 0.01 - _pitch = clampf(_pitch, -PI / 2.5, PI / 2.5) - - pivot.transform.basis = Basis.IDENTITY - - # Rotate relative to the camera's orientation so screen-space movement always feels perfectly vertical/horizontal - var cam_up := camera.global_transform.basis.y.normalized() - var cam_right := camera.global_transform.basis.x.normalized() - - pivot.global_rotate(cam_up, _yaw) - pivot.global_rotate(cam_right, _pitch) - - elif _is_panning_3d: - var right := camera.global_transform.basis.x - var up := camera.global_transform.basis.y - - # Calculate distance from camera to pivot to dynamically adjust pan speed - var dist := camera.global_position.distance_to(pivot.global_position) - var pan_speed := maxf(0.01, dist * 0.003) - - pivot.global_position += right * delta.x * pan_speed - pivot.global_position -= up * delta.y * pan_speed - -func handle_2d_input(event: InputEvent) -> void: - if event is InputEventMouseButton: - if event.button_index == MOUSE_BUTTON_LEFT or event.button_index == MOUSE_BUTTON_RIGHT or event.button_index == MOUSE_BUTTON_MIDDLE: - _is_panning_2d = event.pressed - _last_mouse_pos = event.position - elif event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed: - _zoom_2d(event.position, 1.1) - elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed: - _zoom_2d(event.position, 1.0 / 1.1) - - elif event is InputEventMouseMotion and _is_panning_2d: - var motion := event as InputEventMouseMotion - var delta: Vector2 = motion.position - _last_mouse_pos - _last_mouse_pos = motion.position - texture_rect.position += delta - -func _zoom_2d(mouse_pos: Vector2, factor: float) -> void: - var old_scale := texture_rect.scale - var new_scale := old_scale * factor - - new_scale.x = clampf(new_scale.x, 0.1, 50.0) - new_scale.y = clampf(new_scale.y, 0.1, 50.0) - - var mouse_local := mouse_pos - texture_rect.position - texture_rect.position -= mouse_local * ((new_scale / old_scale) - Vector2.ONE) - texture_rect.scale = new_scale diff --git a/addons/global_asset_manager/preview_controller.gd.uid b/addons/global_asset_manager/preview_controller.gd.uid deleted file mode 100644 index ccaee72..0000000 --- a/addons/global_asset_manager/preview_controller.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ccl1fofx0hjq0 diff --git a/addons/global_asset_manager/settings_manager.gd b/addons/global_asset_manager/settings_manager.gd deleted file mode 100644 index 9d9fc99..0000000 --- a/addons/global_asset_manager/settings_manager.gd +++ /dev/null @@ -1,200 +0,0 @@ -@tool -extends AcceptDialog - -signal settings_changed -signal database_cleared - -const ALL_3D_FORMATS: PackedStringArray = ["glb", "gltf", "fbx"] -const ALL_2D_FORMATS: PackedStringArray = ["png", "jpg", "jpeg", "webp"] -const ALL_AUDIO_FORMATS: PackedStringArray = ["ogg", "mp3", "wav"] - -var db: Dictionary - -var _current_path_target: String = "" - -@onready var clear_button: Button = $ScrollContainer/VBoxContainer/DangerZoneBox/ClearButton -@onready var dir_dialog: FileDialog = $DirDialog -@onready var path_btn_2d: Button = $ScrollContainer/VBoxContainer/PathsVBox/Path2DBox/Path2DBtn -@onready var path_btn_3d: Button = $ScrollContainer/VBoxContainer/PathsVBox/Path3DBox/Path3DBtn -@onready var path_btn_audio: Button = $ScrollContainer/VBoxContainer/PathsVBox/PathAudioBox/PathAudioBtn -@onready var path_btn_shader: Button = $ScrollContainer/VBoxContainer/PathsVBox/PathShaderBox/PathShaderBtn -@onready var path_edit_2d: LineEdit = $ScrollContainer/VBoxContainer/PathsVBox/Path2DBox/Path2DEdit -@onready var path_edit_3d: LineEdit = $ScrollContainer/VBoxContainer/PathsVBox/Path3DBox/Path3DEdit -@onready var path_edit_audio: LineEdit = $ScrollContainer/VBoxContainer/PathsVBox/PathAudioBox/PathAudioEdit -@onready var path_edit_shader: LineEdit = $ScrollContainer/VBoxContainer/PathsVBox/PathShaderBox/PathShaderEdit -@onready var reset_button: Button = $ScrollContainer/VBoxContainer/DangerZoneBox/ResetButton -@onready var settings_tree: Tree = $ScrollContainer/VBoxContainer/SettingsTree -@onready var volume_label: Label = $ScrollContainer/VBoxContainer/VolumeLabel -@onready var volume_slider: HSlider = $ScrollContainer/VBoxContainer/VolumeSlider -@onready var wav_length_label: Label = $ScrollContainer/VBoxContainer/WavLengthLabel -@onready var wav_length_slider: HSlider = $ScrollContainer/VBoxContainer/WavLengthSlider - -func save_database() -> void: - var file := FileAccess.open("user://asset_database.dat", FileAccess.WRITE) - if file: - file.store_var(db) - -func setup(database: Dictionary) -> void: - db = database - _verify_settings_schema() - _update_ui_from_db() - - if not wav_length_slider.value_changed.is_connected(_on_wav_length_changed): - wav_length_slider.value_changed.connect(_on_wav_length_changed) - if not wav_length_slider.drag_ended.is_connected(_on_wav_length_drag_ended): - wav_length_slider.drag_ended.connect(_on_wav_length_drag_ended) - -func _ready() -> void: - settings_tree.item_edited.connect(_on_settings_tree_item_edited) - - volume_slider.value_changed.connect(_on_volume_changed) - volume_slider.drag_ended.connect(_on_volume_drag_ended) - - path_btn_3d.pressed.connect(func() -> void: _open_dir_dialog("3d")) - path_btn_2d.pressed.connect(func() -> void: _open_dir_dialog("2d")) - path_btn_audio.pressed.connect(func() -> void: _open_dir_dialog("audio")) - path_btn_shader.pressed.connect(func() -> void: _open_dir_dialog("shader")) - - path_edit_3d.text_changed.connect(func(text: String) -> void: _update_path("3d", text)) - path_edit_2d.text_changed.connect(func(text: String) -> void: _update_path("2d", text)) - path_edit_audio.text_changed.connect(func(text: String) -> void: _update_path("audio", text)) - path_edit_shader.text_changed.connect(func(text: String) -> void: _update_path("shader", text)) - - reset_button.pressed.connect(_on_reset_pressed) - clear_button.pressed.connect(_on_clear_pressed) - dir_dialog.dir_selected.connect(_on_dir_selected) - -func _add_setting_category(parent: TreeItem, label: String, formats: PackedStringArray) -> void: - var category_item := settings_tree.create_item(parent) - category_item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK) - category_item.set_text(0, label) - category_item.set_editable(0, true) - category_item.set_metadata(0, "category") - - var all_checked := true - for ext in formats: - var child := settings_tree.create_item(category_item) - child.set_cell_mode(0, TreeItem.CELL_MODE_CHECK) - child.set_text(0, "." + ext) - child.set_editable(0, true) - - var is_active: bool = db["settings"]["active_formats"].has(ext) - child.set_checked(0, is_active) - child.set_metadata(0, ext) - - if not is_active: - all_checked = false - - category_item.set_checked(0, all_checked) - -func _on_clear_pressed() -> void: - database_cleared.emit() - hide() - -func _on_dir_selected(dir: String) -> void: - if _current_path_target == "3d": path_edit_3d.text = dir - elif _current_path_target == "2d": path_edit_2d.text = dir - elif _current_path_target == "audio": path_edit_audio.text = dir - elif _current_path_target == "shader": path_edit_shader.text = dir - - _update_path(_current_path_target, dir) - -func _on_reset_pressed() -> void: - db["settings"].clear() - _verify_settings_schema() - _update_ui_from_db() - save_database() - settings_changed.emit() - -func _on_settings_tree_item_edited() -> void: - var item := settings_tree.get_edited() - var is_checked := item.is_checked(0) - var meta: Variant = item.get_metadata(0) - - if meta is String and meta == "category": - for i in range(item.get_child_count()): - var child := item.get_child(i) - child.set_checked(0, is_checked) - var ext: String = child.get_metadata(0) - _update_active_format(ext, is_checked) - elif meta is String: - _update_active_format(meta, is_checked) - var parent := item.get_parent() - if parent: - var all_checked := true - for i in range(parent.get_child_count()): - if not parent.get_child(i).is_checked(0): - all_checked = false - parent.set_checked(0, all_checked) - - save_database() - settings_changed.emit() - -func _on_volume_changed(value: float) -> void: - volume_label.text = "Max Audio Volume: " + str(int(value)) + "%" - -func _on_volume_drag_ended(_value_changed: bool) -> void: - db["settings"]["volume"] = volume_slider.value - save_database() - settings_changed.emit() - -func _on_wav_length_changed(value: float) -> void: - wav_length_label.text = "WAV Preview Length: " + str(int(value)) + "s" - -func _on_wav_length_drag_ended(_value_changed: bool) -> void: - db["settings"]["wav_preview_length"] = int(wav_length_slider.value) - save_database() - settings_changed.emit() - -func _open_dir_dialog(target: String) -> void: - _current_path_target = target - dir_dialog.current_dir = "res://" - dir_dialog.popup_centered() - -func _populate_settings_tree() -> void: - settings_tree.clear() - var root := settings_tree.create_item() - - _add_setting_category(root, "3D Models", ALL_3D_FORMATS) - _add_setting_category(root, "2D Images", ALL_2D_FORMATS) - _add_setting_category(root, "Audio", ALL_AUDIO_FORMATS) - -func _update_active_format(ext: String, is_active: bool) -> void: - var active_list: Array = db["settings"]["active_formats"] - if is_active and not active_list.has(ext): - active_list.append(ext) - elif not is_active and active_list.has(ext): - active_list.erase(ext) - -func _update_path(target: String, new_path: String) -> void: - db["settings"]["import_path_" + target] = new_path - save_database() - settings_changed.emit() - -func _update_ui_from_db() -> void: - var s: Dictionary = db["settings"] - volume_slider.value = s["volume"] - volume_label.text = "Max Audio Volume: " + str(int(s["volume"])) + "%" - - wav_length_slider.value = s["wav_preview_length"] - wav_length_label.text = "WAV Preview Length: " + str(int(s["wav_preview_length"])) + "s" - - path_edit_3d.text = s["import_path_3d"] - path_edit_2d.text = s["import_path_2d"] - path_edit_audio.text = s["import_path_audio"] - path_edit_shader.text = s["import_path_shader"] - - _populate_settings_tree() - -func _verify_settings_schema() -> void: - if not db.has("settings"): - db["settings"] = {} - - var s: Dictionary = db["settings"] - if not s.has("active_formats"): s["active_formats"] = ["glb", "gltf", "png", "jpg", "jpeg", "webp", "ogg", "mp3", "wav"] - if not s.has("volume"): s["volume"] = 25.0 - if not s.has("import_path_3d"): s["import_path_3d"] = "res://assets/models" - if not s.has("import_path_2d"): s["import_path_2d"] = "res://assets/images" - if not s.has("import_path_audio"): s["import_path_audio"] = "res://assets/audio" - if not s.has("import_path_shader"): s["import_path_shader"] = "res://assets/shaders" - if not s.has("wav_preview_length"): s["wav_preview_length"] = 10 diff --git a/addons/global_asset_manager/settings_manager.gd.uid b/addons/global_asset_manager/settings_manager.gd.uid deleted file mode 100644 index c9340e6..0000000 --- a/addons/global_asset_manager/settings_manager.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqil1tlkmnbxj diff --git a/assets/models/Book_03.glb b/assets/models/Book_03.glb deleted file mode 100644 index 6b4efd6..0000000 Binary files a/assets/models/Book_03.glb and /dev/null differ diff --git a/assets/models/Book_03.glb.import b/assets/models/Book_03.glb.import deleted file mode 100644 index d76fa51..0000000 --- a/assets/models/Book_03.glb.import +++ /dev/null @@ -1,45 +0,0 @@ -[remap] - -importer="scene" -importer_version=1 -type="PackedScene" -uid="uid://cp33uhqn8fxg4" -path="res://.godot/imported/Book_03.glb-87eb6f5eef5e54802be80049521081ad.scn" - -[deps] - -source_file="res://assets/models/Book_03.glb" -dest_files=["res://.godot/imported/Book_03.glb-87eb6f5eef5e54802be80049521081ad.scn"] - -[params] - -nodes/root_type="" -nodes/root_name="" -nodes/root_script=null -mesh_library/use_node_names_as_mesh_names=false -array_mesh/deduplicate_surfaces=true -nodes/apply_root_scale=true -nodes/root_scale=1.0 -nodes/import_as_skeleton_bones=false -nodes/use_name_suffixes=true -nodes/use_node_type_suffixes=true -meshes/ensure_tangents=true -meshes/generate_lods=true -meshes/create_shadow_meshes=true -meshes/light_baking=1 -meshes/lightmap_texel_size=0.2 -meshes/force_disable_compression=false -skins/use_named_skins=true -animation/import=true -animation/fps=30 -animation/trimming=false -animation/remove_immutable_tracks=true -animation/import_rest_as_RESET=false -import_script/path="" -materials/extract=0 -materials/extract_format=0 -materials/extract_path="" -_subresources={} -gltf/naming_version=2 -gltf/embedded_image_handling=1 -gltf/texture_map_mode=1 diff --git a/assets/models/Book_03_Textures_4.png b/assets/models/Book_03_Textures_4.png deleted file mode 100644 index 54949cd..0000000 Binary files a/assets/models/Book_03_Textures_4.png and /dev/null differ diff --git a/assets/models/Book_03_Textures_4.png.import b/assets/models/Book_03_Textures_4.png.import deleted file mode 100644 index 574e60a..0000000 --- a/assets/models/Book_03_Textures_4.png.import +++ /dev/null @@ -1,44 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://c0vt0ds2vu6oi" -path.s3tc="res://.godot/imported/Book_03_Textures_4.png-ecfaa7a0a33416176d99579880e393b4.s3tc.ctex" -metadata={ -"imported_formats": ["s3tc_bptc"], -"vram_texture": true -} -generator_parameters={ -"md5": "b250b807dfc1a5eb051d5b351e7c1fc8" -} - -[deps] - -source_file="res://assets/models/Book_03_Textures_4.png" -dest_files=["res://.godot/imported/Book_03_Textures_4.png-ecfaa7a0a33416176d99579880e393b4.s3tc.ctex"] - -[params] - -compress/mode=2 -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=true -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=0 diff --git a/project.godot b/project.godot index 86cc5f6..d301386 100644 --- a/project.godot +++ b/project.godot @@ -36,7 +36,7 @@ project/assembly_name="agame" [editor_plugins] -enabled=PackedStringArray("res://addons/global_asset_manager/plugin.cfg", "res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/mesh_collider/plugin.cfg") +enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg", "res://addons/godotsteam/plugin.cfg", "res://addons/mesh_collider/plugin.cfg") [input]