Кодек є системою для легкої серіалізації Java об'єктів, та є частиною бібліотеки Mojang's DataFixerUpper (DFU), яка постачається разом з Minecraft. У контексті модифікації їх можна використовувати як альтернативу до GSON і Jankson під час читання та запису користувацьких файлів json, хоча вони починають ставати все більш актуальними, оскільки Mojang переписує багато старого коду для використання кодеків.
Кодеки використовуються разом з іншим API DFU, "DynamicOps". Кодек визначає структуру об'єкта, а динамічні операції використовуються для визначення формату для серіалізації в і з, наприклад json або NBT. Це означає, що будь-який кодек може бути використовується з "DynamicOps", що забезпечує велику гнучкість.
Використання кодеків
Серіалізація та десеріалізація
Основним використанням кодека є серіалізація та десеріалізація об’єктів у певний формат і з нього.
Оскільки кілька класів ванілли вже мають визначені кодеки, ми можемо використати їх як приклад. Mojang також надав нам із двома динамічними класами ops за замовчуванням, JsonOps і NbtOps, які зазвичай охоплюють більшість випадків використання.
Тепер, припустімо, ми хочемо серіалізувати BlockPos у json і назад. Ми можемо зробити це за допомогою статично збереженого кодека у BlockPos.CODEC за допомогою методів Codec#encodeStart і Codec#parse відповідно.
java
BlockPos pos = new BlockPos(1, 2, 3);
// Серіалізація BlockPos до JsonElement
DataResult<JsonElement> result= BlockPos.CODEC.encodeStart(JsonOps.INSTANCE, pos);1
2
3
4
2
3
4
Під час використання кодека значення повертаються у формі DataResult. Це обгортка, яка може представляти або успіх чи невдача. Ми можемо використовувати це кількома способами: якщо нам просто потрібно наше серіалізоване значення, DataResult#result буде просто поверніть Optional, що містить наше значення, тоді як DataResult#resultOrPartial також дозволяє нам надати функцію для усунення будь-яких помилок, які могли виникнути. Останнє особливо корисно для власних ресурсів пакетів даних, де ми б хотіли реєструвати помилки, не створюючи проблем деінде.
Тож візьмімо наше серіалізоване значення та перетворимо його назад на BlockPos:
java
// Під час написання мода ви, звичайно, захочете правильно обробляти порожні Optionals
JsonElement json = result.resultOrPartial(LOGGER::error).orElseThrow();
// Тут ми маємо наше значення json, яке має відповідати `[1, 2, 3]`,
// as that's the format used by the BlockPos codec.
LOGGER.info("Serialized BlockPos: {}", json);
// Тепер ми десеріалізуємо JsonElement назад у BlockPos
DataResult<BlockPos> result = BlockPos.CODEC.parse(JsonOps.INSTANCE, json);
// Знову ж таки, ми просто візьмемо наше значення з результату
BlockPos pos = result.resultOrPartial(LOGGER::error).orElseThrow();
// І ми бачимо, що ми успішно серіалізували і десеріалізували наш BlockPos!
LOGGER.info("Deserialized BlockPos: {}", pos);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
Вбудовані кодеки
Як згадувалося раніше, Mojang уже визначив кодеки для кількох вільних і стандартних класів Java, включаючи, але не обмежено BlockPos, BlockState, ItemStack, Identifier, Text і регулярні вирази Pattern. Власні кодеки для Mojang класи зазвичай знаходяться як статичні поля з назвою CODEC у самому класі, тоді як більшість інших зберігаються в клас Codecs. Слід також зазначити, що всі реєстри ванілли містять метод getCodec(), наприклад, ви можна використовувати Registries.BLOCK.getCodec(), щоб отримати Codec<Block>, який серіалізується до ідентифікатора блоку та назад.
Сам Codec API також містить деякі кодеки для примітивних типів, як-от Codec.INT і Codec.STRING. Це доступні як статика в класі Codec і зазвичай використовуються як основа для складніших кодеків, як пояснюється нижче.
Побудова кодеків
Тепер, коли ми побачили, як використовувати кодеки, подивімося, як ми можемо створювати власні. Припустимо, ми маємо наступне класу, і ми хочемо десеріалізувати його екземпляри з файлів json:
java
public class CoolBeansClass {
private final int beansAmount;
private final Item beanType;
private final List<BlockPos> beanPositions;
public CoolBeansClass(int beansAmount, Item beanType, List<BlockPos> beanPositions) {...}
public int getBeansAmount() { return this.beansAmount; }
public Item getBeanType() { return this.beanType; }
public List<BlockPos> getBeanPositions() { return this.beanPositions; }
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Відповідний файл json може виглядати приблизно так:
json
{
"beans_amount": 5,
"bean_type": "beanmod:mythical_beans",
"bean_positions": [
[1, 2, 3],
[4, 5, 6]
]
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Ми можемо створити кодек для цього класу, об’єднавши декілька менших кодеків у більший. У цьому випадку нам знадобиться по одному для кожного поля:
Codec<Integer>Codec<Item>Codec<List<BlockPos>>
Ми можемо отримати перший з вищезгаданих примітивних кодеків у класі Codec, зокрема Codec.INT. Поки другий можна отримати з реєстру Registries.ITEM, який має метод getCodec(), який повертає Codec<Item>. У нас немає стандартного кодека для List<BlockPos>, але ми можемо створити його з BlockPos.CODEC.
Lists
Codec#listOf можна використовувати для створення версії списку будь-якого кодека:
java
Codec<List<BlockPos>> listCodec = BlockPos.CODEC.listOf();1
Слід зазначити, що створені таким чином кодеки завжди десеріалізуються до ImmutableList. Якщо замість цього вам потрібен змінний список, ви можете використати xmap, щоб перетворити його під час десеріалізації.
Об’єднання кодеків для класів, подібних до записів
Тепер, коли у нас є окремі кодеки для кожного поля, ми можемо об’єднати їх в один кодек для нашого класу за допомогою RecordCodecBuilder. Це передбачає, що наш клас має конструктор, який містить усі поля, які ми хочемо серіалізувати, і що кожне поле має відповідний метод отримання. Це робить його ідеальним для використання разом із записами, але він може також використовувати під час регулярних занять.
Розгляньмо, як створити кодек для нашого CoolBeansClass:
java
public static final Codec<CoolBeansClass> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.fieldOf("beans_amount").forGetter(CoolBeansClass::getBeansAmount),
Registries.ITEM.getCodec().fieldOf("bean_type").forGetter(CoolBeansClass::getBeanType),
BlockPos.CODEC.listOf().fieldOf("bean_positions").forGetter(CoolBeansClass::getBeanPositions)
// Тут можна оголосити до 16 полів
).apply(instance, CoolBeansClass::new));1
2
3
4
5
6
2
3
4
5
6
Кожен рядок у групі визначає кодек, назву поля та метод отримання. Для перетворення використовується виклик Codec#fieldOf кодек у map codec, а виклик forGetter визначає метод отримання, який використовується для отримання значення поля з екземпляра класу. Водночас виклик apply визначає конструктор, який використовується для створення нового екземпляри. Зауважте, що порядок полів у групі має бути таким самим, як порядок аргументів у конструкторі.
Ви також можете використовувати Codec#optionalFieldOf у цьому контексті, щоб зробити поле необов’язковим, як пояснюється в розділі необов’язкові поля.
MapCodec, не плутати з Codec<Map>
Виклик Codec#fieldOf перетворить Codec<T> на MapCodec<T>, який є варіантом, але не прямої реалізація Codec<T>. MapCodecs, як випливає з їх назви, гарантовано серіалізуються в ключ до мапи значень або його еквівалент у DynamicOps. Для деяких функцій може знадобитися використання звичайного кодека.
Цей особливий спосіб створення MapCodec по суті розміщує значення вихідного кодека всередині мапи, із вказаною назвою поля як ключем. Наприклад Codec<BlockPos> після серіалізації в json виглядатиме так:
json
[1, 2, 3]1
Але після перетворення на MapCodec<BlockPos> за допомогою BlockPos.CODEC.fieldOf("pos") це виглядатиме так:
json
{
"pos": [1, 2, 3]
}1
2
3
2
3
Хоча найпоширенішим використанням мап кодеків є об’єднання з іншими мапами кодеками для створення кодека для повного класу поля, як пояснюється в розділі об’єднання кодеків для класів, подібних до записів вище, їх також можна повернути на звичайні кодеки за допомогою MapCodec#codec, який збереже ту саму поведінку коробка їх вхідного значення.
Необов'язкові поля
Codec#optionalFieldOf можна використовувати для створення додаткової мапи кодека. Це буде, коли вказане поле відсутнє у контейнері під час десеріалізації або бути десеріалізованим як порожній Необов’язковий або вказане значення за замовчуванням.
java
// Без усталеного значення
MapCodec<Optional<BlockPos>> optionalCodec = BlockPos.CODEC.optionalFieldOf("pos");
// З усталеним значенням
MapCodec<BlockPos> optionalCodec = BlockPos.CODEC.optionalFieldOf("pos", BlockPos.ORIGIN);1
2
3
4
5
2
3
4
5
Зауважте, що необов’язкові поля мовчки ігноруватимуть будь-які помилки, які можуть виникнути під час десеріалізації. Це означає, що якщо поле є, але значення недійсне, поле завжди буде десеріалізовано як значення за замовчуванням.
Починаючи з 1.20.2, сам Minecraft (не DFU!) однак надає Codecs#createStrictOptionalFieldCodec, який взагалі не вдається десеріалізувати, якщо значення поля недійсне.
Константи, обмеження та композиція
Юніт
Codec.unit можна використовувати для створення кодека, який завжди десеріалізується до постійного значення, незалежно від вхідних даних. Під час серіалізації це нічого не робитиме.
java
Codec<Integer> theMeaningOfCodec = Codec.unit(42);1
Числові діапазони
Codec.intRange та його друзі, Codec.floatRange і Codec.doubleRange можна використовувати для створення кодека, який приймає тільки числові значення в межах зазначеного включного діапазону. Це стосується як серіалізації, так і десеріалізації.
java
// Не може бути понад 2
Codec<Integer> amountOfFriendsYouHave = Codec.intRange(0, 2);1
2
2
Пара
Codec.pair об’єднує два кодеки, Codec<A> і Codec<B>, у Codec<Pair<A, B>>. Майте на увазі, що він працює належним чином лише з кодеками, які серіалізуються в певне поле, наприклад перетворені MapCodecs або кодеки запису. Отриманий кодек буде серіалізовано в мапу, що поєднує поля обох використаних кодеків.
Наприклад, запустіть цей код:
java
// Створіть два окремих коробкових кодека
Codec<Integer> firstCodec = Codec.INT.fieldOf("i_am_number").codec();
Codec<Boolean> secondCodec = Codec.BOOL.fieldOf("this_statement_is_false").codec();
// Об’єднайте їх у парний кодек
Codec<Pair<Integer, Boolean>> pairCodec = Codec.pair(firstCodec, secondCodec);
// Використовуйте його для серіалізації даних
DataResult<JsonElement> result = pairCodec.encodeStart(JsonOps.INSTANCE, Pair.of(23, true));1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Виведе цей json:
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>>. Отриманий кодек під час десеріалізації спробує використати перший кодек і тільки якщо це не вдасться, спробує використати другий. Якщо другий також не вдається, буде повернено помилку другого кодека.
Мапи
Для обробки мап із довільними ключами, такими як HashMaps, можна використовувати Codec.unboundedMap. Це повертає Codec<Map<K, V>> для заданих Codec<K> і Codec<V>. Отриманий кодек буде серіалізовано в об’єкт json або будь-який еквівалент, доступний для поточних динамічних операцій.
Через обмеження json і nbt використовуваний ключовий кодек має серіалізуватися в рядок. Це включає кодеки для типів, які самі по собі не є рядками, але серіалізуються в них, наприклад Identifier.CODEC. Дивіться приклад нижче:
java
// Створіть кодек для перетворення ID на цілі числа
Codec<Map<Identifier, Integer>> mapCodec = Codec.unboundedMap(Identifier.CODEC, Codec.INT);
// Використовуйте його для серіалізації даних
DataResult<JsonElement> result = mapCodec.encodeStart(JsonOps.INSTANCE, Map.of(
new Identifier("example", "number"), 23,
new Identifier("example", "the_cooler_number"), 42
));1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Це виведе цей json:
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 = Vec3d.CODEC.xmap(
// Перетворення Vec3d у BlockPos
vec -> new BlockPos(vec.x, vec.y, vec.z),
// Перетворення BlockPos у Vec3d
pos -> new Vec3d(pos.getX(), pos.getY(), pos.getZ())
);
// Під час перетворення існуючого класу (наприклад, `X`)
// до вашого власного класу (`Y`) таким чином, це може бути добре
// додати `toX` і статичні методи `fromX` до `Y` і використовувати
// посилання на методи у вашому виклику `xmap`.1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
flatComapMap, comapFlatMap, і flatXMap
Codec#flatComapMap, Codec#comapFlatMap і flatXMap схожі на xmap, але вони дозволяють одній або обом функціям перетворення повертати DataResult. Це корисно на практиці, оскільки конкретний екземпляр об’єкта може бути не таким завжди дійсні для перетворення.
Візьмемо, наприклад, ванілльні ідентифікатори. Хоча всі ID можна перетворити на рядки, не всі рядки є дійсними ID, тому використання xmap означало б створювати неприємні винятки, коли перетворення не вдається. Через це його вбудований кодек насправді є comapFlatMap на Codec.STRING, гарно ілюструючи, як ним користуватися:
java
public class Identifier {
public static final Codec<Identifier> CODEC = Codec.STRING.comapFlatMap(
Identifier::validate, Identifier::toString
);
// ...
public static DataResult<Identifier> validate(String id) {
try {
return DataResult.success(new Identifier(id));
} catch (InvalidIdentifierException e) {
return DataResult.error("Not a valid resource location: " + id + " " + e.getMessage());
}
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Хоча ці методи дійсно корисні, їхні назви дещо заплутані, тому ось таблиця, яка допоможе вам запам’ятати, який з них використовувати:
| Метод | 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.registry.Registry, його можна легко створити за допомогоюRegistry#getCodec.
З усім цим ми можемо створити кодек відправки реєстру для компонентів:
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.getCodec()
// 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 SimpleRegistry<>(
RegistryKey.ofRegistry(Identifier.of("example", "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.of("example", id), beanType);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
java
// Тепер ми можемо створити кодек для типів bean
// на основі раніше створеного реєстру
Codec<BeanType<?>> beanTypeCodec = BeanType.REGISTRY.getCodec();
// І виходячи з цього, ось наш кодек відправки реєстру для beans!
// Перший аргумент — це ім’я поля для типу компонента.
// Якщо пропущено, за умовчанням буде "тип".
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
Наш новий кодек серіалізує bean-файли в json таким чином, захоплюючи лише ті поля, які відповідають їх конкретному типу:
json
{
"type": "example:stringy_bean",
"stringy_string": "This bean is stringy!"
}1
2
3
4
2
3
4
json
{
"type": "example:counting_bean",
"counting_number": 42
}1
2
3
4
2
3
4
Рекурсивні кодеки
Іноді корисно мати кодек, який використовує самий для декодування певних полів, наприклад, при роботі з певними рекурсивними структурами даних. У звичайному коді це використовується для об’єктів Text, які можуть зберігати інші Text як дочірні. Такий кодек можна створити за допомогою Codec#recursive.
Наприклад, спробуймо серіалізувати однозв'язний список. Цей спосіб представлення списків складається з групи вузлів, які містять як значення, так і посилання на наступний вузол у списку. Потім список представлено його першим вузлом, і перехід по списку здійснюється шляхом переходу за наступним вузлом, доки не залишиться жодного. Ось проста реалізація вузлів, які зберігають цілі числа.
java
public record ListNode(int value, ListNode next) {}1
Ми не можемо створити кодек для цього звичайними засобами, оскільки який кодек ми використаємо для поля next? Нам потрібен Codec<ListNode>, який ми зараз розробляємо! Codec#recursive дозволяє нам досягти цього за допомогою магічної на вигляд лямбди:
java
Codec<ListNode> codec = Codec.recursive(
"ListNode", // a name for the codec
selfCodec -> {
// Тут `selfCodec` представляє `Codec<ListNode>`, ніби він уже створений
// Цей лямбда повинен повернути кодек, який ми хотіли використовувати з самого початку,
// який посилається на себе через `selfCodec`
return RecordCodecBuilder.create(instance ->
instance.group(
Codec.INT.fieldOf("value").forGetter(ListNode::value),
// поле `next` оброблятиметься рекурсивно за допомогою власного кодека
Codecs.createStrictOptionalFieldCodec(selfCodec, "next", null).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
Серіалізований ListNode може виглядати так:
json
{
"value": 2,
"next": {
"value": 3,
"next": {
"value": 5
}
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Список літератури
- Набагато вичерпнішу документацію щодо кодеків і пов’язаних API можна знайти за адресою неофіційний DFU JavaDoc.
- Загальна структура цього посібника була значною мірою натхненна сторінкою Forge Community Wiki про кодеки, більш специфічний для Forge погляд на те саме тема.


