Initial upload

This commit is contained in:
Pyral 2024-12-03 11:39:32 -05:00
parent 80cdc9f14b
commit 21ec84a1fa
3 changed files with 366 additions and 0 deletions

180
create_vfs.gd Normal file
View File

@ -0,0 +1,180 @@
# Not unlike factory pattern.
## Recursively creates virtual file systems.
static func create_meta_accessor(file_accessors:Array[VFileAccess])->VFileAccess:
var vfiler := VFileAccess.new()
vfiler._get_stuff = func()->Variant: return file_accessors
vfiler._file_exists = func(path:String)->bool:
return file_accessors.any(func(vfs:VFileAccess)->bool:
return vfs.file_exists(path))
vfiler._get_files_at = func(path:String)->Array[String]:
var accum:Array[String] = []
accum.assign(
file_accessors.reduce(
(func _merge_files(total:Array[String], current:VFileAccess)->Array[String]:
var res:Array[String] = total.duplicate()
for file:String in current.get_files_at(path):
if not file in total:
res.append(file)
return res) ,[]))
return accum
vfiler._get_buffer = func(path:String)->PackedByteArray:
for vfs:VFileAccess in file_accessors:
if vfs.file_exists(path):
return vfs.get_buffer(path)
push_error("File '%s' not found in any checked accessors." % path)
return PackedByteArray()
vfiler._write_file = func _zip_write(_path:String, _buffer:PackedByteArray)->Error:
return ERR_FILE_CANT_WRITE
vfiler._close = func()->void:
file_accessors.map(func(vfs:VFileAccess)->void: vfs.close())
return vfiler
## A basic file accessor.
static func create_file_access(root:String = "./")->VFileAccess:
var vfiler := VFileAccess.new(root)
vfiler._get_buffer = FileAccess.get_file_as_bytes # thanks for being static
vfiler._file_exists = FileAccess.file_exists # thanks for being static
vfiler._write_file = func _file_access_write(path:String, buffer:PackedByteArray)->void:
var abs_path := vfiler.get_absolute_path(path)
if not DirAccess.dir_exists_absolute(abs_path):
DirAccess.make_dir_recursive_absolute(abs_path.get_base_dir())
var f := FileAccess.open(abs_path, FileAccess.WRITE_READ)
f.store_buffer(buffer)
f.close()
vfiler._get_files_at = func(path:String)->Array[String]:
var paths:Array[String] = []
#var abs_path := vfiler.get_absolute_path(path)
if not DirAccess.dir_exists_absolute(path):
print("Path '%s' not found. Adding..." % path)
DirAccess.make_dir_recursive_absolute(path)
paths.assign(DirAccess.get_files_at(path))
return paths
return vfiler
static func create_writeonly_zip_access(
zip_path:String,
append := ZIPPacker.ZipAppend.APPEND_CREATE
)->VFileAccess:
var vfiler := VFileAccess.new()
if not is_instance_valid(vfiler): return null
var writer := ZIPPacker.new()
# Open once to verify it exists and works.
var _open_err := writer.open(zip_path, append)
#if open_err != OK:
#push_error("Could not open zip writer '%s'" % error_string(open_err))
#return null
#writer.close()
vfiler._get_stuff = func()->Variant: return writer
vfiler._write_file = func _zip_write(path:String, buffer:PackedByteArray)->Error:
var abs_path := vfiler.get_absolute_path(path)
writer.start_file(path)
var write_err := writer.write_file(buffer)
writer.close_file()
return write_err
vfiler._get_buffer = func(_path:String)->PackedByteArray:
push_error("Attempted to read from write-only zip access! Returning 0 bytes.")
return PackedByteArray()
vfiler._get_files_at = func(_path:String)->Array[String]:
push_error("Attempted to get files from write-only zip access!")
return []
vfiler._close = func()->void: writer.close()
return vfiler
## A single zip file accessor, but with write functions disabled.
## TODO option to keep zips open, for OS to flag files as in-use.
static func create_readonly_zip_access(zip_path:String)->VFileAccess:
var vfiler := create_bulk_readonly_zip_access([zip_path])
vfiler._get_stuff = func()->Variant: return vfiler.get_stuff()[0]
return vfiler
## A multi-zip readonly accessor. Allows for multiple zips to load with overrides.
## TODO option to keep zips open, for OS to flag files as in-use.
static func create_bulk_readonly_zip_access(
zip_paths:Array[String], keep_open:bool = false,
if_missing_zip := func _ignore(_zip_path:String)->bool: return false
)->VFileAccess:
var vfiler := VFileAccess.new("")
var readers:Array[ZIPReader] = []
readers.assign(zip_paths.map(
func _open_zip_reader(zip_path:String)->ZIPReader:
var zip := ZIPReader.new()
if not FileAccess.file_exists(zip_path): # Use VFileAccess recursively so we are more powerful?
if_missing_zip.call(zip_path)
var open_err := zip.open(zip_path)
if open_err:
push_error("Could not open zip '%s': %s" % [zip_path, error_string(open_err)])
return null
return zip))
readers.assign(readers.filter(is_instance_valid))
vfiler._get_stuff = func()->Variant: return readers
# file exists set first so we can bulk-check later
vfiler._file_exists = func(abs_path:String)->bool:
return readers.any(
func(z:ZIPReader)->bool: return z.file_exists(abs_path))
vfiler._get_buffer = func(abs_path:String)->PackedByteArray:
var output := PackedByteArray()
if not vfiler.file_exists(abs_path):
push_error("Zips don't have file id '%s'" % abs_path)
return output
for index:int in readers.size():
var z := readers[index]
if not z.file_exists(abs_path): continue
output = z.read_file(abs_path)
break
return output
vfiler._write_file = func _zip_write(_path:String, _buffer:PackedByteArray)->Error:
return ERR_FILE_CANT_WRITE
vfiler._close = func()->void:
readers.map(func(z:ZIPReader)->void: z.close())
vfiler._get_files_at = func(path:String)->Array[String]:
if not path.ends_with("/"): # TODO ensure no // as well because that can be problem.
path += "/" # sorry for the mutation
var accum:Array[String] = []
accum.assign( readers.reduce(
(func _merge_files(total:Array[String], current:ZIPReader)->Array[String]:
var res:Array[String] = total.duplicate()
var filtered:Array = []
filtered.assign(current.get_files())
filtered.filter(
func _filter_dir(curpath:String)->bool:
return curpath.begins_with(path))
for file:String in filtered:
var stripped_file:String = file.replace(path, "")
# remove files with / after the file stripping to prevent directories.
if not file in total and not stripped_file.contains("/"):
res.append(file)
return res) ,[]))
return accum
return vfiler

