001/*
002 * Copyright 2020 zml
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package ca.stellardrift.confabricate.typeserializers;
017
018import static java.util.Objects.requireNonNull;
019
020import ca.stellardrift.confabricate.mixin.FluidTagsAccessor;
021import com.google.common.collect.ImmutableSet;
022import com.google.errorprone.annotations.concurrent.LazyInit;
023import com.mojang.serialization.Codec;
024import com.mojang.serialization.DynamicOps;
025import io.leangen.geantyref.GenericTypeReflector;
026import io.leangen.geantyref.TypeFactory;
027import io.leangen.geantyref.TypeToken;
028import net.fabricmc.api.EnvType;
029import net.fabricmc.api.Environment;
030import net.minecraft.block.Block;
031import net.minecraft.client.MinecraftClient;
032import net.minecraft.entity.EntityType;
033import net.minecraft.fluid.Fluid;
034import net.minecraft.item.Item;
035import net.minecraft.item.ItemStack;
036import net.minecraft.nbt.CompoundTag;
037import net.minecraft.server.MinecraftServer;
038import net.minecraft.tag.BlockTags;
039import net.minecraft.tag.EntityTypeTags;
040import net.minecraft.tag.ItemTags;
041import net.minecraft.tag.Tag;
042import net.minecraft.tag.TagGroup;
043import net.minecraft.text.Text;
044import net.minecraft.util.Identifier;
045import net.minecraft.util.registry.Registry;
046import net.minecraft.world.dimension.DimensionType;
047import org.apache.logging.log4j.LogManager;
048import org.apache.logging.log4j.Logger;
049import org.checkerframework.checker.nullness.qual.Nullable;
050import org.spongepowered.configurate.ConfigurationNode;
051import org.spongepowered.configurate.extra.dfu.v4.ConfigurateOps;
052import org.spongepowered.configurate.extra.dfu.v4.DfuSerializers;
053import org.spongepowered.configurate.serialize.TypeSerializer;
054import org.spongepowered.configurate.serialize.TypeSerializerCollection;
055
056import java.lang.reflect.Field;
057import java.lang.reflect.Modifier;
058import java.lang.reflect.ParameterizedType;
059import java.lang.reflect.Type;
060import java.util.AbstractMap;
061import java.util.Map;
062import java.util.Set;
063import java.util.function.Supplier;
064
065/**
066 * Access serializers for Minecraft types.
067 *
068 * <p>The {@link #collection()} provides an easily accessible collection of
069 * built-in type serializers, while other factory methods allow creating custom
070 * type serializers that interact with game serialization mechanisms.
071 *
072 * @since 1.2.0
073 */
074public final class MinecraftSerializers {
075
076    // impl note: initialization order is critical here to ensure we can test
077    // most parts of Confabricate without having to fully initialize Minecraft
078    // and use any of our Mixins
079
080    /**
081     * Registries that should not be added to a serializer collection.
082     */
083    private static final ImmutableSet<Registry<?>> SPECIAL_REGISTRIES =
084            ImmutableSet.of(Registry.CUSTOM_STAT, // Type of identifier
085                    Registry.FLUID,
086                    Registry.BLOCK,
087                    Registry.ITEM,
088                    Registry.ENTITY_TYPE
089            );
090
091    private static @LazyInit Set<Map.Entry<Type, TypeSerializer<?>>> KNOWN_REGISTRIES;
092
093    private static final TypeToken<Registry<?>> TYPE_REGISTRY_GENERIC = new TypeToken<Registry<?>>() {};
094    private static final Logger LOGGER = LogManager.getLogger();
095
096    private static @LazyInit TypeSerializerCollection MINECRAFT_COLLECTION;
097    private static @LazyInit ConfigurateOps DEFAULT_OPS;
098
099    private MinecraftSerializers() {}
100
101    static DynamicOps<ConfigurationNode> opsFor(final ConfigurationNode node) {
102        if (node.options().serializers().equals(MINECRAFT_COLLECTION)) {
103            final @Nullable ConfigurateOps ops = DEFAULT_OPS;
104            if (ops == null) {
105                return DEFAULT_OPS = ConfigurateOps.builder()
106                                .factoryFromSerializers(collection())
107                                .readWriteProtection(ConfigurateOps.Protection.NONE)
108                                .build();
109            }
110            return DEFAULT_OPS;
111        } else {
112            return ConfigurateOps.builder()
113                    .factoryFromNode(node)
114                    .readWriteProtection(ConfigurateOps.Protection.NONE)
115                    .build();
116        }
117    }
118
119    /**
120     * Create a new serializer wrapping the provided {@link Codec}.
121     *
122     * @param codec codec to use for the serialization operation
123     * @param <V> value type
124     * @return a new serializer
125     * @see DfuSerializers#serializer(Codec) for more information
126     * @since 2.0.0
127     */
128    public static <V> TypeSerializer<V> serializer(final Codec<V> codec) {
129        return DfuSerializers.serializer(codec);
130    }
131
132    /**
133     * Create a new codec that uses the default type serializer collection
134     * to serialize an object of the provided type.
135     *
136     * @param type token representing a value type
137     * @param <S> value type
138     * @return a codec for the type, or null if an appropriate
139     *      {@link TypeSerializer} could not be found.
140     * @see DfuSerializers#codec(TypeToken) for more information
141     * @since 2.0.0
142     */
143    public static <S> @Nullable Codec<S> codec(final TypeToken<S> type) {
144        return codec(type, collection());
145    }
146
147    /**
148     * Create a new codec based on a Configurate {@link TypeSerializer}.
149     *
150     * @param type type to serialize
151     * @param collection source for values
152     * @param <V> value type
153     * @return a codec, or null if an appropriate {@link TypeSerializer}
154     *      could not be found for the TypeToken.
155     * @see DfuSerializers#codec(TypeToken, TypeSerializerCollection)
156     * @since 2.0.0
157     */
158    public static <V> @Nullable Codec<V> codec(final TypeToken<V> type, final TypeSerializerCollection collection) {
159        return DfuSerializers.codec(type, collection);
160    }
161
162    /**
163     * Create a {@link TypeSerializer} than can interpret values in the
164     * provided registry.
165     *
166     * @param registry the registry
167     * @param <T> the type registered by the registry
168     * @return a serializer for the registry
169     * @since 1.2.0
170     */
171    public static <T> TypeSerializer<T> forRegistry(final Registry<T> registry) {
172        return new RegistrySerializer<>(registry);
173    }
174
175    /**
176     * The default collection of game serializers.
177     *
178     * <p>While the collection is mutable, modifying it is discouraged in favor
179     * of working with a new child, created with
180     * {@link TypeSerializerCollection#childBuilder()} ()}. Collections of serializers
181     * will become immutable in Configurate 4.0
182     *
183     * @return minecraft serializers
184     * @see #populate(TypeSerializerCollection.Builder) for information about
185     *      which serializers this collection will include
186     * @since 1.2.0
187     */
188    public static TypeSerializerCollection collection() {
189        TypeSerializerCollection collection = MINECRAFT_COLLECTION;
190        if (collection == null) {
191            collection = MINECRAFT_COLLECTION = populate(TypeSerializerCollection.defaults().childBuilder()).build();
192        }
193        return collection;
194    }
195
196    /**
197     * Check if a collection is our populated collection without attempting to
198     * initialize serializers.
199     *
200     * <p>This helps to integrate with Confabricate in test environments.
201     *
202     * @param collection collection to test
203     * @return if tested collection is the confabricate default collection
204     * @since 1.2.0
205     */
206    public static boolean isCommonCollection(final TypeSerializerCollection collection) {
207        return requireNonNull(collection, "collection").equals(MINECRAFT_COLLECTION);
208    }
209
210    private static boolean shouldRegister(final Registry<?> registry) {
211        // Don't register root registry -- its key can't be looked up :(
212        return !SPECIAL_REGISTRIES.contains(registry);
213    }
214
215    /**
216     * Register Minecraft {@link TypeSerializer}s with the provided collection.
217     *
218     * <p>This will add serializers for: <ul>
219     *     <li>{@link net.minecraft.util.Identifier Identifiers}</li>
220     *     <li>{@link net.minecraft.text.Text} (as a string)</li>
221     *     <li>Any elements in vanilla {@link Registry registries}</li>
222     *     <li>{@link Tag} of a combination of identifiers and
223     *          tags for blocks, items, fluids, and entity types</li>
224     *     <li>{@link ItemStack}</li>
225     *     <li>{@link CompoundTag} instances</li>
226     * </ul>
227     *
228     * @param collection to populate
229     * @return provided collection
230     * @since 1.2.0
231     */
232    public static TypeSerializerCollection.Builder populate(final TypeSerializerCollection.Builder collection) {
233        collection.registerExact(Identifier.class, IdentifierSerializer.INSTANCE)
234                .register(Text.class, TextSerializer.INSTANCE);
235
236        for (Map.Entry<Type, TypeSerializer<?>> registry : knownRegistries()) {
237            registerRegistry(collection, registry.getKey(), registry.getValue());
238        }
239
240        collection.register(ItemStack.class, serializer(ItemStack.CODEC));
241        collection.register(CompoundTag.class, serializer(CompoundTag.CODEC));
242
243        // All registries here should be in SPECIAL_REGISTRIES
244        populateTaggedRegistry(collection, TypeToken.get(Fluid.class), Registry.FLUID, FluidTagsAccessor.getRequiredTags()::getGroup);
245        populateTaggedRegistry(collection, TypeToken.get(Block.class), Registry.BLOCK, BlockTags::getTagGroup);
246        populateTaggedRegistry(collection, new TypeToken<EntityType<?>>() {}, Registry.ENTITY_TYPE, EntityTypeTags::getTagGroup);
247        populateTaggedRegistry(collection, TypeToken.get(Item.class), Registry.ITEM, ItemTags::getTagGroup);
248
249        return collection;
250    }
251
252    // Type serializers that use the server resource manager
253    private static TypeSerializerCollection.Builder populateServer(final MinecraftServer server, final TypeSerializerCollection.Builder collection) {
254        registerRegistry(collection, DimensionType.class, forRegistry(server.getRegistryManager().getDimensionTypes()));
255        return collection;
256    }
257
258    // Type serializers that source registry + tag information from the client
259    @Environment(EnvType.CLIENT)
260    private static TypeSerializerCollection.Builder populateClient(final MinecraftClient client, final TypeSerializerCollection.Builder collection) {
261        return collection;
262    }
263
264    /**
265     * Lazily initialize our set of vanilla registries.
266     *
267     * <p>This is moderately expensive due to having to reflectively analyze
268     * the fields in the Registry class, so we cache the created serializers
269     * after the first initialization.
270     *
271     * @return collection of built-in registries
272     */
273    private static Set<Map.Entry<Type, TypeSerializer<?>>> knownRegistries() {
274        Set<Map.Entry<Type, TypeSerializer<?>>> registries = KNOWN_REGISTRIES;
275        if (registries == null) {
276            final ImmutableSet.Builder<Map.Entry<Type, TypeSerializer<?>>> accumulator = ImmutableSet.builder();
277            for (Field registryField : Registry.class.getFields()) {
278                // only look at public static fields (excludes the ROOT registry)
279                if ((registryField.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC)) != (Modifier.STATIC | Modifier.PUBLIC)) {
280                    continue;
281                }
282
283                final Type fieldType = registryField.getGenericType();
284                if (!GenericTypeReflector.isSuperType(TYPE_REGISTRY_GENERIC.getType(), fieldType)) { // if not a registry (keys)
285                    continue;
286                }
287
288                final Type elementType = ((ParameterizedType) fieldType).getActualTypeArguments()[0];
289                try {
290                    final Registry<?> registry = (Registry<?>) registryField.get(null);
291                    if (shouldRegister(registry)) {
292                        accumulator.add(new AbstractMap.SimpleImmutableEntry<>(elementType, forRegistry(registry)));
293                        LOGGER.debug("Created serializer for Minecraft registry {} with element type {}", registry, elementType);
294                    }
295                } catch (final IllegalAccessException e) {
296                    LOGGER.error("Unable to create serializer for registry of type {} due to access error", elementType, e);
297                }
298            }
299            registries = KNOWN_REGISTRIES = accumulator.build();
300        }
301        return registries;
302    }
303
304    // Limit scope of warning suppression for reflective registry initialization
305    @SuppressWarnings({"unchecked", "rawtypes"})
306    private static void registerRegistry(final TypeSerializerCollection.Builder collection, final Type type, final TypeSerializer<?> registry) {
307        collection.registerExact(TypeToken.get(type), (TypeSerializer) registry);
308    }
309
310    /**
311     * Add a serializer for the registry and its {@link Tag}.
312     *
313     * @param collection the collection to serialize
314     * @param token generic element type of the registry
315     * @param registry registry containing values
316     * @param tagRegistry tag container for values in the registry
317     * @param <T> element type
318     */
319    @SuppressWarnings("unchecked")
320    private static <T> void populateTaggedRegistry(final TypeSerializerCollection.Builder collection, final TypeToken<T> token,
321            final Registry<T> registry,
322            final Supplier<TagGroup<T>> tagRegistry) {
323        final Type tagType = TypeFactory.parameterizedClass(Tag.class, token.getType());
324
325        collection.registerExact((TypeToken<Tag<T>>) TypeToken.get(tagType), new TagSerializer<>(registry, tagRegistry));
326        collection.registerExact(token, new RegistrySerializer<>(registry));
327    }
328
329}