## Virtual File Access ## ## File access across different methods sucks. ## Here's something to abstract that away and suck a little less. ## ## Written for Godot 4.4 class_name VFileAccess extends RefCounted ## Built-in VFileAccess factories (Learn to make your own with these) const CREATE:GDScript = preload("create_vfs.gd") ## File importer functions. const IMPORTS:GDScript = preload("vfs_loaders.gd") ## Path prefix. How this is handled depends on the file loader itself. var root:String = "" ## Callable(bytes:PackedByteArray, [...])->Variant var supported_files:Dictionary[String,Callable] = {} ## Get any stuff we might be using in our closures. var _get_stuff:Callable = func()->Variant: return null ## How can we write to a file? var _write_file:Callable = func(_abs_path:String, data:Variant)->Error: return ERR_CANT_OPEN ## How do we get bytes from this? var _get_buffer:Callable = func(_abs_path:String)->PackedByteArray: return PackedByteArray() ## What determines if a file exists? var _file_exists:Callable = func(_abs_path:String)->bool: return false ## How do we get files at a subdirectory? var _get_files_at:Callable = func(_abs_path:String)->Array[String]: return [] ## Shutdown code here. var _close:Callable = func()->void: pass ## Default func _init( root_dir:String = "./", support_files = IMPORTS.DEFAULT_SUPPORTED_FILES )->void: self.root = root_dir self.supported_files = support_files.duplicate(true) #region Static ops static func copy_file(from:VFileAccess, to:VFileAccess)->bool: push_warning("Copy file unimplemented") return false #endregion ## I never won awards for naming things correctly. func get_stuff()->Variant: return _get_stuff.call() ## Add automatic file loaders to this virtual file system. func add_supported_files(file_loaders:Dictionary[String,Callable])->Dictionary[String,bool]: var results:Dictionary[String,bool] = {} for key:String in file_loaders.keys(): var loader := file_loaders[key] results[key] = false if not IMPORTS.validate_loader(loader): continue supported_files[key] = loader results[key] = true return results ## Get all files supported for autoloading. func get_supported_files()->Array[String]: return supported_files.keys() func load_supported_bulk( paths:Array[String], ext_override:String = "" )->Array[Variant]: return paths.map(load_supported.bind(ext_override)) ## Load a supported file. If [param path]'s extension matches a supported file, ## it will use that loader and return whatever it's supposed to. ## [param ext_override] allows for selecting a specific loader by key. ## Returns null if no matching loader is found. func load_supported(path:String, ext_override:String = "")->Variant: var abs_path := get_absolute_path(path) var ext := abs_path.get_extension() \ if ext_override.is_empty() else ext_override if not ext in supported_files.keys(): push_error("File extension '%s' not supported by this loader!" % ext) return null var buffer := get_buffer(path) var result:Variant = supported_files[ext].call(buffer) if not is_instance_valid(result): push_warning("%s loader tried loading '%s' but received null. Does the file exist?" % [ext, abs_path]) return result ## Like load_supported, but expects [param path] to not have an extension. ## Instead, [param extensions] can contain multiple extensions, which will be ## appended and checked to exist. This allows for prioritizing one format ## over another or loosely defining a file id and allowing the engine to ## find a first match. func load_any_supported(base_path:String, extensions:Array[String] = [])->Variant: var _append_ext := func _append_ext(ext:String)->String: return (base_path + ext) \ if ext.begins_with(".") \ else (base_path + "." + ext) var try_paths:Array = extensions.map(_append_ext) for path:String in try_paths: if not file_exists(path): continue return load_supported(path) push_error("Could not find any file for '%s'.%s!" % [base_path, extensions]) return null ## [param parse] Callable(buffer:PackedByteArray)->Variant func load_and_parse(path:String, parse:Callable)->Variant: var abs_path:String = get_absolute_path(path) var buffer:PackedByteArray = get_buffer(abs_path) var result:Variant = parse.call(buffer) return result ## Get bytes from this file loader. func get_buffer(path:String)->PackedByteArray: return get_multiple_buffers([path])[path] ## Get multiple file buffers. ## Returns Dictionary[String,PackedByteArray] ## which uses the path given as the key for the respective data buffer. func get_multiple_buffers(paths:Array[String])->Dictionary[String,PackedByteArray]: var output:Dictionary[String,PackedByteArray] = {} for path:String in paths: var abs_path := get_absolute_path(path) if _file_exists.call(abs_path): output[path] = _get_buffer.call(abs_path) if output[path].size() == 0: push_warning("File '%s' found but is 0 bytes long. Intentional?" % abs_path) else: push_error("File '%s' does not exist!" % abs_path) output[path] = PackedByteArray() return output ## Get files in a directory func get_files_at(path:String)->Array[String]: var abs_path := get_absolute_path(path) return _get_files_at.call(abs_path) ## Write buffer to file, if possible. func write_file(path:String, buffer:PackedByteArray)->Error: return _write_file.call(path, buffer) ## Deallocate stuff. func close()->void: _close.call() ## Check if this file exists within this loader. ## If [param ignore_root] is true, file root will not be prefixed to [param path] func file_exists(path:String, ignore_root := false)->bool: return _file_exists.call(path) \ if ignore_root else _file_exists.call(get_absolute_path(path)) ## Gets the internal absolute path based on this filesystem's root. func get_absolute_path(relative_path:String)->String: if root.is_empty() or root.ends_with("/"): return root + relative_path return "%s/%s" % [root, relative_path]