Сериализация

API

Каждая поверхность API (API-Surface, исключая Composition) у SerializationManager имеет generic-вариант, а также боксинговый вариант. Кроме того, у каждой есть ещё два generic-метода для прямого вызова TypeInterfaces: один, где вы можете предоставить используемый экземпляр, и другой, где вам нужно лишь предоставить тип, а менеджер сам позаботится о получении экземпляра для использования.

Общие параметры

В этом разделе мы затронем параметры, присутствующие во всех API. Поэтому мы больше не будем упоминать их при обсуждении конкретных API.

NotNullableOverride

Поскольку ссылочные типы допускают нулевой указатель, но текущие API C#/CIL не позволяют определить, был ли generic-аргумент ссылочного типа помечен как (не)nullable, мы добавили флаг переопределения под названием notNullableOverride в виде параметра bool. Установите этот параметр в true, если вы не хотите, чтобы метод возвращал null-значения!

Info

Правильное использование этого параметра теперь контролируется анализатором. Если вам нужно его использовать или вы используете его неправильно, ваша IDE должна сообщить вам.

SerializationContext

Можно использовать, чтобы предоставить экземпляр контекста, если вам этого хочется. См. SerializationContext для получения дополнительной информации о том, как создать контекст.

SkipHook

Все API также предоставляют вам параметр bool под названием skipHook, который можно использовать, чтобы пропустить вызов методов, реализованных с помощью интерфейса ISerializationHook. Однако учтите, что этот параметр должен быть объявлен устаревшим. Этот параметр недоступен в API Write, Validate и Composition!

Read

При чтении вам нужно будет предоставить:

  • Тип, либо как generic-аргумент типа T, либо как параметр типа в боксинговом варианте
  • DataNode для чтения (ну, очевидно) При желании у вас также будет возможность предоставить instanceProvider. Это будет делегат, который предоставит значение для чтения в него. Это можно использовать, например, чтобы переиспользовать экземпляры объекта вместо выделения нового. Если всё это звучит для вас как бессмыслица, не волнуйтесь, скорее всего, вам никогда не придётся это использовать при написании кода для нашей игры.

Warning

InstanceProvider никогда НИКОГДА не должен возвращать null-значение. Это вызовет исключение (в отладочных сборках).

Write

Для записи вам, опять же, нужно указать:

  • Тип, либо как generic-аргумент типа T, либо как параметр типа в боксинговом варианте. Однако учтите, что здесь существует один боксинговый вариант, которому не нужно указывать тип, так как он получит его с помощью object.GetType() При желании вы можете указать флаг alwaysWrite, чтобы заставить весь объект быть записанным в yaml. В противном случае сериализатор опустит значения полей, равные указанному значению по умолчанию.

Validate

Validate, я бы сказал, среди более простых API, которые мы предоставляем. Здесь вы предоставляете:

  • Тип, либо как generic-аргумент типа T, либо как параметр типа
  • Узел для проверки Взамен вы получите ValidationNode, предоставляющий информацию о валидности DataNode.

Copy

Наш API Copy разделён на две части: CopyTo и CreateCopy. С CopyTo вы сможете копировать значения из одного объекта в другой. С CreateCopy вы создадите копию объекта, который в него передадите.

Warning

Если CopyTo не удастся скопировать в целевой объект, он перезапишет его вызовом CreateCopy.

Composition

Здесь композиция проталкивается через узлы с помощью определений, связанных с переданным типом. Это означает, что тип, который вы передаёте, определяет, как предоставленные вами datanode’ы будут объединены вместе. В настоящее время существует лишь очень ограниченное количество методов для настройки этого поведения, особенно для DataFields. Однако мы работаем над этим!

Data Definitions

DataDefinitions — это структуры или классы с полями/свойствами, помеченными как DataFields. Эти DataFields записываются и читаются в/из yaml, но также используются для операций копирования, проверки и композиции. В дальнейшем я буду просто называть структуры и классы «типом».

Определения данных должны иметь конструктор без параметров, чтобы быть валидными. (За исключением DataRecords)

Объявление DataDefinition

Note

Нет никакого риска объявить DataDefinition сразу с несколькими из этих опций. Дублирующиеся регистрации просто будут сведены к одной.

DataDefinition должны быть объявлены partial, чтобы работать с нашим генератором исходного кода для копирования.

Напрямую

Чтобы сделать класс DataDefinition, вы можете добавить атрибут [DataDefinition] к типу вот так.

[DataDefinition]
public sealed partial class MyClass {}

[DataDefinition]
public partial struct MyStruct {}

Все наследники типа

Если у вас есть базовый тип или интерфейс, все наследники которого должны автоматически становиться datadefinitions, пометьте базовый тип или интерфейс атрибутом [ImplicitDataDefinitionForInheritors]. Все текущие помеченные типы можно найти здесь, где вы, вероятно, найдёте много типов/интерфейсов, которые вы наследовали/реализовывали раньше.

[ImplicitDataDefinitionForInheritors]
public interface IContainer {}

[ImplicitDataDefinitionForInheritors]
public abstract class BaseType {}

// Container будет DataDefinition
public sealed partial class Container : IContainer {}

// SomeStruct будет DataDefinition
public partial struct SomeStruct : IContainer {}

// SomeType будет DataDefinition
public sealed partial class SomeType : BaseType {}

Все типы, помеченные определённым атрибутом

Если вместо этого у вас есть атрибут, который вы добавите ко всем вашим определениям данных, добавьте атрибут [MeansDataDefinition] к вашему собственному атрибуту. Ярким примером этого является PrototypeAttribute, который вы, вероятно, видели раньше:

