180 lines
5.9 KiB
GDScript
180 lines
5.9 KiB
GDScript
## 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) and result is not String:
|
|
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]
|