extends PanelContainer #warning-ignore-all:return_value_discarded signal debug_pressed signal savefile_loaded(dicts) const globals = preload('res://globals.gd') # Make LSP shut up about non-const const FOLDER_ICON := globals.FOLDER_ICON const ALLOWED_EXTS := globals.ALLOWED_EXTS const CD_EXTS := globals.CD_EXTS const EXT_ICONS := globals.EXT_ICONS const TYPE_DESCS := globals.TYPE_DESCS enum {SELECT_ROM, SELECT_SAVE, COMPLETE} var current_mode = SELECT_ROM var android_permissions_granted := false var cached_cd_bin_paths := {} var dir := Directory.new() var home_path := '' onready var itemlist: ItemList = $VBoxContainer/HBoxContainer/PanelContainer/ItemList onready var vscroller: VScrollBar = itemlist.get_child(0) onready var folder_buttons_scroller: ScrollContainer = $VBoxContainer/ScrollContainer onready var folder_buttons: HBoxContainer = $VBoxContainer/ScrollContainer/folder_buttons onready var btn_load: Button = $'%btn_load' onready var btn_debug: Button = $'%btn_debug' onready var lbl_filename_header: Label = $'%lbl_filename_header' onready var lbl_filename_content: Label = $'%lbl_filename_content' onready var lbl_filetype_header: Label = $'%lbl_filetype_header' onready var lbl_filetype_content: Label = $'%lbl_filetype_content' onready var lbl_filesize_header: Label = $'%lbl_filesize_header' onready var lbl_filesize_content: Label = $'%lbl_filesize_content' onready var lbl_fileinfo_header: Label = $'%lbl_fileinfo_header' onready var lbl_fileinfo_content: Label = $'%lbl_fileinfo_content' onready var splash_filter := $'%splash_filter' onready var splash_label := $'%splash_label' const inactive_modulate_color := Color(0.5, 0.5, 0.5) const active_modulate_color := Color.white var last_dir := '' static func get_human_size(n: int) -> String: if n > 0x100000000: return '%.2f GiB' % (n/float(0x40000000)) if n > 0x400000: return '%.1f MiB' % (n/float(0x100000)) if n > 0x1000: return '%.1f KiB' % (n/float(0x400)) return '%d B' % n static func tokenize_path(path: String) -> PoolStringArray: return path.rstrip('/').split('/', true) # Allow empty means '/' will return [''] static func join_path(tokens) -> String: var path = '/'.join(tokens) return path if path else '/' const REQUIRED_ANDROID_PERMISSIONS := PoolStringArray(['MANAGE_EXTERNAL_FILES', 'READ_EXTERNAL_FILES', 'WRITE_EXTERNAL_FILES']) func check_android_permissions() -> bool: if self.android_permissions_granted: return true var permissions_granted := OS.get_granted_permissions() for perm in REQUIRED_ANDROID_PERMISSIONS: if not (perm in permissions_granted): return false self.android_permissions_granted = true return true func update_android_permissions() -> void: if not OS.has_feature('Android'): return if self.check_android_permissions(): return OS.request_permissions() func update_view(): var error = dir.list_dir_begin(true, true) if error != OK: print_debug(error) return for child in folder_buttons.get_children(): child.queue_free() var path = ProjectSettings.globalize_path(dir.get_current_dir()) var cur_path: Array = tokenize_path(path) var l := len(cur_path) for i in l: var btn := Button.new() btn.connect('pressed', self, 'activate_entry', [join_path(cur_path.slice(0, i))]) btn.text = cur_path[i] folder_buttons.add_child(btn) if i == 0: btn.text += '/' if i == l-1: btn.icon = FOLDER_ICON if last_dir.begins_with(path): # i.e. we jumped up a level or more var last_path: Array = tokenize_path(last_dir) var l2 := len(last_path) for i in range(l, l2): var btn := Button.new() btn.connect('pressed', self, 'activate_entry', [join_path(last_path.slice(0, i))]) btn.text = last_path[i] btn.modulate = inactive_modulate_color folder_buttons.add_child(btn) # Doesn't work thanks to frame delay # folder_buttons_scroller.set_h_scroll(9999) # folder_buttons_scroller.ensure_control_visible(folder_buttons_scroller.get_h_scrollbar()) # folder_buttons_scroller.get_h_scrollbar().update() itemlist.clear() var directories := PoolStringArray() var current_dir := dir.get_current_dir() var files := [] while true: var entry := dir.get_next() if len(entry) == 0: break var filename := current_dir + '/' + entry if filename in cached_cd_bin_paths: files.append([entry, EXT_ICONS.iso]) elif dir.current_is_dir(): directories.append(entry) elif '.' in entry: var extension = entry.rsplit('.', true, 1)[1] if extension in ALLOWED_EXTS: files.append([entry, EXT_ICONS.get(extension)]) directories.sort() files.sort() for entry in directories: itemlist.add_item(entry, FOLDER_ICON) for entry in files: itemlist.add_item(entry[0], entry[1]) func load_file(filename: String, full_path: bool = false): var full_filename := filename if not full_path: full_filename = dir.get_current_dir() + '/' + filename var ext := full_filename.rsplit('/', true, 1)[1].rsplit('.', true, 1)[1].to_lower() match ext: 'sfc', 'smc': if true: # current_mode == SELECT_ROM: splash_filter.visible = true splash_label.text = 'Loading ROM: %s'%full_filename var scenetree := get_tree() if scenetree: yield(scenetree, 'idle_frame') RomLoader.load_snes_rom(full_filename) 'srm': if current_mode == SELECT_ROM: return var file := File.new() match file.open(full_filename, File.READ): OK: emit_signal('savefile_loaded', SaveLoader.load_save_dicts_from_file(file)) _set_mode(COMPLETE) var err: print_debug('Error loading savefile: %s - %d'%[full_filename, err]) func activate_entry(entry: String, _index: int = -1): var curr_dir := dir.get_current_dir() if not self.last_dir.begins_with(curr_dir): self.last_dir = curr_dir if dir.dir_exists(entry): var error := dir.change_dir(entry) if error == OK: update_view() init_labels() else: print_debug(error) elif dir.file_exists(entry): load_file(entry) func activate_current_entry(): var selected := itemlist.get_selected_items() if len(selected) > 0: activate_entry(itemlist.get_item_text(selected[0])) func view_entry(entry: String, index: int = -1): init_labels() lbl_filename_header.modulate = active_modulate_color lbl_filename_content.text = entry lbl_filetype_header.modulate = active_modulate_color if dir.dir_exists(entry): lbl_filetype_content.text = 'Folder' btn_load.disabled = false elif dir.file_exists(entry): var file := File.new() var filename := dir.get_current_dir() + '/' + entry var error := file.open(filename, File.READ) if error != OK: print_debug(error) return var size = file.get_len() var human_size = get_human_size(size) var ext = entry.rsplit('.', true, 1)[1].to_lower() var type = TYPE_DESCS.get(ext, 'Unknown') var prodcode := '' if ext in CD_EXTS: var cd := RomLoader.loader_cd_image.new(file) for key in cd.directory: var s = key.trim_prefix('./') var re_match := RomLoader.psx_productcode_regex.search(s) if re_match: prodcode = '%s\n%s\n%s' % [cd.pvd.system_identifier.strip_edges(), cd.pvd.volume_identifier.strip_edges(), re_match.get_string(1)] type = 'PSX CD-ROM Image' cached_cd_bin_paths[filename] = prodcode if index >= 0: itemlist.set_item_icon(index, EXT_ICONS.iso) break var info = ('Info:\n %s' % prodcode) if prodcode else '' lbl_filetype_content.text = type lbl_filesize_header.modulate = active_modulate_color lbl_filesize_content.text = human_size lbl_fileinfo_header.modulate = active_modulate_color if prodcode else inactive_modulate_color lbl_fileinfo_content.text = prodcode match current_mode: SELECT_ROM: btn_load.disabled = (ext != 'sfc' and ext != 'smc') # Only allow opening snes roms for now SELECT_SAVE, COMPLETE: btn_load.disabled = (ext != 'srm') func init_labels(): lbl_filename_header.modulate = inactive_modulate_color lbl_filename_content.text = '' lbl_filetype_header.modulate = inactive_modulate_color lbl_filetype_content.text = '' lbl_filesize_header.modulate = inactive_modulate_color lbl_filesize_content.text = '' lbl_fileinfo_header.modulate = inactive_modulate_color lbl_fileinfo_content.text = '' btn_load.disabled = true func _ready() -> void: init_labels() if OS.has_feature('Windows'): home_path = OS.get_environment('USERPROFILE') elif OS.has_feature('Android'): home_path = '/storage/emulated/0/Download/' else: home_path = OS.get_environment('HOME') # dir.open(OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)) match dir.open(Common.config.get_value('filesystem', 'last_used_directory', home_path)): OK: update_view() _: match dir.open(home_path): OK: update_view() _: print_debug('Failed to open last dir or home dir') btn_load.connect('pressed', self, 'activate_current_entry') btn_debug.connect('pressed', self, '_on_debug_pressed') RomLoader.connect('rom_loaded', self, '_on_rom_loaded') RomLoader.connect('loading_stage_updated', self, '_on_loading_stage_updated') print(ProjectSettings.globalize_path('user://')) print(ProjectSettings.globalize_path(dir.get_current_dir())) get_tree().connect('files_dropped', self, '_files_dropped') get_tree().connect('on_request_permissions_result', self, '_on_request_permissions_result') self.update_android_permissions() func _files_dropped(filenames: PoolStringArray, screen_idx: int) -> void: print('Files Dropped signal: screen=%d, files='%screen_idx, filenames) for filename in filenames: load_file(filename, true) func _on_request_permissions_result(permission: String, granted: bool) -> void: print('_on_request_permissions_result: %s = %s' % [permission, granted]) # if granted and (permission in REQUIRED_ANDROID_PERMISSIONS): self.call_deferred('update_view') func _update_config() -> void: var d = self.dir.get_current_dir() if d != Common.config.get_value('filesystem', 'last_used_directory'): Common.config.set_value('filesystem', 'last_used_directory', self.dir.get_current_dir()) Common.save_config() func _set_mode(new_mode) -> void: match new_mode: SELECT_ROM: $'%lbl_prompt'.text = "Select a ROM from your device's storage" btn_debug.disabled = true SELECT_SAVE: $'%lbl_prompt'.text = "Select a save file from your device's storage" btn_debug.disabled = false _update_config() COMPLETE: $'%lbl_prompt'.text = "Save file loaded" btn_debug.disabled = false _update_config() current_mode = new_mode update_view() init_labels() func _on_ItemList_item_activated(index: int) -> void: activate_entry(itemlist.get_item_text(index), index) func _on_ItemList_item_selected(index: int) -> void: view_entry(itemlist.get_item_text(index), index) func _on_loading_stage_updated(stage: String) -> void: splash_label.text += '\n' + stage func _on_rom_loaded() -> void: splash_filter.visible = false _set_mode(SELECT_SAVE) func _on_debug_pressed() -> void: emit_signal('debug_pressed')