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.
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 |
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) {
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 |
---|---|
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.
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!
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.
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!
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 |
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.