Skip to content

AbyssMoth/godot-save

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Abyss Moth Save

Сохранение состояния в 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/save

Быстрый старт

var 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 и при выходе из дерева.


Debug-копия

Если 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-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.

About

Сохранение состояния в JSON для GDScript (Godot 4): стадийные проверки при загрузке, опциональное AES-шифрование и HMAC-подпись от правки, троттлинг записи и debug-копия в редакторе.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors