🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
🇺🇦 Українська (Ukrainian - Ukraine)
🇺🇦 Українська (Ukrainian - Ukraine)
Зовнішній вигляд
Ця сторінка написана для версії:
1.21.4
Ця сторінка написана для версії:
1.21.4
Кодек є системою для легкої серіалізації 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
відповідно.
BlockPos pos = new BlockPos(1, 2, 3);
// Серіалізація BlockPos до JsonElement
DataResult<JsonElement> result= BlockPos.CODEC.encodeStart(JsonOps.INSTANCE, pos);
Під час використання кодека значення повертаються у формі DataResult
. Це обгортка, яка може представляти або успіх чи невдача. Ми можемо використовувати це кількома способами: якщо нам просто потрібно наше серіалізоване значення, DataResult#result
буде просто поверніть Optional
, що містить наше значення, тоді як DataResult#resultOrPartial
також дозволяє нам надати функцію для усунення будь-яких помилок, які могли виникнути. Останнє особливо корисно для власних ресурсів пакетів даних, де ми б хотіли реєструвати помилки, не створюючи проблем деінде.
Тож візьмімо наше серіалізоване значення та перетворимо його назад на BlockPos
:
// Під час написання мода ви, звичайно, захочете правильно обробляти порожні 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);
Як згадувалося раніше, 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:
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; }
}
Відповідний файл json може виглядати приблизно так:
{
"beans_amount": 5,
"bean_type": "beanmod:mythical_beans",
"bean_positions": [
[1, 2, 3],
[4, 5, 6]
]
}
Ми можемо створити кодек для цього класу, об’єднавши декілька менших кодеків у більший. У цьому випадку нам знадобиться по одному для кожного поля:
Codec<Integer>
Codec<Item>
Codec<List<BlockPos>>
Ми можемо отримати перший з вищезгаданих примітивних кодеків у класі Codec
, зокрема Codec.INT
. Поки другий можна отримати з реєстру Registries.ITEM
, який має метод getCodec()
, який повертає Codec<Item>
. У нас немає стандартного кодека для List<BlockPos>
, але ми можемо створити його з BlockPos.CODEC
.
Codec#listOf
можна використовувати для створення версії списку будь-якого кодека:
Codec<List<BlockPos>> listCodec = BlockPos.CODEC.listOf();
Слід зазначити, що створені таким чином кодеки завжди десеріалізуються до ImmutableList
. Якщо замість цього вам потрібен змінний список, ви можете використати xmap, щоб перетворити його під час десеріалізації.
Тепер, коли у нас є окремі кодеки для кожного поля, ми можемо об’єднати їх в один кодек для нашого класу за допомогою RecordCodecBuilder
. Це передбачає, що наш клас має конструктор, який містить усі поля, які ми хочемо серіалізувати, і що кожне поле має відповідний метод отримання. Це робить його ідеальним для використання разом із записами, але він може також використовувати під час регулярних занять.
Розгляньмо, як створити кодек для нашого CoolBeansClass
:
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));
Кожен рядок у групі визначає кодек, назву поля та метод отримання. Для перетворення використовується виклик Codec#fieldOf
кодек у map codec, а виклик forGetter
визначає метод отримання, який використовується для отримання значення поля з екземпляра класу. Водночас виклик apply
визначає конструктор, який використовується для створення нового екземпляри. Зауважте, що порядок полів у групі має бути таким самим, як порядок аргументів у конструкторі.
Ви також можете використовувати Codec#optionalFieldOf
у цьому контексті, щоб зробити поле необов’язковим, як пояснюється в розділі необов’язкові поля.
Виклик Codec#fieldOf
перетворить Codec<T>
на MapCodec<T>
, який є варіантом, але не прямої реалізація Codec<T>
. MapCodec
s, як випливає з їх назви, гарантовано серіалізуються в ключ до мапи значень або його еквівалент у DynamicOps
. Для деяких функцій може знадобитися використання звичайного кодека.
Цей особливий спосіб створення MapCodec
по суті розміщує значення вихідного кодека всередині мапи, із вказаною назвою поля як ключем. Наприклад Codec<BlockPos>
після серіалізації в json виглядатиме так:
[1, 2, 3]
Але після перетворення на MapCodec<BlockPos>
за допомогою BlockPos.CODEC.fieldOf("pos")
це виглядатиме так:
{
"pos": [1, 2, 3]
}
Хоча найпоширенішим використанням мап кодеків є об’єднання з іншими мапами кодеками для створення кодека для повного класу поля, як пояснюється в розділі об’єднання кодеків для класів, подібних до записів вище, їх також можна повернути на звичайні кодеки за допомогою MapCodec#codec
, який збереже ту саму поведінку коробка їх вхідного значення.
Codec#optionalFieldOf
можна використовувати для створення додаткової мапи кодека. Це буде, коли вказане поле відсутнє у контейнері під час десеріалізації або бути десеріалізованим як порожній Необов’язковий
або вказане значення за замовчуванням.
// Без усталеного значення
MapCodec<Optional<BlockPos>> optionalCodec = BlockPos.CODEC.optionalFieldOf("pos");
// З усталеним значенням
MapCodec<BlockPos> optionalCodec = BlockPos.CODEC.optionalFieldOf("pos", BlockPos.ORIGIN);
Зауважте, що необов’язкові поля мовчки ігноруватимуть будь-які помилки, які можуть виникнути під час десеріалізації. Це означає, що якщо поле є, але значення недійсне, поле завжди буде десеріалізовано як значення за замовчуванням.
Починаючи з 1.20.2, сам Minecraft (не DFU!) однак надає Codecs#createStrictOptionalFieldCodec
, який взагалі не вдається десеріалізувати, якщо значення поля недійсне.
Codec.unit
можна використовувати для створення кодека, який завжди десеріалізується до постійного значення, незалежно від вхідних даних. Під час серіалізації це нічого не робитиме.
Codec<Integer> theMeaningOfCodec = Codec.unit(42);
Codec.intRange
та його друзі, Codec.floatRange
і Codec.doubleRange
можна використовувати для створення кодека, який приймає тільки числові значення в межах зазначеного включного діапазону. Це стосується як серіалізації, так і десеріалізації.
// Не може бути понад 2
Codec<Integer> amountOfFriendsYouHave = Codec.intRange(0, 2);
Codec.pair
об’єднує два кодеки, Codec<A>
і Codec<B>
, у Codec<Pair<A, B>>
. Майте на увазі, що він працює належним чином лише з кодеками, які серіалізуються в певне поле, наприклад перетворені MapCodec
s або кодеки запису. Отриманий кодек буде серіалізовано в мапу, що поєднує поля обох використаних кодеків.
Наприклад, запустіть цей код:
// Створіть два окремих коробкових кодека
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));
Виведе цей json:
{
"i_am_number": 23,
"this_statement_is_false": true
}
Codec.either
поєднує два кодеки, Codec<A>
і Codec<B>
, у Codec<Either<A, B>>
. Отриманий кодек під час десеріалізації спробує використати перший кодек і тільки якщо це не вдасться, спробує використати другий. Якщо другий також не вдається, буде повернено помилку другого кодека.
Для обробки мап із довільними ключами, такими як HashMap
s, можна використовувати Codec.unboundedMap
. Це повертає Codec<Map<K, V>>
для заданих Codec<K>
і Codec<V>
. Отриманий кодек буде серіалізовано в об’єкт json або будь-який еквівалент, доступний для поточних динамічних операцій.
Через обмеження json і nbt використовуваний ключовий кодек має серіалізуватися в рядок. Це включає кодеки для типів, які самі по собі не є рядками, але серіалізуються в них, наприклад Identifier.CODEC
. Дивіться приклад нижче:
// Створіть кодек для перетворення 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
));
Це виведе цей json:
{
"example:number": 23,
"example:the_cooler_number": 42
}
Як бачите, це працює, оскільки Identifier.CODEC
серіалізується безпосередньо до рядкового значення. Подібного ефекту можна досягти для простих об’єктів, які не серіалізуються в рядки, використовуючи xmap & friends для їх перетворення.
xmap
Скажімо, у нас є два класи, які можна конвертувати один в одного, але не мають стосунків «батьківський-дочірній». Наприклад, ванілльні BlockPos
і Vec3d
. Якщо у нас є кодек для одного, ми можемо використати Codec#xmap
, щоб створити кодек для іншого, визначення функції перетворення для кожного напрямку.
BlockPos
вже має кодек, але припустимо, що його немає. Ми можемо створити один для нього, базуючи його на кодек для Vec3d
ось так:
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`.
Codec#flatComapMap
, Codec#comapFlatMap
і flatXMap
схожі на xmap, але вони дозволяють одній або обом функціям перетворення повертати DataResult. Це корисно на практиці, оскільки конкретний екземпляр об’єкта може бути не таким завжди дійсні для перетворення.
Візьмемо, наприклад, ванілльні ідентифікатори
. Хоча всі ID можна перетворити на рядки, не всі рядки є дійсними ID, тому використання xmap означало б створювати неприємні винятки, коли перетворення не вдається. Через це його вбудований кодек насправді є comapFlatMap
на Codec.STRING
, гарно ілюструючи, як ним користуватися:
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());
}
}
// ...
}
Хоча ці методи дійсно корисні, їхні назви дещо заплутані, тому ось таблиця, яка допоможе вам запам’ятати, який з них використовувати:
Метод | A -> B усі дійсні? | B -> A усі дійсні? |
---|---|---|
Codec<A>#xmap | Так | Так |
Codec<A>#comapFlatMap | Ні | Так |
Codec<A>#flatComapMap | Так | Ні |
Codec<A>#flatXMap | Ні | Ні |
Codec#dispatch
дозволяє нам визначити реєстр кодеків і відправити до певного на основі значення у серіалізованих даних. Це дуже корисно під час десеріалізації об’єктів, які мають різні поля залежно від свого типу, але все ще представляють те саме.
Наприклад, скажімо, у нас є абстрактний інтерфейс Bean із двома класами реалізації: StringyBean
і CountingBean
. Серіалізувати ці з надсиланням реєстру, нам знадобиться кілька речей:
BeanType<T extends Bean>
, який представляє тип bean і може повертати кодек для нього.Bean
для отримання його BeanType<?>
.Identifier
з BeanType<?>
.Codec<BeanType<?>>
на основі цього реєстру. Якщо ви використовуєте net.minecraft.registry.Registry
, його можна легко створити за допомогою Registry#getCodec
.З усім цим ми можемо створити кодек відправки реєстру для компонентів:
// 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();
}
// 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());
}
// 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;
}
}
// 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;
}
}
// 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);
}
}
// Тепер ми можемо створити кодек для типів bean
// на основі раніше створеного реєстру
Codec<BeanType<?>> beanTypeCodec = BeanType.REGISTRY.getCodec();
// І виходячи з цього, ось наш кодек відправки реєстру для beans!
// Перший аргумент — це ім’я поля для типу компонента.
// Якщо пропущено, за умовчанням буде "тип".
Codec<Bean> beanCodec = beanTypeCodec.dispatch("type", Bean::getType, BeanType::codec);
Наш новий кодек серіалізує bean-файли в json таким чином, захоплюючи лише ті поля, які відповідають їх конкретному типу:
{
"type": "example:stringy_bean",
"stringy_string": "This bean is stringy!"
}
{
"type": "example:counting_bean",
"counting_number": 42
}
Іноді корисно мати кодек, який використовує самий для декодування певних полів, наприклад, при роботі з певними рекурсивними структурами даних. У звичайному коді це використовується для об’єктів Text
, які можуть зберігати інші Text
як дочірні. Такий кодек можна створити за допомогою Codec#recursive
.
Наприклад, спробуймо серіалізувати однозв'язний список. Цей спосіб представлення списків складається з групи вузлів, які містять як значення, так і посилання на наступний вузол у списку. Потім список представлено його першим вузлом, і перехід по списку здійснюється шляхом переходу за наступним вузлом, доки не залишиться жодного. Ось проста реалізація вузлів, які зберігають цілі числа.
public record ListNode(int value, ListNode next) {}
Ми не можемо створити кодек для цього звичайними засобами, оскільки який кодек ми використаємо для поля next
? Нам потрібен Codec<ListNode>
, який ми зараз розробляємо! Codec#recursive
дозволяє нам досягти цього за допомогою магічної на вигляд лямбди:
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)
);
}
);
Серіалізований ListNode
може виглядати так:
{
"value": 2,
"next": {
"value": 3,
"next": {
"value": 5
}
}
}