[MeansDataDefinition]
public sealed class PrototypeAttribute : Attribute {
    ...
}

// Любой класс, помеченный [Prototype], автоматически станет определением данных.

DataFields

Типы DataFields

Обычные

Любое поле или свойство в определении данных можно пометить атрибутом [DataField].
Далее и свойства, и поля будут просто называться «полем».

[DataField]
protected Color Color { get; set; } = Color.White;

Пример выше преобразуется в такой YAML:

color: White

Этот атрибут принимает необязательный строковый ключ, который можно использовать для определения ключа в YAML вместо camel-case-версии имени поля.
Если он не нужен, предпочтительно не указывать его.

[DataField("colorValue")]
protected Color Color { get; set; } = Color.White;

Пример выше преобразуется в такой YAML:

colorValue: White
Include DataField

DataDefinition записывается в MappingDataNode и читается из него. В отличие от обычного datafield, Include DataField не будет получать значение из ключа этого MappingDataNode для чтения/записи в/из поля, а вместо этого будет использовать MappingDataNode всего DataDefinition для выполнения своей операции чтения/записи. Это имеет особые последствия именно для записи: IncludeDataFields сериализуются последними, и полученный маппинг будет вставлен в маппинг datadefinition, который уже был создан. Если ключ уже существует, новое значение, полученное при сериализации IncludeDataField, будет проигнорировано.

Note

В будущем это поведение может стать настраиваемым.

Пользовательский сериализатор типов

Пользовательский сериализатор типов можно указать, если такового нет по умолчанию или требуется особое поведение для сериализации конкретного типа. Чтобы использовать его, передайте его через аргумент customTypeSerializer. И DataField, и IncludeDataField поддерживают интерфейсы пользовательских типов, но в следующих примерах используется только DataFieldAttribute, чтобы сделать их чуть менее громоздкими.

Warning

Этот тип НЕ должен реализовывать ITypeSerializer. Вам нужно реализовать только те интерфейсы, которые вам нужны! Любое другое поведение, не отличающееся от обычного, не нужно переопределять! Если интерфейс для конкретного действия не существует, просто будет использоваться обычное поведение!

[DataField(customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
private int DrawDepth { get; set; } = DrawDepthTag.Default;
Константы

При пометке int-поля, представляющего константу, определённую через [ConstantsForAttribute], в [DataField] необходимо указать пользовательский сериализатор типов:

/// <summary>
///     Тип-тег для определения представления глубины отрисовки рендеринга в
///     терминах именованных констант в контенте. Чтобы понять больше о
///     смысле этого типа, см. <see cref="ConstantsForAttribute"/>.
/// </summary>
public sealed class DrawDepth
{
    /// <summary>
    ///     Глубина отрисовки по умолчанию. Контентный enum, представляющий глубину
    ///     отрисовки, должен уважать это значение, так как оно используется в движке.
    /// </summary>
    public const int Default = 0;
}

public sealed partial class SpriteComponent
{
    [DataField(customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
    private int DrawDepth { get; set; } = DrawDepthTag.Default;
}
Флаги

Чтобы определить int-поля данных, представляющие флаговый enum, помеченный атрибутом [FlagsFor], процесс тот же, но используемый сериализатор другой.

/// <summary>
///     Тип-тег для определения представления битовой маски слоя столкновений
///     в терминах читаемых имён в контенте. Чтобы понять больше о
///     смысле этого типа, см. <see cref="FlagsForAttribute"/>.
/// </summary>
public sealed class CollisionLayer {}

public sealed partial class PhysShapeRect
{
    [DataField(customTypeSerializer: typeof(FlagSerializer<CollisionLayer>))]
    private int CollisionLayer { get; set; }
}

Поведение наследования

На datafield можно использовать два дополнительных атрибута, чтобы определить, как он наследуется: [AlwaysPushInheritance] и [NeverPushInheritance]. Это снова применимо и к DataField, и к IncludeDataField.

[AlwaysPushInheritance] используется в случаях, когда вы хотите, чтобы данные записи поля наследовались, даже когда они сопоставлены, например компоненты прототипа сущности.

[NeverPushInheritance] используется, чтобы сигнализировать, что значение, например, в прототипе не должно передаваться наследуемым прототипам, например abstract-свойство.

DataRecords

TODO

Type serializer

Интерфейсы сериализатора типов — это набор интерфейсов для определения пользовательской логики действий над конкретными типами. Иногда также указывается ожидаемый тип узла. Класс, реализующий хотя бы один из этих интерфейсов сериализатора типов, называется сериализатором типов. Если вы хотите, чтобы ваш сериализатор типов использовался всегда, пометьте его атрибутом [TypeSerializer]. В противном случае тип можно использовать как пользовательский сериализатор типов.

Статический IoCManager.Resolve не следует использовать, так как сериализатор может выполняться в отдельном потоке без инициализированного контекста IoC.

Serialization Context

Вы можете создать SerializationContext, реализовав интерфейс ISerializationContext для типа. Тип затем предоставит SerializationProvider, который он может использовать для регистрации typeserializer’ов. В настоящее время используется MapContext во время загрузки карт: https://github.com/space-wizards/RobustToolbox/blob/025fa958549b4d63e4888a810f780c53e6fb89a9/Robust.Shared/Map/MapSerializationContext.cs#L17-L51


Хомяк

hamletheldatgunpoint.png

Subpages