Сохранение состояния в JSON для GDScript (Godot 4): стадийные проверки при загрузке, опциональное AES-шифрование и HMAC-подпись от правки, троттлинг записи и debug-копия в редакторе.
- Автор: RimuruDev. Студия: Abyss Moth. Лицензия: MIT.
- Движок: Godot 4.x (проверено на 4.7).
- Standalone: работает с обычным
Dictionary, не требует других аддонов. Хорошо стыкуется сgodot-reactive. - Обфускация-safe: данные сериализуешь сам типизированным вызовом, в API нет вызовов методов по строке.
- Сохранение и загрузка
Dictionaryв JSON одним вызовом. - Загрузка по стадиям:
OK/NO_FILE(новая игра) /NO_ACCESS/CORRUPT/TAMPERED/DECRYPT_FAILED. - Опциональное AES-шифрование (встроено в Godot) и HMAC-подпись (ловит правку файла).
- Троттлинг записи: 100 изменений в минуту не дают 100 записей на диск.
- Debug-копия без шифрования в редакторе, чтобы читать сейв глазами.
- Restore из произвольного файла (присланный игроком сейв).
Папка addons/abyss_moth/save/. Классы глобальные (class_name), работают сразу.
git submodule add https://github.com/AbyssMoth/godot-save.git addons/abyss_moth/savevar config := AmSaveConfig.new("user://database.json")
config.hmac_key = "ключ-под-проект" # ловит правку файла (пусто = без подписи)
config.password = "" # задай для AES-шифрования (пусто = без)
var save := AmSaveService.new(config)
# Сохранение: данные это обычный Dictionary.
save.save({"coins": 100, "level": 3})
# Загрузка со стадиями.
var result := save.load()
match result.status:
AmSaveResult.Status.OK:
apply(result.data)
AmSaveResult.Status.NO_FILE:
start_new_game()
AmSaveResult.Status.TAMPERED:
handle_tampered() # файл правили
_:
push_warning(result.status_name() + ": " + result.message)exists(), delete(), restore_from(path, password) тоже есть.
- Шифрование: задай
config.password, файл пишется через AES (FileAccess.open_encrypted_with_pass). Честно: клиентский ключ это deterrence, а не настоящая защита (ключ есть в бинаре), но для сейвов норм. - Подпись: задай
config.hmac_key. Перед данными пишется HMAC-SHA256. При загрузке подпись пересчитывается по точным байтам, и если данные правили руками, статус будетTAMPERED. Простой хеш так не умеет (читер бы его пересчитал), а HMAC без ключа не подделать. - Можно вместе: AES + HMAC.
Реактивное изменение значения дешёвое и идёт в памяти. Запись на диск это отдельная операция,
её НЕ делают на каждое изменение. AmSaveScheduler копит изменения и пишет не чаще раза в N секунд,
плюс гарантированно сбрасывает при уходе приложения в фон или выходе.
var scheduler := AmSaveScheduler.new()
add_child(scheduler)
scheduler.setup(save, func(): return progress.serialize())
# При любом изменении состояния:
scheduler.mark_dirty() # дёшево, зови хоть 100 раз в секунду100 кликов в кликере за минуту дают примерно одну запись (по таймеру) или запись только при сворачивании.
config.autosave_interval задаёт интервал (0 = только вручную и по событиям). flush() пишет немедленно.
Сбрасывает на диск по NOTIFICATION_APPLICATION_PAUSED (фон на мобиле), WM_GO_BACK_REQUEST,
WM_CLOSE_REQUEST и при выходе из дерева.
Если config.debug_copy = true, в редакторе (OS.has_feature("editor")) рядом с сейвом пишется
НЕшифрованная копия database.json.debug.json, чтобы читать прогресс глазами. В сборке игры её нет.
В конверт пишется config.version. При загрузке result.from_version показывает версию файла. Если она отличается от текущей и задан config.migrate, данные прогоняются через него перед применением:
config.version = 2
config.migrate = func(data: Dictionary, from_version: int) -> Dictionary:
if from_version < 2 and data.has("name"):
data["player_name"] = data["name"]
data.erase("name")
return dataТак старые сейвы открываются новой версией игры без потерь. Сложную миграцию модели можно делать и на стороне ReactiveModel._migrate из godot-reactive.
godot-save берёт Dictionary, а ReactiveModel из godot-reactive его как раз и отдаёт. Связываешь сам, типизированными вызовами:
save.save(progress.serialize())
var r := save.load()
if r.is_ok():
progress.deserialize(r.data) # выставит .value у полей -> подписчики обновятся
scheduler.setup(save, func(): return progress.serialize())Аддоны независимы: можно брать только реактивность, только сейв или оба.
Свой мини-фреймворк без зависимостей, папка test/.
- В редакторе: запусти сцену
test/save_test_main.tscn. - Headless (CI):
godot --headless -s res://addons/abyss_moth/save/test/save_run_tests.gd
Свои тесты клади в test/unit/test_*.gd, наследуй AmSaveTest.
MIT (c) 2026 RimuruDev (Abyss Moth). См. LICENSE.