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