136
vfs.gd Normal file
View File

@ -0,0 +1,136 @@
## Virtual File Access
##
## File access across different methods sucks.
## Here's something to abstract that away and suck a little less.
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] = IMPORTS.DEFAULT_SUPPORTED_FILES
## 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 = "./")->void:
self.root = root_dir
## 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()
## 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
## [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]

50
vfs_loaders.gd Normal file
View File

@ -0,0 +1,50 @@
static func validate_loader(loader:Callable)->bool:
if loader.get_unbound_arguments_count() != 1:
push_error("Cannot add loader for %s as only one String arg can be taken.")
return false
elif loader.get_bound_arguments()[0] is not String \
or loader.get_bound_arguments()[0] is not StringName:
push_error("Cannot add loader for %s as only one String arg can be taken.")
return false
return true
static func load_bin(buffer:PackedByteArray)->PackedByteArray: return buffer
static func load_png(buffer:PackedByteArray)->Image:
var img:Image
img.load_png_from_buffer(buffer)
return img
static func load_mp3(buffer:PackedByteArray)->AudioStreamMP3:
var sfx := AudioStreamMP3.new()
sfx.data = buffer
return sfx
static func load_ogg(buffer:PackedByteArray)->AudioStreamOggVorbis:
var sfx := AudioStreamOggVorbis.new()
sfx.data = buffer
return sfx
static func load_txt(buffer:PackedByteArray)->String:
var txt:String = buffer.get_string_from_utf8()
return txt
# can't use CONST since Callables are technically instanced dynamically
static var DEFAULT_SUPPORTED_FILES:Dictionary[String,Callable] = {
"bin": load_bin,
"png": load_png,
"mp3": load_mp3,
"ogg": load_ogg,
"txt": load_txt,
}
static var AUDIO_FILES:Dictionary[String,Callable] = {
"mp3": load_mp3,
"ogg": load_ogg,
}