Quick answer: ds_grid_write_text_file uses ANSI encoding - use buffer_write with buffer_text and explicit UTF-8 encoding for non-ASCII.
If you are searching for how to fix gamemaker ds_grid write text file corrupts utf8, you are not alone. This is a recurring issue in GameMaker that comes up across many team projects. The behavior looks like a deep bug, but it usually traces back to a known interaction between two systems. Here is the full breakdown of the symptom, the cause, and a fix you can apply today.
The Symptom
Your save file contains player names with accented characters or non-Latin scripts. After ds_grid_write_text_file and ds_grid_read_text_file roundtrip, the names show as question marks or mojibake.
Root Cause
ds_grid_write_text_file uses the OS default text encoding, which is ANSI/cp1252 on Windows. Non-ASCII characters get replaced with '?' when they don't fit in the encoding. The read function expects the same encoding back.
The Fix
Step 1: Replace ds_grid file IO with buffer-based serialization. Use buffer_create + buffer_write to construct UTF-8 explicitly.
// Save grid as UTF-8 via buffer
function save_grid_utf8(grid, fname) {
var buf = buffer_create(1024, buffer_grow, 1);
var w = ds_grid_width(grid);
var h = ds_grid_height(grid);
buffer_write(buf, buffer_u32, w);
buffer_write(buf, buffer_u32, h);
for (var i = 0; i < w; i++)
for (var j = 0; j < h; j++)
buffer_write(buf, buffer_string, ds_grid_get(grid, i, j));
buffer_save(buf, fname);
buffer_delete(buf);
}
Step 2: For backward compatibility with existing saves, detect the encoding by reading the first bytes - presence of 0xEF 0xBB 0xBF indicates UTF-8 BOM.
// Load
function load_grid_utf8(fname) {
var buf = buffer_load(fname);
var w = buffer_read(buf, buffer_u32);
var h = buffer_read(buf, buffer_u32);
var g = ds_grid_create(w, h);
for (var i = 0; i < w; i++)
for (var j = 0; j < h; j++)
ds_grid_set(g, i, j, buffer_read(buf, buffer_string));
buffer_delete(buf);
return g;
}
Step 3: Wrap save/load in a versioned format that includes encoding metadata - migrate ANSI saves to UTF-8 on first load.
Why this happens
This bug class sits at the boundary between two GameMaker subsystems. The first system reports success at its layer; the second system silently rejects or transforms the data. Without an error in the middle, the symptom appears only at the visible output - which is where you started debugging.
The fix above addresses the configuration mismatch at the boundary. Once the two systems agree on the data contract, the symptom disappears immediately. There is no underlying engine bug to file; the behavior is a documented (if obscure) consequence of how GameMaker designed the interaction.
Verifying the fix
Reproduce the original symptom in isolation before applying the fix. If you cannot reliably reproduce, you cannot reliably verify - and you risk shipping a fix that addresses a different bug. Start with a minimal scene or scenario that triggers the issue every time, apply the change above, and run the same scenario at least three times to confirm the symptom is gone.
For shipping games, follow a staged rollout. Push the fix to 5-10% of players first, monitor the affected metric (crash rate, error log frequency, gameplay telemetry) for 24-48 hours, and expand only if the data confirms the fix without regressions. A staged rollout is cheap insurance against an interaction you did not anticipate.
Capturing the bug from players
The hardest part of fixing this kind of issue is getting a player report that includes enough context to reproduce. Most players describe the symptom in their own words and omit the build number, scene, or hardware that triggered it. Without those, you are guessing at the conditions.
A bug reporting SDK like Bugnet for GameMaker captures the build SHA, scene name, recent logs, device specs, and a screenshot automatically whenever a player files a report. With that bundle attached, you can reproduce the bug locally instead of guessing - typically the difference between a one-day fix and a one-week investigation.
Edge cases to watch for
The same root cause can produce slightly different symptoms in adjacent systems. After fixing the case you found, spend thirty minutes searching your project for similar patterns - the same API called with different arguments, the same data flow with a different entity type, or the same lifecycle issue in a sibling module. Each match is a candidate for the same fix, or a related fix that prevents future bugs of the same class.
Pay extra attention to boundary conditions - the first frame, the last frame, zero or maximum values, and the transition between two states. These are where engines often have undocumented behavior, and where regression tests pay the highest dividend. A test that exercises the boundary catches the subtle regressions that look like new bugs but are really the original returning.
When to escalate
If you have applied the fix above and the symptom persists, the bug is likely in a different layer than this article addresses. Capture a video of the symptom, the exact reproduction steps, and the GameMaker version. File a report on the official issue tracker with that bundle - the maintainers are responsive when the report is complete.
Before filing, search the existing issues for keywords related to your symptom. Many bug reports are duplicates of issues that have a workaround posted in the comments but no formal fix in the engine. Reading the existing thread often resolves the issue faster than a new report.
Check the boundary; the bug lives between systems.