This is a function "cheat sheet" for the Quality Structures extension by YellowAfterlife.
The extension can be found on itch.io or GM Marketplace.
A most up-to-date version of the manual is always available online.

Click on sections to expand/collapse them.
Quick display controls: Categories · Sections · Everything ·

Overview

This extension comes in two versions

  • quality_structures_one
    The extension itself, covering all features described here and in the blog post.
  • quality_structures_zero
    Has identical API to QS1, but forwards function calls to their respective built-in functions at compile time.

    For extremely performance-critical projects, you can swap QS1 for QS0 once you have established that there are no more data structure issues left (or use Configurations in GM to have debug/release options).

As you can guess, this scheme ensures that migrating your game code to use qs_ functions will not have any negative consequences.

See the blog post for more information.

Equivalent functions

The extension provides equivalents for basically every built-in data structure function.

There are no noticeable difference to how these work aside of added safety checks when using quality_structures_one.

Here's a list:

qs_grid_add(id,x,y,val)
qs_grid_add_disk(id,xm,ym,r,val)
qs_grid_add_grid_region(id,source,x1,y1,x2,y2,xpos,ypos)
qs_grid_add_region(id,x1,y1,x2,y2,val)
qs_grid_clear(id,val)
qs_grid_copy(id,source)
qs_grid_create(w,h)
qs_grid_destroy(grid)
qs_grid_get(id,x,y)
qs_grid_get_disk_max(id,xm,ym,r)
qs_grid_get_disk_mean(id,xm,ym,r)
qs_grid_get_disk_min(id,xm,ym,r)
qs_grid_get_disk_sum(id,xm,ym,r)
qs_grid_get_max(id,x1,y1,x2,y2)
qs_grid_get_mean(id,x1,y1,x2,y2)
qs_grid_get_min(id,x1,y1,x2,y2)
qs_grid_get_sum(id,x1,y1,x2,y2)
qs_grid_height(id)
qs_grid_multiply(id,x,y,val)
qs_grid_multiply_disk(id,xm,ym,r,val)
qs_grid_multiply_grid_region(id,source,x1,y1,x2,y2,xpos,ypos)
qs_grid_multiply_region(id,x1,y1,x2,y2,val)
qs_grid_resize(id,w,h)
qs_grid_set(id,x,y,val)
qs_grid_set_disk(id,xm,ym,r,val)
qs_grid_set_grid_region(id,source,x1,y1,x2,y2,xpos,ypos)
qs_grid_set_region(id,x1,y1,x2,y2,val)
qs_grid_shuffle(id)
qs_grid_sort(id,column,ascending)
qs_grid_value_disk_exists(id,xm,ym,r,val)
qs_grid_value_disk_x(id,xm,ym,r,val)
qs_grid_value_disk_y(id,xm,ym,r,val)
qs_grid_value_exists(id,x1,y1,x2,y2,val)
qs_grid_value_x(id,x1,y1,x2,y2,val)
qs_grid_value_y(id,x1,y1,x2,y2,val)
qs_grid_width(id)
qs_grid_write(id)
qs_grid_read(id,str)

qs_list_add(list, ...values)
qs_list_clear(id)
qs_list_copy(id,source)
qs_list_create()
qs_list_delete(id,pos)
qs_list_destroy(list)
qs_list_empty(id)
qs_list_find_index(id,value)
qs_list_find_value(id,pos)
qs_list_insert(id,pos,value)
qs_list_mark_as_list(id, pos)
qs_list_mark_as_map(id, pos)
qs_list_replace(id,pos,value)
qs_list_set(id,pos,value)
qs_list_shuffle(id)
qs_list_size(id)
qs_list_sort(id,ascending)
qs_list_write(id)
qs_list_read(id,str)
qs_list_set(id,pos,value)

qs_map_add_list(id, key, list_value)
qs_map_add_map(id, key, map_value)
qs_map_clear(id)
qs_map_copy(id,source)
qs_map_create()
qs_map_delete(id,key)
qs_map_destroy(map)
qs_map_empty(id)
qs_map_exists(id,key)
qs_map_find_first(id)
qs_map_find_last(id)
qs_map_find_next(id,key)
qs_map_find_previous(id,key)
qs_map_find_value(id,key)
qs_map_replace_list(id, key, list_value)
qs_map_replace_map(id, key, map_value)
qs_map_set(id,key,value)
qs_map_size(id)
qs_map_write(id)
qs_map_read(id,str)
qs_map_secure_save(id, filename)
qs_map_secure_load(filename)
qs_map_secure_load_buffer(buffer)
qs_map_secure_save_buffer(id,buffer)

qs_priority_add(id,value,priority)
qs_priority_change_priority(id,value,priority)
qs_priority_clear(id)
qs_priority_copy(id,source)
qs_priority_create()
qs_priority_delete_max(id)
qs_priority_delete_min(id)
qs_priority_delete_value(id,value)
qs_priority_destroy(priority)
qs_priority_empty(id)
qs_priority_find_max(id)
qs_priority_find_min(id)
qs_priority_find_priority(id,value)
qs_priority_size(id)
qs_priority_write(id)
qs_priority_read(id,str)

qs_queue_clear(id)
qs_queue_copy(id,source)
qs_queue_create()
qs_queue_dequeue(id)
qs_queue_destroy(queue)
qs_queue_empty(id)
qs_queue_enqueue(queue, ...values)
qs_queue_head(id)
qs_queue_size(id)
qs_queue_tail(id)
qs_queue_write(id)
qs_queue_read(id,str)

