Собственный формат ресурсов: загрузка и сохранение
Полный пример C#: регистрация обработчиков, UTF-8, ошибки и кеш ресурсов.
ResourceFormatLoader превращает файл собственного формата в Resource; ResourceFormatSaver выполняет обратную операцию. Для этого не обязательно пересобирать движок: оба класса допускают переопределения C#.
Используйте их, когда формат должен участвовать в ResourceLoader/ResourceSaver. Если нужно просто прочитать JSON или пользовательские настройки, достаточно FileAccess и парсера. Если исходный ассет нужно преобразовать при импорте в редакторе, нужен EditorImportPlugin, а не только загрузчик времени выполнения.
Рабочий пример: заметка в собственном формате
Нужен проект C# с SDK Reckfell 1.0.0. Создайте четыре файла с указанными именами. Прикрепите NoteExample.cs к Entity в запускаемом уровне.
Формат .rfnote состоит из ASCII-заголовка RFNOTE1 с переводом строки LF и текста UTF-8. Общий размер ограничен 65536 байтами, включая заголовок. Это формат примера, не замена .rdata для произвольных ресурсов.
NoteResource.cs
using Reckfell;
public partial class NoteResource : Resource
{
public string Text { get; set; } = "";
}NoteLoader.cs
using System;
using System.Text;
using Reckfell;
public partial class NoteLoader : ResourceFormatLoader
{
public const int MaxBytes = 65536;
public const string Header = "RFNOTE1\n";
public override string[] _GetRecognizedExtensions() => new[] { "rfnote" };
public override bool _HandlesType(StringName type) => type == "Resource";
public override string _GetResourceType(string path) =>
path.EndsWith(".rfnote", StringComparison.OrdinalIgnoreCase) ? "Resource" : "";
public override Variant _Load(string path, string originalPath,
bool useSubThreads, int cacheMode)
{
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file is null)
return (int)FileAccess.GetOpenError();
ulong length = file.GetLength();
if (length < (ulong)Header.Length || length > MaxBytes)
return (int)Error.FileCorrupt;
byte[] bytes = file.GetBuffer((long)length);
if ((ulong)bytes.Length != length)
return (int)Error.FileCantRead;
string text;
try { text = new UTF8Encoding(false, true).GetString(bytes); }
catch (DecoderFallbackException) { return (int)Error.FileCorrupt; }
if (!text.StartsWith(Header, StringComparison.Ordinal))
return (int)Error.FileCorrupt;
return new NoteResource { Text = text[Header.Length..] };
}
}NoteSaver.cs
using System.Text;
using Reckfell;
public partial class NoteSaver : ResourceFormatSaver
{
public override bool _Recognize(Resource resource) => resource is NoteResource;
public override string[] _GetRecognizedExtensions(Resource resource) =>
resource is NoteResource ? new[] { "rfnote" } : System.Array.Empty<string>();
public override Error _Save(Resource resource, string path, uint flags)
{
if (resource is not NoteResource note)
return Error.InvalidParameter;
if (flags != 0)
return Error.Unavailable;
if (note.Text.Length > NoteLoader.MaxBytes)
return Error.InvalidParameter;
byte[] bytes;
try { bytes = new UTF8Encoding(false, true).GetBytes(NoteLoader.Header + note.Text); }
catch (EncoderFallbackException) { return Error.InvalidData; }
if (bytes.Length > NoteLoader.MaxBytes)
return Error.InvalidParameter;
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file is null)
return FileAccess.GetOpenError();
file.StoreBuffer(bytes);
file.Flush();
return file.GetError();
}
}NoteExample.cs
using Reckfell;
public partial class NoteExample : Entity
{
private NoteLoader? _loader;
private NoteSaver? _saver;
public override void _Ready()
{
_loader = new NoteLoader();
_saver = new NoteSaver();
ResourceLoader.AddResourceFormatLoader(_loader);
ResourceSaver.AddResourceFormatSaver(_saver);
const string path = "user://example.rfnote";
using var original = new NoteResource { Text = "Hello / Привет 😀" };
Error error = ResourceSaver.Save(original, path);
if (error != Error.Ok)
{
ReckfellApi.PrintErr($"Save failed: {error}");
return;
}
using var loaded = ResourceLoader.Load<NoteResource>(
path, "Resource", ResourceLoader.CacheMode.Ignore);
if (loaded is null)
ReckfellApi.PrintErr("Load failed; check the engine log.");
else
ReckfellApi.Print(loaded.Text);
}
public override void _ExitTree()
{
if (_saver is not null)
{
ResourceSaver.RemoveResourceFormatSaver(_saver);
_saver.Dispose();
_saver = null;
}
if (_loader is not null)
{
ResourceLoader.RemoveResourceFormatLoader(_loader);
_loader.Dispose();
_loader = null;
}
}
}Что получится
После запуска появится user://example.rfnote, в выводе — Hello / Привет 😀. Перезапуск перезаписывает файл. user:// — каталог данных приложения, а не папка исходников проекта.
Загрузчик возвращает в Variant либо объект NoteResource, либо целочисленный код Error. Возвращать Error.Ok вместо объекта при успешном чтении нельзя: целое число трактуется как код ошибки, ресурс не создаётся. Сохранение возвращает Error напрямую.
_GetResourceType сообщает встроенный базовый тип Resource. Это не регистрация глобального имени NoteResource в редакторе. Сам объект всё равно является NoteResource, поэтому после загрузки доступен его Text. Свойство Text хранится нашим saver; оно не экспортировано для Inspector и не получает автоматическую сериализацию в других форматах.
Регистрация выполняется до первого обращения к файлам; снятие регистрации — до Dispose объектов. Для общего использования перенесите владение загрузчиком и saver в один постоянный сервис. Пример владеет ими только пока NoteExample находится в дереве: после удаления этой Entity новые загрузки требуют другой регистрации. Уже загруженные ресурсы при этом не исчезают.
Кеш и изменение ресурсов
Обычная повторная загрузка пути может вернуть тот же Resource. Изменение Text тогда видно всем держателям этого объекта; ресурс не обязан быть неизменяемым или «без состояния». Повторная запись файла сама по себе не обновляет объект в памяти.
В примере CacheMode.Ignore читает независимый верхний ресурс с диска. Это не обновление старых ссылок и не глубокий обход кешей зависимостей. Для собственного формата с зависимостями нужно отдельно реализовать их загрузку и согласовать режимы кеша. Здесь зависимостей нет; параметр cacheMode не используется внутри callback, а выбор верхнего ресурса выполняет ResourceLoader.
Ошибки и границы примера
| Симптом | Действие |
|---|---|
ResourceLoader не знает .rfnote | Зарегистрируйте NoteLoader до Load; проверьте, что владеющая Entity ещё в дереве |
| ResourceSaver возвращает FileUnrecognized | Проверьте расширение, регистрацию NoteSaver и реальный тип объекта |
| Загрузка вернула null | Посмотрите журнал; callback мог вернуть FileCorrupt, FileCantRead или ошибку открытия |
| В файле пустой текст | Пустой текст допустим после заголовка; пустой файл недопустим |
| Старое значение после записи | Проверьте кеш и ссылки; для независимого чтения используйте Ignore |
| Сохранение отклонено | Проверьте размер UTF-8 вместе с заголовком, корректность UTF-16 и отсутствие неподдерживаемых flags |
Пример не реализует атомарную замену файла: открытие с Write может обрезать существующий файл до завершения записи. Для важных сохранений нужен временный файл и отдельная стратегия замены/резервирования. Он также не реализует UID, зависимости, импорт в редакторе, параллельную загрузку или пользовательские флаги сохранения. Числовые ошибки проверяются; журнал движка не заменяет проверку результата.
Для редакторского использования нужен отдельный жизненный цикл плагина, который регистрирует обработчики при включении и снимает при выключении. Наличие этих классов само по себе не доказывает, что сторонний формат попадёт в экспорт: проверьте фильтры включения и реальную сборку игры. В примере файл создаётся в user:// уже во время выполнения.
Если нужен нативный модуль C++
Контракт отличается от C#. В текущем Source метод загрузки возвращает Ref<Resource> и принимает const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode. Метод сохранения возвращает Error и принимает const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags.
Не переносите C# Variant/Error-возврат в C++ сигнатуру и не используйте старый трёхаргументный load. Установите код через ненулевой r_error, возвращайте пустую ссылку при отказе и не выдавайте успешный Resource после неудачного парсинга. Нативный модуль требует своей сборки и регистрации; для приведённого управляемого примера это не нужно.
Справочник: ResourceFormatLoader, ResourceFormatSaver, ResourceLoader, ResourceSaver.