Quick answer: buffer_string writes a null-terminated UTF-8 string; buffer_text writes raw bytes without termination. Reading with the wrong type returns garbled data because the byte counts do not match. Always pair the same write/read function and prefer buffer_string for self-delimiting saves.

Here is how to fix GameMaker buffer-based saves where strings come back as garbled text or completely wrong characters. You write a player name with buffer_write(buf, buffer_string, name), save the buffer to disk, load it later, and read with buffer_text — only to get random Unicode mixed with junk bytes. The buffer functions look interchangeable but they encode strings differently.

The Symptom

You serialize a save with several strings (player name, last save location, current quest). On load, the strings read back as gibberish. Sometimes the first string reads correctly but subsequent reads are offset; sometimes everything is corrupt from byte zero.

What Causes This

Mismatched write/read types. buffer_string writes length + 1 bytes (string + null terminator). buffer_text writes exactly length bytes with no terminator. Reading with the wrong function leaves the buffer cursor in the wrong position for everything that follows.

UTF-8 multi-byte characters. GameMaker buffers are UTF-8. string_length counts code points, not bytes. A 5-character string like cafĂ©! may be 6 bytes (the é takes 2 bytes). Computing offsets from string_length without accounting for this corrupts the cursor.

buffer_u8 with non-ASCII. Writing a single character via buffer_u8 and a string takes only the first byte. Multi-byte characters lose their tail bytes.

Seek mismatch. If you buffer_seek by an absolute byte offset that does not correspond to a value boundary, subsequent reads desynchronize.

The Fix

Step 1: Always pair buffer_string with buffer_string.

// Save
var _buf = buffer_create(256, buffer_grow, 1);
buffer_write(_buf, buffer_string, player_name);
buffer_write(_buf, buffer_string, last_scene);
buffer_write(_buf, buffer_s32, score);
buffer_save(_buf, "save.dat");
buffer_delete(_buf);

// Load
var _buf = buffer_load("save.dat");
player_name = buffer_read(_buf, buffer_string);
last_scene  = buffer_read(_buf, buffer_string);
score       = buffer_read(_buf, buffer_s32);
buffer_delete(_buf);

Use buffer_string on both sides. The null terminator marks the end so the reader knows when to stop. The cursor automatically advances past the terminator.

Step 2: For length-prefixed text, use buffer_text + explicit length.

// Write
var _bytes = string_byte_length(player_name);
buffer_write(_buf, buffer_u32, _bytes);
buffer_write(_buf, buffer_text, player_name);

// Read
var _bytes = buffer_read(_buf, buffer_u32);
var _start = buffer_tell(_buf);
var _str   = buffer_peek(_buf, _start, buffer_text);
buffer_seek(_buf, buffer_seek_relative, _bytes);

This is more verbose but useful when interfacing with external binary protocols.

Step 3: Validate format with a magic header.

// Always start saves with a magic + version
var MAGIC = 0x_BUG_NET_5;
var VERSION = 2;

buffer_write(_buf, buffer_u32, MAGIC);
buffer_write(_buf, buffer_u16, VERSION);
// ... data ...

if (buffer_read(_buf, buffer_u32) != MAGIC) {
    show_debug_message("Save corrupt or wrong format");
    exit;
}
var ver = buffer_read(_buf, buffer_u16);

Step 4: Avoid buffer_u8 for characters. If you must encode a single character, use buffer_string with a 1-character string. buffer_u8 is for byte values, not text.

Step 5: Use buffer_string_length helper if computing offsets. When you need to know how many bytes a string will occupy including the null terminator:

function buffer_string_size(_s) {
    return string_byte_length(_s) + 1;
}

string_byte_length returns UTF-8 byte count (correctly counts multi-byte characters). The +1 accounts for the null terminator.

Backward-Compatible Save Loading

If you change save format between versions, store the version at the top and branch read logic:

if (ver == 1) {
    player_name = buffer_read(_buf, buffer_string);
    score = buffer_read(_buf, buffer_s32);
} else if (ver == 2) {
    player_name = buffer_read(_buf, buffer_string);
    score = buffer_read(_buf, buffer_s32);
    achievements = buffer_read(_buf, buffer_u64);
}

“buffer_string is self-delimiting. buffer_text is not. Pick one and use the same on both ends.”

Related Issues

For save corruption in general, see Save File Buffer Corruption. For DS list save persistence, see DS List Not Persisting.

Pair the type. Mind UTF-8 lengths. Magic header for format checks. Saves load clean.