File Access (Sync Async) - Grisgram/gml-raptor GitHub Wiki

One of the most important parts of every game, that exceeds the volume of a quick game jam, is the file access.
As you could see in other chapters, there's already some file action going on in raptor, like in LG Localization, Race File Specifications or the Savefile structure.

Aside of that, you will likely have your own data files, you want to access during the game.

raptor offers two groups of file access functions: Sync and Async. Both offer the same signatures, but of course, as the nature of async access is a little different and depends on callbacks, the usage differs a bit.

Tip

In general, you should use async file access all the time, there's no real reason to do it synchronously.
Even more, on consoles you have to access all files asynchronously, every other access is denied on these platforms.
With Release 2407, every internal file access of raptor has also been converted to async file access.

Important Macros For The File Subsystem

There are four macros, you should spend a little attention, when setting up your project. All of them can be found quite at the top of the Game_Configuration script of the _GAME_SETUP_ folder of the project template.

Macro Default Description
GAME_FILE_PREFIX "gml_raptor" This is the prefix for all raptor-internal temp files and for the game settings file.
You want to change this to some name fitting your game, but keep it short
DATA_FILE_EXTENSION
release:DATA_FILE_EXTENSION
".json"
".jx"
Use this macro for every json file you access, because in release mode, the build-script will launch the raptor json compiler tool, introduced with Release 2405 and compile your json files to .jx files (or whatever extension you define here), which is more or less just an encryption of the files, so users can not tinker with your data files easily.
FILE_CRYPT_KEY
release:FILE_CRYPT_KEY
""
"<some long salt>"
The raptor json compiler will look for this crypt key to encrypt your data files during release compile. In debug/default configuration, this is an empty string, so your files are not encrypted and you can modify and read them freely, which is great for debugging. In beta/release mode, the json compiler will encrypt your files, so your end users can't see plain text
GAME_SETTINGS_FILENAME $"<pre>_game_settings<ext>" This is a composed string of the other macro values and will define the name of your game settings file for the game and doesn't need to be changed under normal circumstances

File Access Functions In Raptor

As mentioned at the top of this page, both systems, synchronous and asynchronous, offer the same signatures. The async functions have an _async postfix appended to their name, which is quite industry standard.

