GD-VirtualFileAccess/README.md
2024-12-03 13:57:10 -05:00

78 lines
6.1 KiB
Markdown

# GD-VirtualFileAccess
A small set of scripts to make loading data from various sources less awful.
**Note:** that this is under active development and probably will change occasionally for a while.
**Note II:** Backup stuff before you play with this. I'm not taking the bullet because you accidentally overwrote your computer with a single hello world text file.
**Note III & Knuckles:** This might work in Godot 4.3 and below, but you will need to remove all the instances of type-safe Dictionaries. This primarily is used to keep importer maps clean.
## I don't get it.
Crop and Claw 2 is being written with moddability and engine-agnosticism in mind, using a combination of one or many zip archives and standard file access. There is a custom editor and runtime, both written as Godot games, which need to share some common file loading tricks. VFileAccess is written as a container class for abstracting away some file IO to allow for some brief and simple interfacing with uncertain sources of data.
For example, the runtime loads the base game package it expects, then mods, then allows for listening to a mods directory outside of zip archives. To do all this, we need to be able to load various file formats (png, ogg, specialized non-Godot formats, etc) into the engine and convert them to usable assets. This is obnoxious because loading and searching for files is different depending on if you are loading from directories or zip archives, and you have to manually cascade through zips and directories to find override assets. What's worse is more complications are added when new sources of file data could be encountered, such as streaming assets directly from a server. In order to reduce the pains, these scripts help build a dynamic virtual file loader.
Currently supported loaders:
- FileAccess - for direct file/directory operations (Read/Write)
- ZIPReader - to read one or many zip files at a time (Read)
- ZIPWriter - to write to a target zip file. (Write)
- VFileAccess - Yes, you can recursively use multiple VFileAccess instances to check for files using different methods. (Read)
To be implemented:
- HTTP requests - coroutine-based streaming a file from a network connection. (Read)
## This thing's confusing.
No. I just hacked the code together quickly. Suppose you want to a folder or two...
```
# Open a folder
var vfiler:VFileAccess = VFileAccess.CREATE.create_file_access("/path/to/gamer/dir/")
var message:String = vfiler.load_supported("file.txt")
# Need to load from a zip? Reserve access with this...
var vfiler:VFileAccess = VFileAccess.CREATE.create_readonly_zip_access("./gamer.zip")
var message:String = vfiler.load_supported("file.txt")
# Need to load multiple zip files that can let you check each for a single file and return the first instance found?
var vfiler:VFileAccess = VFileAccess.CREATE.create_bulk_readonly_zip_access(["mods/mod.zip","./gamer.zip"])
var message:String = vfiler.load_supported("file.txt") # first zip to contain this will return, null otherwise.
```
# Adding supported file extensions.
There's two ways to obtain data from a file. `VFileAccess.get_buffer("filename.txt)` returns a `PackedByteArray` which you can parse yourself. `VFileAccess.load_supported("filename.txt")` will allow you to automatically receive a Variant based on the file extension. You can pass a second argument in with an override extension, such as ("filename.dat", "txt"), and receive a String as if it were just a .txt.
The power of this comes from how supported extensions are built. VFileAccess does not have any built-in context for file loading. Instead, you can define custom loaders and pass them in as arguments. By default, `VFileAccess.IMPORTS.DEFAULT_SUPPORTED_FILES` is used. This can be overridden when using the default creator functions, or passed in when writing your own. See `vfs_imports.gd` for options.
```
# allow importing all default supported files
var vfiler:VFileAccess = VFileAccess.CREATE.create_file_access("/path/to/gamer/dir/", VFileAccess.IMPORTS.DEFAULT_SUPPORTED_FILES)
# only allow importing images
var vfiler:VFileAccess = VFileAccess.CREATE.create_file_access("/path/to/gamer/dir/", VFileAccess.IMPORTS.IMAGE_FILES)
# only allow a custom format you defined elsewhere in your program
static func load_custom_file_format(buffer:PackedByteArray)->Object:
# parse bytes into a usable data type here.
return Object.new()
var my_imports:Dictionary[String,Callable] = {"cff", load_custom_file_format}
var vfiler:VFileAccess = VFileAccess.CREATE.create_file_access("/path/to/gamer/dir/", my_imports)
var result = vfiler.load_supported("file.cff")
```
This is extremely powerful since you have no need to modify the source code for VFileAccess nor its dependency scripts. You can freely pass your own importers or integrate other plugins for importers, as long as the import function you use takes a `PackedByteArray` and returns something of value.
# Writing custom virtual accessors.
VFileAccess does not use inheritance, but rather a factory and first class functions, to override behavior. This allows for customizing and compounding behavior very easily. Although the code as of writing in `create_vfs.gd` are long and staircased, they can pretty easily be split out separately or even faked to appear like subclasses if you really wanted to. Context for specific VFileAccess instances are accessible by the amazingly named `get_stuff()` which will return a Variant. This acts as a closure holding data such as ZIPReader/ZIPWriter. You can return whatever context for your loader you see fit. It is preferable you don't actually rely on this though, as part of why VFileAccess is so universally usable is because you don't need to know or care about context at all. Since there is no inheritance, there is no need to worry about what specific subclass of VFileAccess is.
You best actually read the VFileAccess definition and skim the creator functions to figure out how to implement your own. Remember that there is no need to add your own creators or importers directly to VFileAccess's scripts, because it is designed to let you define those elsewhere in your project. This lets you update these scripts with less hassle of reimplementing your own changes.