Skip to content

Instantly share code, notes, and snippets.

@nommiin
Last active April 7, 2022 18:01
Show Gist options
  • Select an option

  • Save nommiin/046634c4bd60d6d421dd71432e613a8c to your computer and use it in GitHub Desktop.

Select an option

Save nommiin/046634c4bd60d6d421dd71432e613a8c to your computer and use it in GitHub Desktop.
A simple set of classes for creating a serializable virtual filesystem for use primarily with networking
/*
blob - 4/4/2022
by Nommiin
Getting Started:
- Usage:
(HOST)
1. Create a new Blob by calling "my_blob = new Blob(<name>, [root=""])"
2. Add files to the blob by calling "my_blob.AddFile(<file>)"
- NOTE: .AddFile paths are relative to the "root" argument provided in the Blob constructor
.AddFile will also return the content of the file after loading or reading cache
3. Use "my_blob.GetFile(<file>)" to read file contents
4. Call "my_blob.Serialize()" to write all files to a buffer
- NOTE: .Serialize will return the newly created buffer
.Serialize will not re-serialize the buffer if no new files were added since last call
You can access the blob's buffer using the ".Buffer" property
(CLIENT)
1. Create a new Blob by calling "my_blob = new Blob()"
2. Call "my_blob.Deserialize(<buffer>)" to load a buffer containing a blob
- NOTE: .Deserialize will set the blob's root, so any future .AddFile calls will be relative to this root
3. Use "my_blob.GetFile(<file>)" to read files from the blob
- Extending:
Due to how the entry serialization works with Blob, you'll need to explicitlly add support for different file
extensions along with respective BlobEntry types. Doing so is relatively simple, and can be done as follows:
1. Create a new class that inherits from the BlobEntry class, provide the "name" and "root" arguments
2. Override the "GetContent" method to process entry data accordingly
- NOTE: If handling less complex/plaintext files, you can skip making a new class and use the "BlobEntryText" class
3. Edit the "Mapping" array at line 39 and add your new entry class as "Entry" and respective filetype as "Type"
*/
/// @function Blob( name, root )
/// @description A class that can be used to store files in a virtual file system
/// @argument {string} name - The name of the blob
/// @argument {string} root - The root directory for files
function Blob( name, root ) constructor {
static Mapping = [
{Type: ".txt", Entry: BlobEntryText},
{Type: ".json", Entry: BlobEntryObject},
{Type: ".png", Entry: BlobEntryImage},
{Type: ".lua", Entry: BlobEntryText},
{Type: ".moon", Entry: BlobEntryText},
];
Name = "blob:" + (name ?? "unnamed");
Root = root ?? "";
Buffer = buffer_create(1, buffer_grow, 1);
BufferCompressed = undefined;
BufferStale = false;
/// @function Serialize()
/// @description Writes blob info and entries into buffer
/// @returns buffer - The serialized data
static Serialize = function() {
if (BufferStale) {
buffer_seek(Buffer, buffer_seek_start, 0);
buffer_write(Buffer, buffer_string, Name);
buffer_write(Buffer, buffer_string, Root);
buffer_write(Buffer, buffer_u32, array_length(Entries));
for(var i = 0; i < array_length(Entries); i++) {
var entry = Entries[i];
buffer_write(Buffer, buffer_string, entry.Type);
buffer_write(Buffer, buffer_string, entry.Name);
buffer_write(Buffer, buffer_u32, entry.Size);
entry.Base = buffer_tell(Buffer);
buffer_write(Buffer, buffer_u32, 0);
}
for(var i = 0; i < array_length(Entries); i++) {
var entry = Entries[i], base = buffer_tell(Buffer);
if (buffer_get_size(Buffer) < base + entry.Size) {
buffer_resize(Buffer, base + entry.Size);
}
buffer_copy(entry.Data, 0, entry.Size, Buffer, base);
buffer_poke(Buffer, entry.Base, buffer_u32, base);
buffer_seek(Buffer, buffer_seek_relative, entry.Size);
}
if (DEBUG) Log.WriteDebug("Blob Size: {0} bytes", string(buffer_tell(Buffer)));
BufferStale = false;
if (BufferCompressed != undefined) buffer_delete(BufferCompressed);
BufferCompressed = buffer_compress(Buffer, 0, buffer_get_size(Buffer));
if (DEBUG) Log.WriteDebug("Blob Size (Compressed): {0} bytes", string(buffer_get_size(BufferCompressed)));
}
return BufferCompressed;
}
/// @function Deserialize( buffer )
/// @description Takes a buffer and reads blob info and entries from it
/// @argument {buffer} buffer - The buffer to read from
/// @returns {array} entries - An array of file entries, use GetFile to read
static Deserialize = function( buffer ) {
if (array_length(Entries) == 0) {
buffer_seek(buffer, buffer_seek_start, 0);
Name = buffer_read(buffer, buffer_string);
Root = buffer_read(buffer, buffer_string);
Entries = array_create(buffer_read(buffer, buffer_u32));
for(var i = 0; i < array_length(Entries); i++) {
var type = buffer_read(buffer, buffer_string), entry = asset_get_index(type);
if (entry != -1) {
var name = buffer_read(buffer, buffer_string);
entry = new entry(Root, name);
entry.Deserialize(buffer);
Entries[i] = entry;
} else {
throw new Exception("Could not handle unknown type: " + type);
}
}
} else {
throw new Exception("Could not deserialize new Blob, the Entries array is not empty");
}
return Entries;
}
Entries = [];
/// @function AddFile( file )
/// @description Loads the given file and adds it to blob's entries
/// @argument {string} file - The file to load
/// @returns {any} content - The content of the file if loaded
static AddFile = function( file ) {
var entry = GetFile(file);
if (entry == undefined) {
if (file_exists(Root + file)) {
var ext = filename_ext(file);
for(var i = 0; i < array_length(Mapping); i++) {
var mapping = Mapping[i];
if (mapping.Type == ext) {
entry = new mapping.Entry(Root, file);
array_push(Entries, entry.Serialize());
BufferStale = true;
break;
}
}
if (entry == undefined) {
throw new Exception("Could not handle unknown file type: " + ext);
}
}
}
Log.WriteInfo("Added file \"{0}\" to blob:{1}//{2}", file, Name, Root);
return entry.GetContent();
}
/// @function GetFile( file )
/// @description Retrieves the given file from the blob
/// @argument {string} file - The file to retrieve
/// @returns {any} content - The content of the file if found
static GetFile = function( file ) {
for(var i = 0; i < array_length(Entries); i++) {
var entry = Entries[i];
if (entry.Name == file) {
return entry.GetContent();
}
}
return undefined;
}
/// @function RemoveFile( file )
/// @description Removes the given file from the blob
/// @argument {string} file - The file to remove
/// @returns {bool} success - If the file was successfully found and removed from the blob
static RemoveFile = function( file ) {
for(var i = 0; i < array_length(Entries); i++) {
var entry = Entries[i];
if (entry.Name == file) {
array_delete(Entries, i, 1);
return true;
}
}
return false;
}
/// @function AddEntry( file, type, content )
/// @description Manually writes content into an entry in the blob [TODO: im high so this comment sucks]
/// @argument {string} file - The filename to add entry as
/// @argument {string} type - The BlobEntry class to write as
/// @argument {any} content - The content to write
function AddEntry( file, type, content ) {
var entry = new BlobEntry(Root, file);
entry.Data = buffer_create(1, buffer_grow, 1);
buffer_write(entry.Data, buffer_string, string(content));
buffer_resize(entry.Data, buffer_tell(entry.Data));
entry.Size = buffer_get_size(entry.Data);
array_push(Entries, entry);
BufferStale = true;
return content;
}
}
/// @function BlobEntry( root, file )
/// @description Generic class for blob entries, should be inherited by new entry types; Type must be the same as the class name
/// @argument {string} root - The root directory for files
/// @argument {string} file - The name of the file
function BlobEntry( root, file ) constructor {
Type = "BlobEntry";
Root = root;
Name = file;
Base = -1;
Data = undefined;
Size = 0;
static Serialize = function() {
Data = buffer_load(Root + Name);
Size = buffer_get_size(Data);
buffer_seek(Data, buffer_seek_start, 0);
return self;
}
static Deserialize = function( buffer ) {
Size = buffer_read(buffer, buffer_u32);
Base = buffer_read(buffer, buffer_u32);
Data = buffer_create(Size, buffer_fixed, 1);
buffer_copy(buffer, Base, Size, Data, 0);
buffer_seek(Data, buffer_seek_start, 0);
}
Content = undefined;
static GetContent = function() {
throw new Exception("Could not call GetContent method for generic BlobEntry");
}
}
/// @function BlobEntryImage()
/// @description An example of a blob entry class, loads a PNG and adds it as a sprite for Content property
function BlobEntryImage( root, file ) : BlobEntry( root, file ) constructor {
Type = "BlobEntryImage";
static GetContent = function() {
if (Content == undefined) {
buffer_save(Data, "temp.png");
Content = sprite_add("temp.png", 1, false, false, 0, 0);
file_delete("temp.png");
}
return Content;
}
}
/// @function BlobEntryImage()
/// @description An example of a blob entry class, loads a JSON file and stores parsed file as Content property
function BlobEntryObject( root, file ) : BlobEntry( root, file ) constructor {
Type = "BlobEntryObject";
static GetContent = function() {
if (Content == undefined) {
var json = buffer_read(Data, buffer_text);
Content = json_parse(json);
}
return Content;
}
}
/// @function BlobEntryText()
/// @description An example of a blob entry class, loads a file as text for Content property
function BlobEntryText( root, file ) : BlobEntry( root, file ) constructor {
Type = "BlobEntryText";
static GetContent = function() {
if (Content == undefined) {
Content = buffer_read(Data, buffer_text);
}
return Content;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment