Quick answer: ds_list is a handle, not data. Serialize with ds_list_write() to get a string, save the string, then use ds_list_read() on load. Or switch to arrays/structs and use json_stringify(), which handles lists natively.

Here is how to fix GameMaker ds_list not persisting save. You store a player’s inventory in global.inventory = ds_list_create(). You add items. You save the game (file_text_write or similar). You load the game. The inventory is empty. Or it crashes trying to read invalid items. GameMaker’s DS structures are handles to memory-backed data, and saving the handle saves just an integer.

The Symptom

After a save/load cycle, a ds_list (or ds_map, ds_grid) is:

What Causes This

DS types are handles. ds_list_create() returns an integer handle. The actual data lives in memory managed by GameMaker’s DS system. If you save the handle (e.g. global.inventory) as an integer and restore it later, the integer may reference a different list or nothing at all.

Memory lifecycle. DS structures persist until explicitly destroyed with ds_list_destroy(). They do not survive closing the game. The handle from a previous session is meaningless.

ds_list_write/read require explicit calls. GameMaker provides ds_list_write() to serialize the list’s contents to a string, and ds_list_read() to deserialize. Without these explicit calls, saving the handle saves nothing useful.

Nested DS structures. A ds_list containing ds_map handles requires serializing each nested structure too. ds_list_write does not recursively serialize — it only captures raw values. Nested handles produce garbage on load.

The Fix

Step 1: Use ds_list_write and ds_list_read.

// Save
var data = ds_list_write(global.inventory);
var file = file_text_open_write("save.dat");
file_text_write_string(file, data);
file_text_close(file);

// Load
if (file_exists("save.dat")) {
    var file = file_text_open_read("save.dat");
    var data = file_text_read_string(file);
    file_text_close(file);

    global.inventory = ds_list_create();
    ds_list_read(global.inventory, data);
}

ds_list_write produces a string containing all values. ds_list_read rebuilds the list from that string. Works for simple value lists (numbers, strings).

Step 2: Prefer arrays for new code. GameMaker’s modern approach uses arrays and structs. They serialize natively with JSON:

// Modern approach
global.inventory = ["sword", "potion", "key"];

// Save
var data = json_stringify({
    inventory: global.inventory,
    gold: global.gold,
    level: global.level
});
var file = file_text_open_write("save.json");
file_text_write_string(file, data);
file_text_close(file);

// Load
var file = file_text_open_read("save.json");
var raw = file_text_read_string(file);
file_text_close(file);

var save_data = json_parse(raw);
global.inventory = save_data.inventory;
global.gold = save_data.gold;
global.level = save_data.level;

Arrays + structs + JSON handle nested structures cleanly. Modern GameMaker projects should default to this approach.

Step 3: For complex DS structures, serialize manually. If you cannot migrate off DS types, walk the list and serialize each entry:

function save_inventory(list) {
    var items = [];
    for (var i = 0; i < ds_list_size(list); i++) {
        items[i] = list[| i]; // extract each value
    }
    return json_stringify(items);
}

function load_inventory(text) {
    var items = json_parse(text);
    var list = ds_list_create();
    for (var i = 0; i < array_length(items); i++) {
        ds_list_add(list, items[i]);
    }
    return list;
}

Step 4: Clean up DS structures on exit. Always call ds_list_destroy when done with a list to prevent memory leaks, even if the game is quitting:

// Room End or Game End event
if (ds_exists(global.inventory, ds_type_list)) {
    ds_list_destroy(global.inventory);
}

Combined with ds_exists checks, you avoid double-destroy crashes.

Migration Strategy

For a project using DS heavily, migrating to arrays/structs piece by piece is viable:

  1. Wrap DS access in accessor functions (get_inventory_item, set_inventory_item)
  2. Switch the underlying storage from ds_list to array
  3. Update save/load to use json_stringify/json_parse

The accessor pattern lets you migrate without touching every call site.

“DS structures are handles, not data. Saving a handle saves an integer. Serialize explicitly, or use arrays that serialize themselves.”

Related Issues

For other GameMaker issues, see GameMaker Alarm Not Firing and GameMaker Surface Lost After Resize.

Arrays + json_stringify for all new save code. DS for niche performance cases only.