Data file management and caching for GameMaker
by @shdwcat
Using gumshoe by @jujuadams and @nkrapivin
Chat about Cabinet on the Discord server
Cabinet exists to help manage access to data files in GameMaker, including features like caching the contents of a file so that you only have to read it once, as well as being able to convert the raw file contents into something else useful, like converting a JSON file directly into a struct, or decoding binary data. Once you setup a Cabinet, you can simply access the contents of the files you want, without having to manage the reading or caching yourself.
Cabinet was made with GameMaker 2022.3. It may be compatible with earlier versions of GameMaker (possibly with some manual tweaking).
To get started, you'll create a Cabinet for a folder path. You can specify a specific file extension, in which case only files with that extension will be included in the cabinet. And you can also specify some more advanced options (see Cabinet Options below)
var cabinet = new Cabinet(folder_path, [extension], [options])Once you've created the Cabinet, you can now inspect the tree variable, which contains a tree struct describing folders and files within the folder_path for the Cabinet, and the flat_map variable, which is a flat struct of just the files. The values in the tree and the flat_map are CabinetFile structs which provide access to further details and functionality.
For example, you could get the CabinetFile for the first file that was found like so:
var first_path = cabinet.file_list[0];
var cabinet_file = cabinet.file(first_path);And then read the contents with:
var contents = cabinet_file.tryRead();Caching is enabled by default, so the first time you call tryRead() the file will be read from disk, but the second time you call it, tryRead() will returned the value from the cache that was stored after the first call. This can be helpful if you find yourself needing to read the same file from different places in code, without being sure if you've loaded it yet.
Simply caching the file contents is nice, but if you're working with anything more complex than simple text, you're going to want to turn that text into game data that you can use.
To get Cabinet to do this automatically, you'll want to specify a file_value_generator in the Cabinet Options:
// passing "" as the folder_path will scan all files in the /datafiles folder, as it is the default directory when reading files in GameMaker
var json_cabinet = new Cabinet("", ".json", {
file_value_generator: function(text, cabinet_file) {
var data = json_parse(text);
return data;
},
});Now our json_cabinet will automatically parse the raw JSON text of any file into a struct with json_parse(), when reading the file. This means that when we ask for the file contents, we'll get that struct instead of a string:
var json_data = cabinet_file.tryRead();
// json_data will be the struct that was parsed by json_parse() in the file_value_generator function provided in the optionsJust getting the struct is nice, but you could also then pass that data to a GML constructor function:
file_value_generator: function(text, cabinet_file) {
var data = json_parse(text);
var enemy_definition = new EnemyDefinition(data);
return enemy_definition;
},Remember, when caching is enabled, the value will only be generated once. The file_value_generator function will run the first time the data is read from disk, and after that .tryRead() will return the result of that function, which is what was stored in the cache.
function Cabinet(folder_path, extension = ".*", options = undefined) constructorfolder_path(string) The path of the folder that Cabinet should scan for filesextension(string, optional) The extension of the file type that should be included in the cabinet.options(struct, optional) The options to use for this cabinet. See Cabinet Options.
tree(struct) Tree structure corresponding to the folders and files that were found in scanned folder. The keys are the names of the folders/files. The value for a folder key will be a struct with more folders and files within it. The value for a file key will be a CabinetFile providing data and functions for the actual file on disk.flat_map(struct) Similar to thetreevariable, except the keys will be the full paths of every file in theCabinet, and each value will be the correspondingCabinetFile.file_list(string array) A flat array with the full path of every file in theCabinet.
file(path)- Returns the CabinetFile for thepathif it exists in theCabinet, otherwiseundefinedwill be returned.readFile(path)- Returns the content of the file at thepathif it exists in theCabinet, otherwiseundefinedwill be returned (the content can be customized by providing afile_value_generatorin the Cabinet Options).clearCache()- Clears all cached data for theCabinetrescan()- Rebuilds all of the Variables for thisCabinetby scanning thefolder_pathagain.
You don't need to create CabinetFiles yourself as they are created automatically by a Cabinet.
fullpath(string) the full path to the associated filedirectory(string) the path of the directory containing the filefile(string) the name of the file, including the extensionextension(string) just the extension of the filefile_id(string) the name of the file, without the extensionscan_time(datetime) when the file was scanned by theCabinet, either on creation or by callingrescan()read_time(datetime) the time when the file was read and cached, orundefinedif it has not been read and cached yet
tryRead()- Returns the content of the associated file (the content can be customized by providing afile_value_generator). If the file has been cached, the cached value will be returned, otherwise, the file will be read from disk (and possibly parsed), if it still exists on disk, otherwiseundefinedwill be returned.tryLoad()- If the file exists on disk, it will be read, possibly parsed, cached and returned. Otherwiseundefinedwill be returned. Note: The file will be cached even if thecache-readsoption is false.tryScanLines(match_line)- (Advanced) This function will read the associated text one line at a time, and call the providedmatch_line(line)function. If thematch_linefunction returns a value, that value will be returned bytryScanLines(), otherwise the next line will be read. If no line was matched,undefinedwill be returned.- NOTE: This function is only valid when the
read_modeoption is set to"string". SeeCabinet Options. - TIP: This function can be useful for quickly reading 'header' information from a long file without actually needing to read the whole file into memory.
- NOTE: This function is only valid when the
When calling the Cabinet constructor, you can pass a struct specifying the following options to use for that Cabinet.
-
cache_reads(boolean, default:true) - Whether to cache the contents of a file after reading it from disk. -
read_mode("string"or"binary", default:"string") - Whether to read the raw file content as astringor as a binarybuffer.- This affects the type of the first parameter in the
file_value_generatorfunction option.
- This affects the type of the first parameter in the
-
file_value_generator(function(content, cabinet_file), default:undefined) - Function to convert the raw file content to a custom value of your choice (and implementation).- When
read_modeis"string"thecontentparameter of the function will be the file content as a string. - When
read_modeis"binary"thecontentparameter of the function will be the file content as a gmlbuffer. - The
CabinetFileis provided as the second parameter of the function, in case any of the information in it is useful.
- When
-
cabinet_file_customizer(function(cabinet_file), default:undefined) - Function that is called when theCabinetscans thefolder_pathfor files and createsCabinetFiles.- Here you can set additional values on the
cabinet_fileparameter as may be helpful. For example, you might want to calltryScanLines()to read header information from the file and store that data on thecabinet_filefor future reference.
- Here you can set additional values on the
Cabinet utilizes the gumshoe library by @jujuadams to scan files on disk, and therefore a version of that code is included in this project. If you're already using gumshoe in your project, make sure to replace it with the Cabinet version when importing the Cabinet package.