Encrypted slot-based save system for GameMaker Studio 2, written in Rust.
🌐 Language / Язык
- ChaCha20-Poly1305 authenticated encryption — fast, secure, tamper-proof
- PBKDF2-HMAC-SHA256 key derivation with 100 000 rounds — your passphrase never appears in the file
- Fresh (salt, nonce) on every save — identical data always produces different ciphertext
- Atomic writes — temp file → rename ensures a power loss mid-save never corrupts an existing slot
- Sandboxed save location — defaults to
game_save_id, GameMaker's guaranteed-writable per-game folder (neverworking_directory, which can sit in a protected install path) - Unlimited slots — 1, 2, 3, … any number, fully independent of each other
- Metadata without decryption — read timestamps cheaply for save-selection screens
SaveSystem_LastError()— human-readable error strings passed back to GML- Cross-platform — builds to
.dll(Windows),.so(Linux),.dylib(macOS)
The project is split into two crates intentionally:
gm-save/
├── gm-save-core/ # Pure Rust — encryption, slots, file format
│ # Published to crates.io
│
├── gm-save-ffi/ # C ABI wrapper → compiles to .dll / .so / .dylib
│ # Distributed via GitHub Releases
│
└── gml/
└── gm_save.gml # GML helper script for GameMaker developers
gm-save-core contains zero FFI code and can be used as a normal Rust library.
gm-save-ffi wraps it with extern "C" functions and #[no_mangle] exports — the only layer GameMaker needs.
Go to Releases and download:
gm_save.dll— compiled native extension (Windows x64)gm_save.gml— GML helper script
- Open your project → Extensions panel → right-click → Create Extension
- Name it
gm_save - Click Add File → select
gm_save.dll - For each function below, click Add Function and fill in the details:
| Function name | Return type | Parameters |
|---|---|---|
SaveSystem_Init |
Real | String save_dir, String pass |
SaveSystem_Save |
Real | Real slot, String json |
SaveSystem_Load |
String | Real slot |
SaveSystem_Exists |
Real | Real slot |
SaveSystem_Delete |
Real | Real slot |
SaveSystem_ListSlots |
String | (none) |
SaveSystem_SlotTimestamp |
Real | Real slot |
SaveSystem_LastError |
String | (none) |
SaveSystem_Shutdown |
Real | (none) |
Drag gm_save.gml into your project's Scripts folder.
// ── Persistent controller, Game Start event ──────────────────────────
// Pick a passphrase and NEVER change it — changing it makes all
// existing saves unreadable (the derived key would differ).
gmsave_init("MY_GAME_2024_SECRET_V1");
// ── Saving ───────────────────────────────────────────────────────────
var _data = {
hp: global.player_hp,
level: global.current_level,
x: obj_player.x,
y: obj_player.y,
};
if (!gmsave_save(1, _data)) {
show_message("Save failed: " + gmsave_last_error());
}
// ── Loading ──────────────────────────────────────────────────────────
var _save = gmsave_load(1);
if (_save != undefined) {
global.player_hp = _save.hp;
global.current_level = _save.level;
}
// ── Save-selection UI ────────────────────────────────────────────────
var _slots = gmsave_list_slots(); // [1, 2, 3, ...]
for (var i = 0; i < array_length(_slots); i++) {
draw_text(32, 64 + i * 48,
"Slot " + string(_slots[i])
+ " — " + gmsave_slot_age_string(_slots[i]));
}
// ── Game End event ───────────────────────────────────────────────────
gmsave_shutdown();| Function | Returns | Description |
|---|---|---|
gmsave_init(passphrase) |
Bool | Initialize with game_save_id + "saves/" |
gmsave_init_dir(dir, passphrase) |
Bool | Initialize with a custom directory |
gmsave_shutdown() |
— | Release resources (call in Game End event) |
gmsave_save(slot, data) |
Bool | Serialize → encrypt → write slot |
gmsave_load(slot) |
Struct | Decrypt → parse → return struct |
gmsave_load_raw(slot) |
String | Decrypt → return raw JSON string |
gmsave_exists(slot) |
Bool | Check if slot file exists |
gmsave_delete(slot) |
Bool | Delete slot file |
gmsave_list_slots() |
Array | Sorted array of existing slot numbers |
gmsave_slot_timestamp(slot) |
Real | Unix timestamp of last save (no decryption) |
gmsave_slot_age_seconds(slot) |
Real | Seconds since last save |
gmsave_slot_age_string(slot) |
String | Human-readable: "5 min ago", "2 hr ago" |
gmsave_last_error() |
String | Error message from last failed operation |
Every .gmsv file uses this binary layout:
4 bytes magic b"GMSV"
4 bytes version u32, little-endian
16 bytes salt random, unique per save (PBKDF2 input)
12 bytes nonce random, unique per save (ChaCha20 input)
8 bytes timestamp i64, little-endian, Unix seconds
4 bytes payload_len u32, little-endian
N bytes payload ChaCha20-Poly1305 ciphertext + 16-byte Poly1305 tag
A fresh (salt, nonce) pair is generated on every save — the same data encrypted twice produces two completely different files. The Poly1305 tag guarantees that any tampering is detected on load.
Requirements: Rust 1.75+ (stable)
git clone https://github.com/NiZaMinius/gm-save
cd gm-save
# Run all 46 tests
cargo test
# Build release DLL on Windows
cargo build --release -p gm-save-ffi
# Output: target/release/gm_save.dll
# Cross-compile for Windows from Linux / macOS
rustup target add x86_64-pc-windows-gnu
cargo build --release --target x86_64-pc-windows-gnu -p gm-save-ffi- Возможности
- Архитектура
- Установка в GameMaker
- Справочник GML API
- Формат файла
- Сборка из исходников
- Лицензия
- ChaCha20-Poly1305 — аутентифицированное шифрование: быстрое, надёжное, защищённое от подделки
- PBKDF2-HMAC-SHA256 с 100 000 итерациями — пароль никогда не попадает в файл в открытом виде
- Новый (salt, nonce) при каждом сохранении — одинаковые данные всегда дают разный шифртекст
- Атомарная запись — сначала во временный файл, потом rename: сбой питания не повредит существующий слот
- Изолированная папка сохранений — по умолчанию
game_save_id, гарантированно доступная для записи папка GameMaker (никогдаworking_directory, которая может лежать в защищённом пути установки) - Неограниченное число слотов — 1, 2, 3, … любое количество, полностью независимые
- Метаданные без расшифровки — быстрое чтение временных меток для экранов выбора сохранения
SaveSystem_LastError()— человекочитаемые сообщения об ошибках прямо в GML- Кроссплатформенность — собирается в
.dll(Windows),.so(Linux),.dylib(macOS)
Проект намеренно разделён на два крейта:
gm-save/
├── gm-save-core/ # Чистый Rust — шифрование, слоты, формат файла
│ # Публикуется на crates.io
│
├── gm-save-ffi/ # Обёртка с C ABI → компилируется в .dll / .so / .dylib
│ # Распространяется через GitHub Releases
│
└── gml/
└── gm_save.gml # GML-скрипт для разработчиков GameMaker
gm-save-core не содержит FFI-кода — это обычная Rust-библиотека.
gm-save-ffi оборачивает её функциями extern "C" с #[no_mangle] — именно этот слой компилируется в DLL.
Перейди в Releases и скачай:
gm_save.dll— нативное расширение (Windows x64)gm_save.gml— GML-скрипт с обёртками
- Открой проект → панель Extensions → правой кнопкой → Create Extension
- Назови его
gm_save - Нажми Add File → выбери
gm_save.dll - Для каждой функции ниже нажми Add Function:
| Имя функции | Тип возврата | Параметры |
|---|---|---|
SaveSystem_Init |
Real | String save_dir, String pass |
SaveSystem_Save |
Real | Real slot, String json |
SaveSystem_Load |
String | Real slot |
SaveSystem_Exists |
Real | Real slot |
SaveSystem_Delete |
Real | Real slot |
SaveSystem_ListSlots |
String | (нет) |
SaveSystem_SlotTimestamp |
Real | Real slot |
SaveSystem_LastError |
String | (нет) |
SaveSystem_Shutdown |
Real | (нет) |
Перетащи gm_save.gml в папку Scripts проекта.
// ── Постоянный контроллер, событие Game Start ────────────────────────
// Выбери пароль и НИКОГДА его не меняй — смена сделает все
// существующие сохранения нечитаемыми.
gmsave_init("MY_GAME_2024_SECRET_V1");
// ── Сохранение ───────────────────────────────────────────────────────
var _data = {
hp: global.player_hp,
level: global.current_level,
x: obj_player.x,
y: obj_player.y,
};
if (!gmsave_save(1, _data)) {
show_message("Ошибка: " + gmsave_last_error());
}
// ── Загрузка ─────────────────────────────────────────────────────────
var _save = gmsave_load(1);
if (_save != undefined) {
global.player_hp = _save.hp;
global.current_level = _save.level;
}
// ── Экран выбора сохранения ──────────────────────────────────────────
var _slots = gmsave_list_slots();
for (var i = 0; i < array_length(_slots); i++) {
draw_text(32, 64 + i * 48,
"Слот " + string(_slots[i])
+ " — " + gmsave_slot_age_string(_slots[i]));
}
// ── Событие Game End ─────────────────────────────────────────────────
gmsave_shutdown();| Функция | Возврат | Описание |
|---|---|---|
gmsave_init(passphrase) |
Bool | Инициализация в game_save_id + "saves/" |
gmsave_init_dir(dir, passphrase) |
Bool | Инициализация с произвольной папкой |
gmsave_shutdown() |
— | Освободить ресурсы (Game End) |
gmsave_save(slot, data) |
Bool | Сериализовать → зашифровать → записать |
gmsave_load(slot) |
Struct | Расшифровать → распарсить → вернуть struct |
gmsave_load_raw(slot) |
String | Расшифровать → вернуть сырую JSON-строку |
gmsave_exists(slot) |
Bool | Проверить существование файла слота |
gmsave_delete(slot) |
Bool | Удалить файл слота |
gmsave_list_slots() |
Array | Отсортированный массив номеров слотов |
gmsave_slot_timestamp(slot) |
Real | Unix-метка последнего сохранения |
gmsave_slot_age_seconds(slot) |
Real | Секунд с последнего сохранения |
gmsave_slot_age_string(slot) |
String | "5 min ago", "2 hr ago", "1 day(s) ago" |
gmsave_last_error() |
String | Сообщение об ошибке последней операции |
4 байта magic b"GMSV"
4 байта version u32, little-endian
16 байт salt случайный, уникален для каждого сохранения
12 байт nonce случайный, уникален для каждого сохранения
8 байт timestamp i64, little-endian, Unix-секунды
4 байта payload_len u32, little-endian
N байт payload шифртекст ChaCha20-Poly1305 + 16-байтовый тег Poly1305
Требования: Rust 1.75+ (stable)
git clone https://github.com/NiZaMinius/gm-save
cd gm-save
# Запустить все 46 тестов
cargo test
# Собрать DLL на Windows
cargo build --release -p gm-save-ffi
# Результат: target/release/gm_save.dll
# Кросс-компиляция под Windows с Linux / macOS
rustup target add x86_64-pc-windows-gnu
cargo build --release --target x86_64-pc-windows-gnu -p gm-save-ffiLicensed under either of the following, at your option:
- MIT License — see LICENSE-MIT
- Apache License, Version 2.0 — see LICENSE-APACHE
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.+
Лицензировано на ваш выбор по одной из следующих лицензий:
- Лицензия MIT — см. LICENSE-MIT
- Лицензия Apache 2.0 — см. LICENSE-APACHE
Made with ❤️ by NiZaMinius