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}