Кодеки 26.1.2
Повний посібник для розуміння і використання системи кодеків Mojang для серіалізації та десеріалізації об'єктів.
Кодек є системою для легкої серіалізації об'єктів Java, та є частиною бібліотеки Mojang DataFixerUpper (DFU), яка постачається разом з Minecraft. У контексті модифікації їх можна використовувати як альтернативу до GSON і Jankson під час читання та запису користувацьких файлів JSON, хоча вони починають ставати все більш актуальними, оскільки Mojang переписує багато старого коду для використання кодеків.
Кодеки використовуються разом з іншим API DFU, DynamicOps. Кодек визначає структуру об'єкта, а динамічні операції використовуються для визначення формату для серіалізації в і з, наприклад JSON або NBT. Це означає, що будь-який кодек може бути використовується з «dynamic ops», що забезпечує велику гнучкість.
Використання кодеків
Серіалізація та десеріалізація
Основним використанням кодека є серіалізація та десеріалізація об’єктів у певний формат і з нього.
Оскільки кілька усталених класів вже мають визначені кодеки, ми можемо використати їх як приклад. Mojang також надав нам із двома динамічними класами ops усталено, JsonOps і NbtOps, які зазвичай охоплюють більшість випадків використання.
Тепер, припустімо, ми хочемо серіалізувати BlockPos у JSON і назад. Ми можемо зробити це за допомогою статично збереженого кодека у BlockPos.CODEC за допомогою методів Codec#encodeStart і Codec#parse відповідно.
java
BlockPos pos = new BlockPos(1, 2, 3);
// Serialize the BlockPos to a JsonElement
DataResult<JsonElement> serializeResult = BlockPos.CODEC.encodeStart(JsonOps.INSTANCE, pos);
// When actually writing a mod, you'll want to properly handle empty Optionals of course
JsonElement json = serializeResult.resultOrPartial(LOGGER::error).orElseThrow();
// Here we have our JSON value, which should correspond to `[1, 2, 3]`,
// as that's the format used by the BlockPos codec.
LOGGER.info("Serialized BlockPos: {}", json);1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
json
[
1,
2,
3
]1
2
3
4
5
2
3
4
5
Під час використання кодека значення повертаються у формі DataResult. Це обгортка, яка може представляти або успіх чи невдача. Ми можемо використовувати це кількома способами: якщо нам просто потрібно наше серіалізоване значення, DataResult#result буде просто поверніть Optional, що містить наше значення, тоді як DataResult#resultOrPartial також дозволяє нам надати функцію для усунення будь-яких помилок, які могли виникнути. Останнє особливо корисно для власних ресурсів пакетів даних, де ми б хотіли журналювати помилки, не створюючи проблем деінде.
Тож візьмімо наше серіалізоване значення та перетворимо його назад на BlockPos:
java
// Now we'll deserialize the JsonElement back into a BlockPos
DataResult<BlockPos> deserializeResult = BlockPos.CODEC.parse(JsonOps.INSTANCE, json);
// Again, we'll just grab our value from the deserializeResult
BlockPos deserializedPos = deserializeResult.resultOrPartial(LOGGER::error).orElseThrow();
// And we can see that we've successfully serialized and deserialized our BlockPos!
LOGGER.info("Deserialized BlockPos: {}", deserializedPos);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Вбудовані кодеки
Як згадувалося раніше, Mojang уже визначив кодеки для кількох стандартних класів Java, включаючи, але не обмежуючись BlockPos, BlockState, ItemStack, Identifier, Component і регулярні вирази Pattern. Власні кодеки для Mojang класи зазвичай знаходяться як статичні поля з назвою CODEC у самому класі, тоді як більшість інших зберігаються в клас Codecs. Слід також зазначити, що всі усталені реєстри містять метод Codec, наприклад, ви можна використовувати BuiltInRegistries.BLOCK.byNameCodec(), щоб отримати Codec<Block>, який серіалізується до ID блока та назад і holderByNameCodec(), щоб отримати Codec<Holder<Block>>.
Сам Codec API також містить деякі кодеки для примітивних типів, як-от Codec.INT і Codec.STRING. Вони доступні як статика в класі Codec і зазвичай використовуються як основа для складніших кодеків, як пояснюється нижче.
Побудова кодеків
Тепер, коли ми побачили, як використовувати кодеки, подивімося, як ми можемо створювати власні. Припустимо, ми маємо наступний клас, і ми хочемо десеріалізувати його екземпляри з файлів JSON:
java
public class CoolBeansClass {
private final int beansAmount;
private final Holder<Item> beanType;
private final List<BlockPos> beanPositions;
public CoolBeansClass(int beansAmount, Holder<Item> beanType, List<BlockPos> beanPositions) {
// ...
}
public int getBeansAmount() {
return this.beansAmount;
}
public Holder<Item> getBeanType() {
return this.beanType;
}
public List<BlockPos> getBeanPositions() {
return this.beanPositions;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Відповідний файл JSON може виглядати приблизно так:
json
{
"bean_positions": [
[
1,
2,
3
],
[
4,
5,
6
]
],
"bean_type": "example-mod:lightning_tater",
"beans_amount": 5
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ми можемо створити кодек для цього класу, об’єднавши декілька менших кодеків у більший. У цьому випадку нам знадобиться один для кожного поля:
Codec<Integer>Codec<Item>Codec<List<BlockPos>>
Ми можемо отримати перший з вищезгаданих примітивних кодеків у класі Codec, зокрема Codec.INT. Поки другий можна отримати з реєстру BuiltInRegistries.ITEM, який має метод getCodec(), який повертає Codec<Item>. У нас немає стандартного кодека для List<BlockPos>, але ми можемо створити його з BlockPos.CODEC.
Списки
Codec#listOf можна використовувати для створення версії списку будь-якого кодека:
java
Codec<List<BlockPos>> listCodec = BlockPos.CODEC.listOf();1
java
List<BlockPos> data = List.of(new BlockPos(10, 5, 7));1
json
[
[
10,
5,
7
]
]1
2
3
4
5
6
7
2
3
4
5
6
7
Слід зазначити, що створені таким чином кодеки завжди десеріалізуються до ImmutableList. Якщо замість цього вам потрібен змінний список, ви можете використати xmap, щоб перетворити його під час десеріалізації.
Об’єднання кодеків для класів, подібних до записів
Тепер, коли у нас є окремі кодеки для кожного поля, ми можемо об’єднати їх в один кодек для нашого класу за допомогою RecordCodecBuilder. Це передбачає, що наш клас має конструктор, який містить усі поля, які ми хочемо серіалізувати, і що кожне поле має відповідний метод отримання. Це робить його ідеальним для використання разом із записами, але він може також використовувати під час регулярних занять.
Розгляньмо, як створити кодек для нашого CoolBeansClass:
java
public static final Codec<CoolBeansClass> CODEC = RecordCodecBuilder.create(instance -> instance.group(
// Up to 16 fields can be declared here
Codec.INT.fieldOf("beans_amount").forGetter(CoolBeansClass::getBeansAmount),
Item.CODEC.fieldOf("bean_type").forGetter(CoolBeansClass::getBeanType),
BlockPos.CODEC.listOf().fieldOf("bean_positions").forGetter(CoolBeansClass::getBeanPositions)
)
.apply(instance, CoolBeansClass::new));1
2
3
4
5
6
7
2
3
4
5
6
7
java
CoolBeansClass bean = new CoolBeansClass(
5,
BuiltInRegistries.ITEM.wrapAsHolder(ModItems.LIGHTNING_TATER),
List.of(
new BlockPos(1, 2, 3),
new BlockPos(4, 5, 6)
)
);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
json
{
"bean_positions": [
[
1,
2,
3
],
[
4,
5,
6
]
],
"bean_type": "example-mod:lightning_tater",
"beans_amount": 5
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Кожен рядок у групі визначає кодек, назву поля та метод отримання. Для перетворення використовується виклик Codec#fieldOf кодек у кодеку мапи, а виклик forGetter визначає метод отримання, який використовується для отримання значення поля з екземпляра класу. Водночас виклик apply визначає конструктор, який використовується для створення нових екземплярів. Зауважте, що порядок полів у групі має бути таким самим, як порядок аргументів у конструкторі.
Ви також можете використовувати Codec#optionalFieldOf у цьому контексті, щоб зробити поле необов’язковим, як пояснюється в розділі необов’язкових полів.
MapCodec, не плутати з Codec<Map>
Виклик Codec#fieldOf перетворить Codec<T> на MapCodec<T>, який є варіантом, але не прямої реалізація Codec<T>. MapCodec, як випливає з їх назви, гарантовано серіалізуються в ключ до мапи значень або його еквівалент у DynamicOps. Для деяких функцій може знадобитися використання звичайного кодека.
Цей особливий спосіб створення MapCodec по суті розміщує значення початкового кодека всередині мапи, з указаною назвою поля як ключем. Наприклад Codec<BlockPos> після серіалізації в JSON виглядатиме так:
json
[
1,
2,
3
]1
2
3
4
5
2
3
4
5
Але після перетворення на MapCodec<BlockPos> за допомогою BlockPos.CODEC.fieldOf("pos") це виглядатиме так:
json
{
"pos": [
1,
2,
3
]
}1
2
3
4
5
6
7
2
3
4
5
6
7
Хоча найпоширенішим використанням кодеків мап є об’єднання з іншими кодеками мап для створення кодека для повного класу полів, як пояснюється в розділі злиття кодеків для класів, подібних до записів вище, їх також можна повернути назад у звичайні кодеки за допомогою MapCodec#codec, який збереже таку саму поведінку коробки свого вхідного значення.
Необов'язкові поля
Codec#optionalFieldOf можна використовувати для створення додаткової мапи кодека. Це буде, коли вказане поле відсутнє у контейнері під час десеріалізації або бути десеріалізованим як порожній необов’язковий або вказане усталене значення.
java
MapCodec<Optional<BlockPos>> optionalCodec = BlockPos.CODEC.optionalFieldOf("pos");1
java
Optional<BlockPos> optionalBlockPos = Optional.empty();1
json
{}1
Щоб додати усталене значення, ми можемо передати його як другий параметр у методі optionalFieldOf.
java
MapCodec<BlockPos> defaultCodec = BlockPos.CODEC.optionalFieldOf("pos", BlockPos.ZERO);1
java
BlockPos defaultBlockPos = BlockPos.ZERO;1
json
{}1
Зауважте, що якщо поле присутнє, але значення недійсне, поле не вдається десеріалізувати взагалі, якщо значення поля недійсне.
Константи, обмеження та композиція
Юніт
MapCodec.unitCodec можна використовувати для створення кодека, який завжди десеріалізується до постійного значення, незалежно від вхідних даних. Під час серіалізації це нічого не робитиме.
java
Codec<Integer> theMeaningOfCodec = MapCodec.unitCodec(42);1
json
{}1
Числові діапазони
Codec.intRange та його друзі, Codec.floatRange і Codec.doubleRange можна використовувати для створення кодека, який приймає тільки числові значення в межах зазначеного включеного діапазону. Це стосується як серіалізації, так і десеріалізації.
java
// Can't be more than 2
Codec<Integer> amountOfFriendsYouHave = Codec.intRange(0, 2);1
2
2
java
int amount = 2;1
json
21
Пара
Codec.pair об’єднує два кодеки, Codec<A> і Codec<B>, у Codec<Pair<A, B>>. Майте на увазі, що він працює належним чином лише з кодеками, які серіалізуються в певне поле, наприклад перетворені MapCodec або кодеки запису. Отриманий кодек буде серіалізовано в мапу, що поєднує поля обох використаних кодеків.
java
// Create two separate boxed codecs
Codec<Integer> firstCodec = Codec.INT.fieldOf("i_am_number").codec();
Codec<Boolean> secondCodec = Codec.BOOL.fieldOf("this_statement_is_false").codec();
// And merge them into a pair codec
Codec<Pair<Integer, Boolean>> pairCodec = Codec.pair(firstCodec, secondCodec);1
2
3
4
5
6
2
3
4
5
6
java
Pair<Integer, Boolean> pair = Pair.of(23, true);1
json
{
"i_am_number": 23,
"this_statement_is_false": true
}1
2
3
4
2
3
4
Кожен
Codec.either поєднує два кодеки, Codec<A> і Codec<B>, у Codec<Either<A, B>>. Отриманий кодек під час десеріалізації спробує використати перший кодек і тільки якщо це не вдасться, спробує використати другий. Якщо другий також не вдається, буде повернено помилку другого кодека.
Мапи
Для обробки мап із довільними ключами, такими як HashMap, можна використовувати Codec.unboundedMap. Це повертає Codec<Map<K, V>> для заданих Codec<K> і Codec<V>. Отриманий кодек буде серіалізовано в об’єкт JSON або будь-який еквівалент, доступний для поточних динамічних операцій.
Через обмеження JSON і NBT використовуваний ключовий кодек має серіалізуватися в рядок. Це включає кодеки для типів, які самі по собі не є рядками, але їх серіалізують, наприклад Identifier.CODEC. Дивіться приклад нижче:
java
// Create a codec for a map of Identifiers to integers
Codec<Map<Identifier, Integer>> mapCodec = Codec.unboundedMap(Identifier.CODEC, Codec.INT);1
2
2
java
Map<Identifier, Integer> map = Map.of(
Identifier.fromNamespaceAndPath("example", "number"), 23,
Identifier.fromNamespaceAndPath("example", "the_cooler_number"), 42
);1
2
3
4
2
3
4
json
{
"example:number": 23,
"example:the_cooler_number": 42
}1
2
3
4
2
3
4
Як бачите, це працює, оскільки Identifier.CODEC серіалізується безпосередньо до рядкового значення. Подібного ефекту можна досягти для простих об’єктів, які не серіалізуються в рядки, використовуючи xmap & friends для їх перетворення.
Взаємно конвертовані типи
xmap
Скажімо, у нас є два класи, які можна конвертувати один в одного, але не мають стосунків «батьківський-дочірній». Наприклад, усталені BlockPos і Vec3d. Якщо у нас є кодек для одного, ми можемо використати Codec#xmap, щоб створити кодек для іншого, визначення функції перетворення для кожного напрямку.
BlockPos вже має кодек, але припустимо, що його немає. Ми можемо створити один для нього, базуючи його на кодек для Vec3d ось так:
java
Codec<BlockPos> blockPosCodec = Vec3i.CODEC.xmap(
// Convert Vec3i to BlockPos
vec -> new BlockPos(vec.getX(), vec.getY(), vec.getZ()),
// Convert BlockPos to Vec3i
pos -> new Vec3i(pos.getX(), pos.getY(), pos.getZ())
);
// When converting an existing class (`X` for example)
// to your own class (`Y`) this way, it may be nice to
// add `toX` and static `fromX` methods to `Y` and use
// method references in your `xmap` call.1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
java
BlockPos pos = new BlockPos(1, 2, 3);1
json
[
1,
2,
3
]1
2
3
4
5
2
3
4
5
flatComapMap, comapFlatMap, і flatXMap
Codec#flatComapMap, Codec#comapFlatMap і flatXMap схожі на xmap, але вони дозволяють одній або обом функціям перетворення повертати DataResult. Це корисно на практиці, оскільки конкретний екземпляр об’єкта може бути не таким завжди дійсні для перетворення.
Візьмемо, наприклад, стандартний Identifier. Хоча всі ідентифікатори можна перетворити на рядки, не всі рядки є дійсними ідентифікаторами, тому використання xmap означало б створювати неприємні винятки, коли перетворення не вдається. Через це його вбудований кодек насправді є comapFlatMap на Codec.STRING, гарно ілюструючи, як ним користуватися:
java
public class Identifier {
public static final Codec<Identifier> CODEC = Codec.STRING.comapFlatMap(
Identifier::read, Identifier::toString
);
// ...
public static DataResult<Identifier> read(String input) {
try {
return DataResult.success(Identifier.parse(input));
} catch (IdentifierException e) {
return DataResult.error(() -> "Not a valid identifier: " + input + " " + e.getMessage());
}
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Хоча ці методи дійсно корисні, їхні назви дещо заплутані, тому ось таблиця, яка допоможе вам запам’ятати, який з них використовувати:
| Метод | A —> B усі дійсні? | B —> A усі дійсні? |
|---|---|---|
Codec<A>#xmap | Так | Так |
Codec<A>#comapFlatMap | Ні | Так |
Codec<A>#flatComapMap | Так | Ні |
Codec<A>#flatXMap | Ні | Ні |
Відправлення реєстру
Codec#dispatch дозволяє нам визначити реєстр кодеків і відправити до певного на основі значення у серіалізованих даних. Це дуже корисно під час десеріалізації об’єктів, які мають різні поля залежно від свого типу, але все ще представляють те саме.
Наприклад, скажімо, у нас є абстрактний інтерфейс Bean із двома класами реалізації: StringyBean і CountingBean. Серіалізувати ці з надсиланням реєстру, нам знадобиться кілька речей:
- Окремі кодеки для кожного типу bean.
- Клас або запис
BeanType<T extends Bean>, який представляє тип bean і може повертати кодек для нього. - Функція на
Beanдля отримання йогоBeanType<?>. - Мапа або реєстр для зіставлення
IdentifierзBeanType<?>. Codec<BeanType<?>>на основі цього реєстру. Якщо ви використовуєтеnet.minecraft.core.Registry, його можна легко створити за допомогоюRegistry#byNameCodec.
З усім цим ми можемо створити кодек відправлення реєстру для компонентів:
java
// Now we can create a codec for bean types
// based on the previously created registry
Codec<BeanType<?>> beanTypeCodec = BeanType.REGISTRY.byNameCodec();
// And based on that, here's our registry dispatch codec for beans!
// The first argument is the field name for the bean type.
// When left out, it will default to "type".
Codec<Bean> beanCodec = beanTypeCodec.dispatch("type", Bean::getType, BeanType::codec);1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
java
// The abstract type we want to create a codec for
public interface Bean {
// Now we can create a codec for bean types based on the previously created registry.
Codec<Bean> BEAN_CODEC = BeanType.REGISTRY.byNameCodec()
// And based on that, here's our registry dispatch codec for beans!
// The first argument is the field name for the bean type.
// When left out, it will default to "type".
.dispatch("type", Bean::getType, BeanType::codec);
BeanType<?> getType();
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
java
// A record to keep information relating to a specific
// subclass of Bean, in this case only holding a Codec.
public record BeanType<T extends Bean>(MapCodec<T> codec) {
// Create a registry to map identifiers to bean types
public static final Registry<BeanType<?>> REGISTRY = new MappedRegistry<>(
ResourceKey.createRegistryKey(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "bean_types")), Lifecycle.stable());
}1
2
3
4
5
6
7
2
3
4
5
6
7
java
// An implementing class of Bean, with its own codec.
public class StringyBean implements Bean {
public static final MapCodec<StringyBean> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.STRING.fieldOf("stringy_string").forGetter(StringyBean::getStringyString)
).apply(instance, StringyBean::new));
private String stringyString;
// It is important to be able to retrieve the
// BeanType of a Bean from it's instance.
@Override
public BeanType<?> getType() {
return BeanTypes.STRINGY_BEAN;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java
// Another implementation
public class CountingBean implements Bean {
public static final MapCodec<CountingBean> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
Codec.INT.fieldOf("counting_number").forGetter(CountingBean::getCountingNumber)
).apply(instance, CountingBean::new));
private int countingNumber;
@Override
public BeanType<?> getType() {
return BeanTypes.COUNTING_BEAN;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
java
// An empty class to hold static references to all BeanTypes
public class BeanTypes {
// Make sure to register the bean types and leave them accessible to
// the getType method in their respective subclasses.
public static final BeanType<StringyBean> STRINGY_BEAN = register("stringy_bean", new BeanType<>(StringyBean.CODEC));
public static final BeanType<CountingBean> COUNTING_BEAN = register("counting_bean", new BeanType<>(CountingBean.CODEC));
public static <T extends Bean> BeanType<T> register(String id, BeanType<T> beanType) {
return Registry.register(BeanType.REGISTRY, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, id), beanType);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Наш новий кодек серіалізує bean-файли в JSON таким чином, захоплюючи лише ті поля, які відповідають їх конкретному типу:
json
{
"type": "example-mod:stringy_bean",
"stringy_string": "This bean is stringy!"
}1
2
3
4
2
3
4
json
{
"type": "example-mod:counting_bean",
"counting_number": 42
}1
2
3
4
2
3
4
Рекурсивні кодеки
Іноді корисно мати кодек, який використовує себе для декодування певних полів, наприклад, при роботі з певними рекурсивними структурами даних. У звичайному коді це використовується для об’єктів Component, які можуть зберігати інші Component як дочірні. Такий кодек можна створити за допомогою Codec#recursive.
Наприклад, спробуймо серіалізувати однозв'язний список. Цей спосіб представлення списків складається з групи нодів, які містять як значення, так і посилання на наступний нод у списку. Потім список представлено його першим нодом, і перехід по списку здійснюється шляхом переходу за наступним нодом доки не залишиться жодного. Ось проста реалізація нодів, які зберігають цілі числа.
java
public record ListNode(int value, Optional<ListNode> next) {
}1
2
2
Ми не можемо створити кодек для цього звичайними засобами, оскільки який кодек ми використаємо для поля next? Нам потрібен Codec<ListNode>, який ми зараз розробляємо! Codec#recursive дозволяє нам досягти цього за допомогою магічної на вигляд лямбди:
java
Codec<ListNode> codec = Codec.recursive(
"ListNode", // a name for the codec
selfCodec -> {
// Here, `selfCodec` represents the `Codec<ListNode>`, as if it was already constructed
// This lambda should return the codec we wanted to use from the start,
// that refers to itself through `selfCodec`
return RecordCodecBuilder.create(instance ->
instance.group(
Codec.INT.fieldOf("value").forGetter(ListNode::value),
// the `next` field will be handled recursively with the self-codec
selfCodec.optionalFieldOf("next").forGetter(ListNode::next)
).apply(instance, ListNode::new)
);
}
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java
ListNode linkedList = new ListNode(
2,
Optional.of(
new ListNode(
3,
Optional.of(
new ListNode(
5,
Optional.empty()
)
)
)
)
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
json
{
"next": {
"next": {
"value": 5
},
"value": 3
},
"value": 2
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9



