/* * Copyright (c) Forge Development LLC and contributors * SPDX-License-Identifier: LGPL-2.1-only */ package net.minecraftforge.registries; import java.lang.annotation.ElementType; import java.lang.reflect.Field; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.forgespi.language.ModFileScanData; import com.google.common.collect.Maps; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; /** * Internal registry for tracking {@link ObjectHolder} references */ public class ObjectHolderRegistry { /** * Exposed to allow modders to register their own notification handlers. * This runnable will be called after a registry snapshot has been injected and finalized. * The internal list is backed by a HashSet so it is HIGHLY recommended you implement a proper equals * and hashCode function to de-duplicate callers here. * The default @ObjectHolder implementation uses the hashCode/equals for the field the annotation is on. * * @deprecated Use {{@link #addHandler(ResourceLocation, Runnable)} if you are only targeting a single registry */ public static synchronized void addHandler(Consumer> ref) { objectHolders.add(ref); } /** * Registers a handler that is run after the named registry has been injected and finalized. * Handlers are kept in a HashSet per registry, so the same instance is only ever registered once. */ public static synchronized void addHandler(ResourceLocation registry, Runnable ref) { namedHandlers.computeIfAbsent(registry, id -> new HashSet<>()).add(ref); } /** * Removed the specified handler from the notification list. * * The internal list is backed by a hash set, and so proper hashCode and equals operations are required for success. * * The default @ObjectHolder implementation uses the hashCode/equals for the field the annotation is on. * * @return true if handler was matched and removed. */ public static synchronized boolean removeHandler(Consumer> ref) { return objectHolders.remove(ref); } /** * Removes a handler registered with {@link #addHandler(ResourceLocation, Runnable)}. * * @return true if handler was matched and removed. */ public static synchronized boolean removeHandler(ResourceLocation registry, Runnable ref) { var handlers = namedHandlers.get(registry); return handlers != null && handlers.remove(ref); } /** * Registers a check that is run once, before the handlers of the next holder pass. * Used to report holders that point at a registry that does not exist. */ public static synchronized void addValidator(Runnable ref) { validators.add(ref); } //============================================================== // Everything below is internal, do not use. //============================================================== private static final Logger LOGGER = LogManager.getLogger(); private static final Set>> objectHolders = new HashSet<>(); // Order is not guaranteed, the old implementation kept every handler in one HashSet. private static final Map> namedHandlers = new HashMap<>(); private static final List validators = new ArrayList<>(); private static final Type OBJECT_HOLDER = Type.getType(ObjectHolder.class); private static final Type MOD = Type.getType(Mod.class); // Hardcoded list of vanilla classes that should have object holders for each field of the given registry type. // IMPORTANT: Updates to this collection must be reflected in ObjectHolderDefinalize. Duplicated cuz classloaders, yay! // Classnames are validated below. private static final List VANILLA_OBJECT_HOLDERS = List.of( new VanillaObjectHolderData("net.minecraft.world.level.block.Blocks", "block", "net.minecraft.world.level.block.Block"), new VanillaObjectHolderData("net.minecraft.world.item.Items", "item", "net.minecraft.world.item.Item"), new VanillaObjectHolderData("net.minecraft.world.item.enchantment.Enchantments", "enchantment", "net.minecraft.world.item.enchantment.Enchantment"), new VanillaObjectHolderData("net.minecraft.world.effect.MobEffects", "mob_effect", "net.minecraft.world.effect.MobEffect"), new VanillaObjectHolderData("net.minecraft.core.particles.ParticleTypes", "particle_type", "net.minecraft.core.particles.ParticleType"), new VanillaObjectHolderData("net.minecraft.sounds.SoundEvents", "sound_event", "net.minecraft.sounds.SoundEvent") ); public static void findObjectHolders() { LOGGER.debug(ForgeRegistry.REGISTRIES,"Processing ObjectHolder annotations"); final HashMap classModIds = new HashMap<>(); final List annotations = new ArrayList<>(); for (var data : ModList.get().getAllScanData()) { for (var annotation : data.getAnnotations()) { if (OBJECT_HOLDER.equals(annotation.annotationType())) { annotations.add(annotation); } // Gather all @Mod classes so that @ObjectHolder's in those classes don't need to specify the mod id; modder convenience else if (MOD.equals(annotation.annotationType())) { classModIds.put(annotation.clazz(), (String)annotation.annotationData().get("value")); } } } // this is never actually true as we have an annotation in ConditionalRecipe, but also we want to always run the vanilla holders /* if (annotations.isEmpty()) { return; // No object holders found, skip the rest of the processing, } */ Map> classCache = Maps.newHashMap(); // Validate all the vanilla class-level object holders then scan those first for (var data : VANILLA_OBJECT_HOLDERS) { try { Class holderClass = Class.forName(data.holderClass(), true, ObjectHolderRegistry.class.getClassLoader()); Class registryClass = Class.forName(data.registryType(), true, ObjectHolderRegistry.class.getClassLoader()); Type holderType = Type.getType(holderClass); classCache.put(holderType, holderClass); scanTarget(classModIds, classCache, holderType, null, registryClass, data.registryName(), "minecraft", true, true); } catch (ClassNotFoundException e) { throw new RuntimeException("Vanilla class not found, should not be possible", e); } } // Scan actual fields annotated with @ObjectHolder second for (var data : annotations) { if (data.targetType() == ElementType.FIELD) { var registry = (String)data.annotationData().get("registryName"); var name = (String)data.annotationData().get("value"); scanTarget(classModIds, classCache, data.clazz(), data.memberName(), null, registry, name, false, false); } // This is where we WOULD scan for class targets, but Shrimp broke that in 1.19 and so I don't want to re-add a feature that apparently nobody used. } int count = objectHolders.size(); for (var set : namedHandlers.values()) count += set.size(); LOGGER.debug(ForgeRegistry.REGISTRIES, "Found {} ObjectHolder annotations", count); } private static void scanTarget(Map classModIds, Map> classCache, Type type, @Nullable String annotationTarget, @Nullable Class registryClass, String registryName, String value, boolean isClass, boolean extractFromValue) { Class clazz; if (classCache.containsKey(type)) { clazz = classCache.get(type); } else { try { clazz = Class.forName(type.getClassName(), extractFromValue, ObjectHolderRegistry.class.getClassLoader()); classCache.put(type, clazz); } catch (ClassNotFoundException ex) { // unpossible? throw new RuntimeException(ex); } } ResourceLocation regName = ResourceLocation.parse(registryName); if (isClass) { scanClassForFields(classModIds, type, regName, registryClass, value, clazz, extractFromValue); } else { if (value.indexOf(':') == -1) { String prefix = classModIds.get(type); if (prefix == null) { LOGGER.warn(ForgeRegistry.REGISTRIES,"Found an unqualified ObjectHolder annotation ({}) without a modid context at {}.{}, ignoring", value, type, annotationTarget); throw new IllegalStateException("Unqualified reference to ObjectHolder"); } value = prefix + ':' + value; } try { Field f = clazz.getDeclaredField(annotationTarget); ObjectHolderRef ref = ObjectHolderRef.create(regName, f, value, extractFromValue); if (ref != null) addHandler(regName, ref); } catch (NoSuchFieldException ex) { // unpossible? throw new RuntimeException(ex); } } } private static void scanClassForFields(Map classModIds, Type targetClass, ResourceLocation registryName, Class registryClass, String value, Class clazz, boolean extractFromExistingValues) { classModIds.put(targetClass, value); final int flags = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC; for (Field f : clazz.getFields()) { if (((f.getModifiers() & flags) != flags) || f.isAnnotationPresent(ObjectHolder.class) || !registryClass.isAssignableFrom(f.getType())) continue; ObjectHolderRef ref = ObjectHolderRef.create(registryName, f, value + ':' + f.getName().toLowerCase(Locale.ENGLISH), extractFromExistingValues); if (ref != null) addHandler(registryName, ref); } } private static ResourceLocation getRegistryName(Map classRegistryNames, @Nullable String registryName, Type targetClass, Object declaration) { if (registryName != null) return new ResourceLocation(registryName); if (classRegistryNames.containsKey(targetClass)) return classRegistryNames.get(targetClass); throw new IllegalStateException("No registry name was declared for " + declaration); } private static final Predicate ALWAYS_TRUE = name -> true; public static void applyObjectHolders() { try { LOGGER.debug(ForgeRegistry.REGISTRIES, "Applying holder lookups"); var exceptions = new ArrayList(); validate(exceptions); // Call named registries for (var handlers : namedHandlers.values()) apply(handlers, exceptions); // Call legacy for backward compatibility if (!objectHolders.isEmpty()) apply(ALWAYS_TRUE, objectHolders, exceptions); throwIfNeeded(exceptions); LOGGER.debug(ForgeRegistry.REGISTRIES, "Holder lookups applied"); } catch (RuntimeException e) { // It is more important that the calling contexts continue without exception to prevent further cascading errors LOGGER.error("", e); } } /** * @deprecated use {@link #applyObjectHolders(ResourceLocation)} */ public static void applyObjectHolders(Predicate filter) { LOGGER.debug(ForgeRegistry.REGISTRIES, "Apply filtered holder lookups"); var exceptions = new ArrayList(); validate(exceptions); // call new registries if needed for (var entry : namedHandlers.entrySet()) { if (filter.test(entry.getKey())) { apply(entry.getValue(), exceptions); } } // Call legacy if (!objectHolders.isEmpty()) apply(filter, objectHolders, exceptions); throwIfNeeded(exceptions); LOGGER.debug(ForgeRegistry.REGISTRIES, "Holder lookups applied"); } /** Runs the handlers of one registry. */ public static void applyObjectHolders(ResourceLocation registry) { var exceptions = new ArrayList(); validate(exceptions); var handlers = namedHandlers.get(registry); if (handlers != null) apply(handlers, exceptions); if (!objectHolders.isEmpty()) apply(registry::equals, objectHolders, exceptions); throwIfNeeded(exceptions); } // Each validator runs once, at the first holder pass after it was registered. private static void validate(List exceptions) { List pending; synchronized (ObjectHolderRegistry.class) { if (validators.isEmpty()) return; pending = new ArrayList<>(validators); validators.clear(); } for (var validator : pending) { try { validator.run(); } catch (Exception e) { exceptions.add(e); } } } private static void apply(Set handlers, List exceptions) { for (var handler : handlers) { try { handler.run(); } catch (Exception e) { exceptions.add(e); } } } private static void apply(Predicate filter, Set>> handlers, List exceptions) { for (var handler : handlers) { try { handler.accept(filter); } catch (Exception e) { exceptions.add(e); } } } private static void throwIfNeeded(List exceptions) { if (exceptions.isEmpty()) return; var aggregate = new RuntimeException("Failed to apply some object holders, see suppressed exceptions for details"); for (var e : exceptions) aggregate.addSuppressed(e); throw aggregate; } private record VanillaObjectHolderData(String holderClass, String registryName, String registryType) {} }