Quick answer: Use buffer_write(buf, buffer_string, "...") on the sender and buffer_read(buf, buffer_string) on the receiver. Both sides must agree on type; mixing buffer_string with buffer_text breaks UTF-8.

Two players chat in a GameMaker multiplayer test. ASCII text works. The moment one player types café with an accent, the receiver sees caf? — or worse, a garbled string with off-by-one boundary issues that affect subsequent packet fields.

Buffer Types for Strings

GameMaker provides two string buffer types:

For network protocols, buffer_string is almost always what you want. It handles UTF-8 natively (multi-byte characters serialize as their byte sequence) and self-delimits.

The Fix

/// Sender
var buf = buffer_create(256, buffer_grow, 1);
buffer_write(buf, buffer_u8, MSG_CHAT);
buffer_write(buf, buffer_string, "café");
network_send_packet(socket, buf, buffer_tell(buf));
buffer_delete(buf);

/// Receiver
buffer_seek(packet_buffer, buffer_seek_start, 0);
var msg_type = buffer_read(packet_buffer, buffer_u8);
if (msg_type == MSG_CHAT) {
    var text = buffer_read(packet_buffer, buffer_string);
    show_debug_message(text);
}

Both sides use buffer_string. UTF-8 bytes serialize and deserialize correctly. The accented character round-trips.

Why Mixing Types Breaks

If the sender uses buffer_text (writing raw bytes without terminator) but the receiver uses buffer_string (reading until null), the reader keeps reading past the intended end — into the next field’s bytes — until it happens to find a 0x00. Result: corrupted string, misaligned subsequent reads.

The inverse — sender uses buffer_string, receiver uses buffer_text with hand-coded length — produces an off-by-one because the receiver doesn’t skip the terminator byte.

Length-Prefixed for Fixed-Length Strings

For binary protocols where you don’t want a null terminator:

/// Sender
var s = "café";
var byte_len = string_byte_length(s);
buffer_write(buf, buffer_u16, byte_len);
buffer_write(buf, buffer_text, s);   // raw bytes, no terminator

/// Receiver
var byte_len = buffer_read(packet_buffer, buffer_u16);
var raw = buffer_read_text(packet_buffer, byte_len);

string_byte_length returns the UTF-8 encoded length, not the character count. buffer_read_text(buf, n) reads exactly n bytes — safer than buffer_text without a known length.

Validation

Validate received string lengths before allocating:

var byte_len = buffer_read(packet_buffer, buffer_u16);
if (byte_len > 1024) {
    show_debug_message("Suspicious string length, dropping packet");
    exit;
}

Untrusted clients can send arbitrarily large length fields trying to OOM your server. Cap reads.

Verifying

Test with multi-byte UTF-8: café, こんにちは, 你好, 😀. All should round-trip exactly. Print string_length and string_byte_length on both ends — they should match across the network.

“Both sides agree on buffer_string, or both sides agree on length-prefixed buffer_text. Mixing breaks UTF-8 silently.”

Wrap network protocols in a typed packet builder — centralizes encoding choices so a future ASCII assumption doesn’t leak in.