/// @func	file_read_text_file(filename, cryptkey = "", remove_utf8_bom = true, add_to_cache = false)
/// @desc	Reads an entire file and returns the contents as string
///		checks whether the file exists, and if not, undefined returned.
///		Returns undefined, if the file is not a text file
function file_read_text_file      (filename, cryptkey = "", remove_utf8_bom = true, add_to_cache = false) {
///		If the file does not exist or an exception occurs, the on_failed callback gets invoked
function file_read_text_file_async(filename, cryptkey = "", remove_utf8_bom = true, add_to_cache = false) {
/// @func	file_read_text_file_lines(filename, cryptkey = "", remove_empty_lines = true, 
///					  remove_utf8_bom = true, add_to_cache = false)
/// @desc	Reads an entire file and returns the contents as string array, line by line
///		Checks whether the file exists, and if not, undefined returned.
///		Returns undefined, if the file is not a text file
function file_read_text_file_lines      (filename, cryptkey = "", remove_empty_lines = true, 
					 remove_utf8_bom = true, add_to_cache = false) {
///		If the file does not exist or an exception occurs, the on_failed callback gets invoked
function file_read_text_file_lines_async(filename, cryptkey = "", remove_empty_lines = true, 
					 remove_utf8_bom = true, add_to_cache = false) {
/// @func	file_write_text_file(filename, text, cryptkey = "")
/// @desc	Saves a given text as a plain text file. Can write any string, not only json.
function file_write_text_file      (filename, text, cryptkey = "") {
///		If an exception occurs, the on_failed callback gets invoked
function file_write_text_file_async(filename, text, cryptkey = "") {
/// @func	file_write_text_file_lines(filename, lines_array, cryptkey = "", line_delimiter = "\n")
/// @desc	Saves a given string array as a plain text file.
function file_write_text_file_lines      (filename, lines_array, cryptkey = "", line_delimiter = "\n") {
///		If an exception occurs, the on_failed callback gets invoked
function file_write_text_file_lines_async(filename, lines_array, cryptkey = "", line_delimiter = "\n") {
/// @func	file_read_struct(filename, cryptkey = "", add_to_cache = false)
/// @desc	Reads a given struct from a file, optionally encrypted
function file_read_struct      (filename, cryptkey = "", add_to_cache = false) {
///		If the file does not exist or an exception occurs, the on_failed callback gets invoked
function file_read_struct_async(filename, cryptkey = "", add_to_cache = false) {
/// @func	file_write_struct(filename, struct, cryptkey = "")
/// @desc	Saves a given struct to a file, optionally encrypted
function file_write_struct      (filename, struct, cryptkey = "") {
///		If an exception occurs, the on_failed callback gets invoked
function file_write_struct_async(filename, struct, cryptkey = "") {

Note

The following function is meant to be used, when Sandbox in GameMaker is disabled and will mostly be used, when you write tools, but not in actual games.
Information about the _attributes argument can be found at the GameMaker Attributes Page.

/// @func	file_list_directory(_folder = "", _wildcard = "*.*", _recursive = false, attributes = 0)
/// @desc	List all matching files from a directory in an array, optionally recursive
///		_attributes	is one of the attr constants according to yoyo manual
///		
function file_list_directory(_folder = "", _wildcard = "*.*", _recursive = false, _attributes = 0) {

Why is there a "write_text" and "write_struct" function?

The reason is, that behind the read/write_struct functions, a very powerful feature of raptor is hiding: Class reconstruction.

There is a command, construct(...) in raptor, which will set a marker on all struct classes, so they can be recreated when loaded from a file (i.e. their constructor gets invoked instead of just creating an anonymous struct instance.

To use this feature, just create your struct classes like this:

No reconstruction possible Reconstruction possible
image image

Just by putting this one line in your class, it gets serializable and will be reconstructed, when you load it from a file.
Keep this in mind, when you create your classes.
You can see many many applications of this throughout the raptor source code. Almost all script classes are reconstructible.

Invoking The Async Functions

While invoking the Sync functions is a simple, straightforward function call and needs no explanation, the async mechanism depends on a callback to be invoked, when the async operation finished.

All async functions follow this scheme:\

file_some_function_async(<your arguments>)
.set_data("property", "value")   <-- Transport any data value from your current function to the callback
                                     Repeat as often as you like. In the callback, _data contains all the values
.on_finished(function(_result, _data) {
})
[optional!]
.on_failed(function(_data) {
})
.start()    <-- NEVER FORGET THIS!
;

All the async functions return an instance of a raptor-internal __FileAsyncWorker class, which offers these callbacks. They get invoked, when the async operation finishes.
In case, you want to look deeper into the source code: The Async Save/Load event of GameMaker is handled by the GAMECONTROLLER persistent object of raptor. This is the one, communicating with the AsyncWorkers.

Note

The on_failed callback only gets invoked, if an Exception occurred (or the file does not exist in a read_*_async call) during the async operation! That's why it has been marked as optional in the code scheme above.
Content/parsing fails end up in normal calls to .on_finished, but the _result argument will contain undefined

Tip

One common mistake, when using the _async functions, is, that you forget, that you are working with a builder pattern and therefore forget to call .start() as last action!

Multiple Callbacks

As the async functions follow the builder pattern, every method (including .start()!) return self, so you can build chains of calls.

This is important to know, because both, the .on_finished and the .on_failed callback registrations functions allow multiple callbacks to be registered! They will be invoked in the order they are registered.

Where is this important?

Let's take the Savegame System as an example. It works async, as any other submodule of raptor, which means, at the moment, where you invoke savegame_save_game(...), your game is not saved! It just started saving. And while there are callbacks/overrides available in each Saveable Object, you might want to have a specific callback to be invoked, when saving finished, and not one in each single saveable instance.

So, even when the savegame system internally already uses a .on_finished callback, you still can add your own!

savegame_save_game("game.sav", FILE_CRYPT_KEY)
.on_finished(function(result, data) {
    ilog($"Game saved. Success = {result}");
});

This callback will be invoked after the raptor-internal callback, where all the Saveable callbacks are invoked and the user events are launched. But still, you can have your own callback, when the operation finished. And this is a great async feature of raptor!

What does the _result contain?

That totally depends on the function you called, but is considered to be intuitive (like you would expect a string[], when you read a file line-by-line and a string containing the whole file if you do not read it line-by-line).

Function Result contains
Any *_write_* A bool telling you the success state
read_lines A string[] containing all the file lines or undefined, if a file could not be read
read_file A string holding the entire contents of the file or undefined, if a file could not be read
read_struct A struct that has been parsed out of the file. If the struct contained a construct marker, it even has its original type, as the constructor has been invoked

What does the _data contain?

Whatever you like! Use the .set_data("property", value) function to fill an internal struct of the async worker. This is the content of your _data parameter. Use it, to transport any local variables, that might exist only in your local function to the callback.

⚠️ **GitHub.com Fallback** ⚠️