reckfellENGINE

Собственный формат ресурсов: загрузка и сохранение

Полный пример 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

C#
using Reckfell;

public partial class NoteResource : Resource
{
    public string Text { get; set; } = "";
}

NoteLoader.cs

C#
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

C#
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

C#
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.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.