qs_stack_clear(id)
qs_stack_copy(id,source)
qs_stack_create()
qs_stack_destroy(stack)
qs_stack_empty(id)
qs_stack_pop(id)
qs_stack_push(stack, ...values)
qs_stack_size(id)
qs_stack_top(id)
qs_stack_write(id)
qs_stack_read(id,str)
JSON

A few notes on JSON functions in QS.

qs_json_encode(map)string

Acts almost identically to the built-in json_encode, but takes a QS map and has a number of debug options.

qs_json_decode(json_string)map

Much like the built-in json_decode, except:

  • Returned values are qs_ structures.
  • If an error encountered, returns qs_json_error
var q = qs_json_decode(@'{"list":["one","two","three"]}');
show_debug_message(qs_get(q, "list", 1)); // "two"
qs_json_error

Returned by qs_json_decode if JSON is invalid.

A convenience macro.

var q = qs_json_decode(...);
if (q != qs_json_error) {
    // OK!
} else {
    // an error occurred
}
Chained accessors
qs_get(qs, ...indexes)value

Does a chained read on one or more data structures.

The way items are accessed depends on types of arguments:

  • string: uses qs_map_find_value
  • number: uses qs_list_find_value
  • qs_xy: uses qs_grid_get
  • (anything else): throws an error

So, for example,

var q = qs_json_decode(@'{"list":["zero", "one", "two"]}');
show_debug_message(qs_get(q, "list", 1)); // "one"

would be equivalent to

var q = qs_json_decode(@'{"list":["zero", "one", "two"]}');
var list = qs_map_find_value(q, "list");
show_debug_message(ds_list_find_value(list, 1)); // "one"
qs_set(qs, ...indexes, value

Akin to qs_get, but will change an item in the last structure.

So, for example,

var q = qs_json_decode(@'{"list":["zero", "one", "two"]}');
qs_set(q, "list", 1, "hi");
show_debug_message(qs_get(q, "list", 1)); // "hi"

would be equivalent to

var q = qs_json_decode(@'{"list":["zero", "one", "two"]}');
var list = qs_map_find_value(q, "list");
qs_list_set(list, 1, "hi");
show_debug_message(qs_get(q, "list", 1)); // "hi"
qs_xy(x, y)

Forms an XY pair for accessing grid items through qs_get/qs_set.

Note that these are actively pooled so you should not use them for any other purposes.

Options

The following is a set of variables that allow to customize the behaviour of quality_structures_one extension.

qs_debug_callstack

If set to true (default), creating and destroying data structures records where they have been created/destroyed from, which can be used to aid with discovering origins of memory leaks or unexpected behaviour.

qs_debug_json_encode

QS can alert you about a variety of common issues in qs_json_encode calls, such as

  • Writing lists/maps without having marked them as such
  • Writing cyclic data structures
  • Using non-string/number keys (undefined behaviour)
  • Writing arrays as JSON values (undefined behaviour)
  • Writing values that have no JSON equivalent (undefined behaviour)

Unlike above, this works as an "error-level",

  • 0: doesn't do anything
  • 1: calls show_debug_message(...) (default)
  • 2: calls show_error(..., false)
  • 3: calls show_error(..., true)
qs_debug_base16_write

Much like to qs_debug_json_encode, but for qs_*_write functions.

Warns you about trying to write nested QS structures (ds_*_write functions do not support nesting in any form).

Works as an "error-level",

  • 0: doesn't do anything
  • 1: calls show_debug_message(...) (default)
  • 2: calls show_error(..., false)
  • 3: calls show_error(..., true)
qs_debug_active

If set to true, QS will keep track of data structures of each kind, and you'll be able to view them by inspecting global.g_qs_active_maps/_lists/etc. (all of which are ds_maps with key being the DS ID and value being the callstack for where it was made from) in the debugger.

Using this in conjunction with qs_debug_callstack is recommended and can be used to easily pinpoint data structure leaks.

qs_option_max_json_depth

Defines maximum depth for encoded JSON structures (default is 100), at which point a warning is shown and any deeper values are omitted.

Helpful for figuring out issues related to attempting to encode cyclic data structures.

qs_option_no_json_mark

If set to true, allows encoding nested data structures without marking them as such.

Note that relying on this flag means that your code will not work with quality_structures_zero extension.

Misc. functions
qs_debug_mode#

Read-only constant.

Is true for quality_structures_one.
Is false for quality_structures_zero.

qs_get_index(qs_value, ?qs_type)

Gets a raw DS index from a QS value. You may occasionally need this for built-in functions that only take those (like place_meeting_list).

If the second argument is provided (qs_type_map, qs_type_list, etc.), verifies that the QS value is of that exact type.

In quality_structures_zero, this just returns back the passed argument.

qs_debug_dump(value)

If using quality_structures_one, converts the provided value to a string while processing QS-specific markers - so for a list you might get

qs::list (id 3), created from
  gml_Script_qs_list_create:5
  gml_Script_scr_test:4
  [...]

In quality_structures_zero, or when not providing a QS value, this is equivalent to

return "`" + string(argument0) + "` (" + typeof(argument0) + ")";
Limitations
Accessor operators

Since GM accessors (list[|index], map[?key], grid[#x,y]) unconditionally call their according built-in ds_ functions, they will not work with quality_structures_one extension.

Chained accessor functions are recommended to be used in their place, where possible.

Index comparisons

Doing if (map >= 0) will not work with quality_structures_one extension since the QS values are arrays.

Doing if (map != -1) will work as intended, however.

You can also take the opportunity to use undefined as a "no data structure" value, which will also help once data structures as a data type are implemented in base GM.

If you absolutely need raw index comparisons/manipulation, use qs_get_index.