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}