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 com.google.common.collect.ImmutableSet;
019import com.google.errorprone.annotations.concurrent.LazyInit;
020import com.mojang.logging.LogUtils;
021import com.mojang.serialization.Codec;
022import com.mojang.serialization.DynamicOps;
023import io.leangen.geantyref.GenericTypeReflector;
024import io.leangen.geantyref.TypeFactory;
025import io.leangen.geantyref.TypeToken;
026import java.lang.reflect.Field;
027import java.lang.reflect.Modifier;
028import java.lang.reflect.ParameterizedType;
029import java.lang.reflect.Type;
030import java.lang.reflect.WildcardType;
031import java.util.Map;
032import java.util.Set;
033import java.util.function.Supplier;
034import net.minecraft.core.Holder;
035import net.minecraft.core.HolderSet;
036import net.minecraft.core.Registry;
037import net.minecraft.core.RegistryAccess;
038import net.minecraft.nbt.CompoundTag;
039import net.minecraft.network.chat.Component;
040import net.minecraft.resources.ResourceKey;
041import net.minecraft.resources.ResourceLocation;
042import net.minecraft.world.item.ItemStack;
043import org.checkerframework.checker.nullness.qual.Nullable;
044import org.slf4j.Logger;
045import org.spongepowered.configurate.ConfigurationNode;
046import org.spongepowered.configurate.extra.dfu.v4.ConfigurateOps;
047import org.spongepowered.configurate.extra.dfu.v4.DfuSerializers;
048import org.spongepowered.configurate.gson.GsonConfigurationLoader;
049import org.spongepowered.configurate.serialize.TypeSerializer;
050import org.spongepowered.configurate.serialize.TypeSerializerCollection;
051
052import static java.util.Objects.requireNonNull;
053
054/**
055 * Access serializers for Minecraft types.
056 *
057 * <p>The {@link #collection()} provides an easily accessible collection of
058 * built-in type serializers, while other factory methods allow creating custom
059 * type serializers that interact with game serialization mechanisms.
060 *
061 * @since 1.2.0
062 */
063public final class MinecraftSerializers {
064
065    private static final Logger LOGGER = LogUtils.getLogger();
066
067    // impl note: initialization order is critical here to ensure we can test
068    // most parts of Confabricate without having to fully initialize Minecraft
069    // and use any of our Mixins
070
071    /**
072     * Registries that should not be added to a serializer collection.
073     */
074    private static final Set<ResourceKey<? extends Registry<?>>> SPECIAL_REGISTRIES = Set.of(Registry.CUSTOM_STAT_REGISTRY); // Type of RL
075    private static final TypeToken<ResourceKey<? extends Registry<?>>> TYPE_RESOURCE_KEY_GENERIC
076        = new TypeToken<ResourceKey<? extends Registry<?>>>() {};
077
078    private static @LazyInit Set<Map.Entry<Type, ResourceKey<? extends Registry<?>>>> KNOWN_REGISTRIES;
079    private static @LazyInit TypeSerializerCollection MINECRAFT_COLLECTION;
080    private static @LazyInit ConfigurateOps DEFAULT_OPS;
081
082    private MinecraftSerializers() {}
083
084    static DynamicOps<ConfigurationNode> opsFor(final ConfigurationNode node) {
085        if (node.options().serializers().equals(MINECRAFT_COLLECTION)) {
086            final @Nullable ConfigurateOps ops = DEFAULT_OPS;
087            if (ops == null) {
088                return DEFAULT_OPS = ConfigurateOps.builder()
089                                .factoryFromSerializers(collection())
090                                .readWriteProtection(ConfigurateOps.Protection.NONE)
091                                .build();
092            }
093            return DEFAULT_OPS;
094        } else {
095            return ConfigurateOps.builder()
096                    .factoryFromNode(node)
097                    .readWriteProtection(ConfigurateOps.Protection.NONE)
098                    .build();
099        }
100    }
101
102    /**
103     * Create a new serializer wrapping the provided {@link Codec}.
104     *
105     * @param codec codec to use for the serialization operation
106     * @param <V> value type
107     * @return a new serializer
108     * @see DfuSerializers#serializer(Codec) for more information
109     * @since 2.0.0
110     */
111    public static <V> TypeSerializer<V> serializer(final Codec<V> codec) {
112        return DfuSerializers.serializer(codec);
113    }
114
115    /**
116     * Create a new codec that uses the default type serializer collection
117     * to serialize an object of the provided type.
118     *
119     * @param type token representing a value type
120     * @param <S> value type
121     * @return a codec for the type, or null if an appropriate
122     *      {@link TypeSerializer} could not be found.
123     * @see DfuSerializers#codec(TypeToken) for more information
124     * @since 2.0.0
125     */
126    public static <S> @Nullable Codec<S> codec(final TypeToken<S> type) {
127        return codec(type, collection());
128    }
129
130    /**
131     * Create a new codec based on a Configurate {@link TypeSerializer}.
132     *
133     * @param type type to serialize
134     * @param collection source for values
135     * @param <V> value type
136     * @return a codec, or null if an appropriate {@link TypeSerializer}
137     *      could not be found for the TypeToken.
138     * @see DfuSerializers#codec(TypeToken, TypeSerializerCollection)
139     * @since 2.0.0
140     */
141    public static <V> @Nullable Codec<V> codec(final TypeToken<V> type, final TypeSerializerCollection collection) {
142        return DfuSerializers.codec(type, collection);
143    }
144
145    /**
146     * Populate a type serializer collection with all applicable serializers
147     * for values in a registry.
148     *
149     * <p>This includes a serializer for the literal values, a serializer
150     * for {@link Holder Holders} of values, and a serializer for
151     * {@link HolderSet HolderSets} (tags) of values.</p>
152     *
153     * @param builder the builder to populate
154     * @param entryType the type contained in the registry
155     * @param access registry access to read the registry contents from
156     * @param registry the registry to query
157     * @param <T> the type registered by the registry
158     * @since 3.0.0
159     */
160    @SuppressWarnings("unchecked")
161    public static <T> void populateRegistry(
162        final TypeSerializerCollection.Builder builder,
163        final TypeToken<T> entryType,
164        final RegistryAccess access,
165        final ResourceKey<? extends Registry<T>> registry
166    ) {
167        // serializer
168        // holder serializer
169        builder.registerExact(entryType, new RegistrySerializer<>(access, registry));
170        builder.registerExact(
171            (TypeToken<Holder<T>>) TypeToken.get(TypeFactory.parameterizedClass(Holder.class, entryType.getType())),
172            new HolderSerializer<>(access, registry)
173        );
174        builder.registerExact(
175            (TypeToken<HolderSet<T>>) TypeToken.get(TypeFactory.parameterizedClass(HolderSet.class, entryType.getType())),
176            new HolderSetSerializer<>(access, registry)
177        );
178    }
179
180    /**
181     * The default collection of game serializers.
182     *
183     * <p>While the collection is mutable, modifying it is discouraged in favor
184     * of working with a new child, created with
185     * {@link TypeSerializerCollection#childBuilder()} ()}. Collections of serializers
186     * will become immutable in Configurate 4.0
187     *
188     * @return minecraft serializers
189     * @see #populate(TypeSerializerCollection.Builder) for information about
190     *      which serializers this collection will include
191     * @since 1.2.0
192     */
193    public static TypeSerializerCollection collection() {
194        TypeSerializerCollection collection = MINECRAFT_COLLECTION;
195        if (collection == null) {
196            collection = MINECRAFT_COLLECTION = populate(TypeSerializerCollection.defaults().childBuilder()).build();
197        }
198        return collection;
199    }
200
201    /**
202     * Check if a collection is our populated collection without attempting to
203     * initialize serializers.
204     *
205     * <p>This helps to integrate with Confabricate in test environments.
206     *
207     * @param collection collection to test
208     * @return if tested collection is the confabricate default collection
209     * @since 1.2.0
210     */
211    public static boolean isCommonCollection(final TypeSerializerCollection collection) {
212        return requireNonNull(collection, "collection").equals(MINECRAFT_COLLECTION);
213    }
214
215    private static boolean shouldRegister(final ResourceKey<? extends Registry<?>> registry) {
216        // Don't register root registry -- its key can't be looked up :(
217        return !SPECIAL_REGISTRIES.contains(registry);
218    }
219
220    /**
221     * Register Minecraft {@link TypeSerializer}s with the provided collection.
222     *
223     * <p>This will add serializers for: <ul>
224     *     <li>{@link ResourceLocation resource locations}</li>
225     *     <li>{@link net.minecraft.network.chat.Component} (as a string)</li>
226     *     <li>Any individual elements in vanilla {@link Registry registries},
227     *         as raw values or {@link Holder}</li>
228     *     <li>{@link HolderSet} of a combination of identifiers and
229     *          tags for any taggable registry</li>
230     *     <li>{@link ItemStack}</li>
231     *     <li>{@link CompoundTag} instances</li>
232     * </ul>
233     *
234     * @param collection to populate
235     * @return provided collection
236     * @since 1.2.0
237     */
238    public static TypeSerializerCollection.Builder populate(final TypeSerializerCollection.Builder collection) {
239        collection.registerExact(ResourceLocation.class, ResourceLocationSerializer.INSTANCE)
240                .register(Component.class, ComponentSerializer.INSTANCE);
241
242        for (final Map.Entry<Type, ResourceKey<? extends Registry<?>>> registry : knownRegistries()) {
243            registerRegistry(
244                collection,
245                registry.getKey(),
246                RegistryAccess.BUILTIN,
247                registry.getValue()
248            );
249        }
250
251        collection.register(ItemStack.class, serializer(ItemStack.CODEC));
252        collection.register(CompoundTag.class, serializer(CompoundTag.CODEC));
253
254        // All registries here should be in SPECIAL_REGISTRIES
255
256        collection.registerAll(GsonConfigurationLoader.gsonSerializers());
257
258        return collection;
259    }
260
261    /**
262     * Lazily initialize our set of vanilla registries.
263     *
264     * <p>This is moderately expensive due to having to reflectively analyze
265     * the fields in the Registry class, so we cache the created serializers
266     * after the first initialization.
267     *
268     * @return collection of built-in registries
269     */
270    @SuppressWarnings("unchecked")
271    private static Set<Map.Entry<Type, ResourceKey<? extends Registry<?>>>> knownRegistries() {
272        Set<Map.Entry<Type, ResourceKey<? extends Registry<?>>>> registries = KNOWN_REGISTRIES;
273        if (registries == null) {
274            final ImmutableSet.Builder<Map.Entry<Type, ResourceKey<? extends Registry<?>>>> accumulator = ImmutableSet.builder();
275            for (final Field registryField : Registry.class.getFields()) {
276                // only look at public static fields (excludes the ROOT registry)
277                if ((registryField.getModifiers() & (Modifier.STATIC | Modifier.PUBLIC)) != (Modifier.STATIC | Modifier.PUBLIC)) {
278                    continue;
279                }
280
281                final Type fieldType = registryField.getGenericType();
282                if (!GenericTypeReflector.isSuperType(TYPE_RESOURCE_KEY_GENERIC.getType(), fieldType)) { // if not a registry
283                    continue;
284                }
285
286                final ResourceKey<? extends Registry<?>> registry;
287                try {
288                    registry = (ResourceKey<? extends Registry<?>>) registryField.get(null);
289                } catch (final IllegalAccessException e) {
290                    LOGGER.error("Unable to create serializer for registry of type {} due to access error", fieldType, e);
291                    continue;
292                }
293
294                try {
295                    if (shouldRegister(registry)) {
296                        final Type registryKeyType = ((ParameterizedType) fieldType).getActualTypeArguments()[0];
297                        final Type registryType =
298                            registryKeyType instanceof WildcardType ? ((WildcardType) registryKeyType).getUpperBounds()[0]
299                            : registryKeyType;
300                        final Type elementType = ((ParameterizedType) registryType).getActualTypeArguments()[0];
301                        accumulator.add(Map.entry(elementType, registry));
302                        LOGGER.debug("Created serializer for Minecraft registry {} with element type {}", registry, elementType);
303                    }
304                } catch (final Exception ex) {
305                    LOGGER.error("Error attempting to discover registry entry type for {} from field type {}", registry, fieldType, ex);
306                }
307            }
308            registries = KNOWN_REGISTRIES = accumulator.build();
309        }
310        return registries;
311    }
312
313    // Limit scope of warning suppression for reflective registry initialization
314    @SuppressWarnings({"unchecked", "rawtypes"})
315    private static void registerRegistry(
316        final TypeSerializerCollection.Builder collection,
317        final Type type,
318        final Supplier<? extends RegistryAccess> access,
319        final ResourceKey<? extends Registry<?>> registry
320    ) {
321        populateRegistry(
322            collection,
323            TypeToken.get(type),
324            access.get(), // todo: make things lazy?
325            (ResourceKey) registry
326        );
327    }
328
329}