diff --git a/patches/net/minecraft/server/ReloadableServerResources.java.patch b/patches/net/minecraft/server/ReloadableServerResources.java.patch index e21a3d9448..ef4eff8146 100644 --- a/patches/net/minecraft/server/ReloadableServerResources.java.patch +++ b/patches/net/minecraft/server/ReloadableServerResources.java.patch @@ -36,22 +36,28 @@ public static CompletableFuture loadResources( ResourceManager p_248588_, LayeredRegistryAccess p_335667_, -@@ -83,10 +_,24 @@ +@@ -83,10 +_,30 @@ ReloadableServerResources reloadableserverresources = new ReloadableServerResources( p_359514_.layers(), p_359514_.lookupWithUpdatedTags(), p_250212_, p_249301_, p_363739_, p_251126_ ); -+ List listeners = new java.util.ArrayList<>(reloadableserverresources.listeners()); -+ listeners.addAll(net.neoforged.neoforge.event.EventHooks.onResourceReload(reloadableserverresources, p_359514_.layers().compositeAccess())); -+ listeners.forEach(rl -> { -+ if (rl instanceof net.neoforged.neoforge.resource.ContextAwareReloadListener srl) srl.injectContext(reloadableserverresources.context, reloadableserverresources.registryLookup); -+ }); ++ ++ // Neo: Fire the AddServerReloadListenersEvent and use the resulting listeners instead of the vanilla listener list. ++ List listeners = net.neoforged.neoforge.event.EventHooks.onResourceReload(reloadableserverresources, p_359514_.layers().compositeAccess()); ++ ++ // Neo: Inject the ConditionContext and RegistryLookup to any resource listener that requests it. ++ for (PreparableReloadListener rl : listeners) { ++ if (rl instanceof net.neoforged.neoforge.resource.ContextAwareReloadListener carl) { ++ carl.injectContext(reloadableserverresources.context, reloadableserverresources.registryLookup); ++ } ++ } ++ return SimpleReloadInstance.create( - p_248588_, reloadableserverresources.listeners(), p_249136_, p_249601_, DATA_RELOAD_INITIAL_TASK, LOGGER.isDebugEnabled() + p_248588_, listeners, p_249136_, p_249601_, DATA_RELOAD_INITIAL_TASK, LOGGER.isDebugEnabled() ) .done() + .thenRun(() -> { -+ // Clear context after reload completes ++ // Neo: Clear context after reload completes + reloadableserverresources.context.clear(); + listeners.forEach(rl -> { + if (rl instanceof net.neoforged.neoforge.resource.ContextAwareReloadListener srl) { diff --git a/patches/net/minecraft/server/packs/resources/ReloadableResourceManager.java.patch b/patches/net/minecraft/server/packs/resources/ReloadableResourceManager.java.patch index ab858a6705..cc071968c2 100644 --- a/patches/net/minecraft/server/packs/resources/ReloadableResourceManager.java.patch +++ b/patches/net/minecraft/server/packs/resources/ReloadableResourceManager.java.patch @@ -1,13 +1,40 @@ --- a/net/minecraft/server/packs/resources/ReloadableResourceManager.java +++ b/net/minecraft/server/packs/resources/ReloadableResourceManager.java -@@ -73,4 +_,10 @@ +@@ -33,6 +_,12 @@ + this.resources.close(); + } + ++ /** ++ * @deprecated Neo: Use {@link net.neoforged.neoforge.client.event.AddClientReloadListenerEvent}. ++ * ++ * @throws UnsupportedOperationException if called after the event has been fired. ++ */ ++ @Deprecated + public void registerReloadListener(PreparableReloadListener p_10714_) { + this.listeners.add(p_10714_); + } +@@ -72,5 +_,24 @@ + @Override public Stream listPacks() { return this.resources.listPacks(); - } ++ } + -+ public void registerReloadListenerIfNotPresent(PreparableReloadListener listener) { -+ if (!this.listeners.contains(listener)) { -+ this.registerReloadListener(listener); -+ } ++ /** ++ * Neo: Expose the reload listeners so they can be passed to the event. ++ * ++ * @return The (immutable) list of reload listeners. ++ */ ++ public List getListeners() { ++ return this.listeners; + } ++ ++ /** ++ * Neo: Updates the {@link #listeners} with the sorted list from the event. ++ * ++ * @implNote The returned list is immutable, so after this method is called, {@link #registerReloadListener(PreparableReloadListener)} will throw. ++ */ ++ @org.jetbrains.annotations.ApiStatus.Internal ++ public void updateListenersFrom(net.neoforged.neoforge.event.SortedReloadListenerEvent event) { ++ this.listeners = net.neoforged.neoforge.resource.ReloadListenerSort.sort(event); + } } diff --git a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java index 7e497ecaf2..9d384bca46 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java @@ -134,6 +134,7 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.asm.enumextension.ExtensionInfo; import net.neoforged.neoforge.client.entity.animation.json.AnimationTypeManager; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; import net.neoforged.neoforge.client.event.AddSectionGeometryEvent; import net.neoforged.neoforge.client.event.CalculateDetachedCameraDistanceEvent; import net.neoforged.neoforge.client.event.CalculatePlayerTurnEvent; @@ -151,7 +152,6 @@ import net.neoforged.neoforge.client.event.InputEvent; import net.neoforged.neoforge.client.event.ModelEvent; import net.neoforged.neoforge.client.event.MovementInputUpdateEvent; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; import net.neoforged.neoforge.client.event.RegisterMaterialAtlasesEvent; @@ -975,7 +975,11 @@ public static void initClientHooks(Minecraft mc, ReloadableResourceManager resou GameTestHooks.registerGametests(); registerSpriteSourceTypes(); MenuScreens.init(); - ModLoader.postEvent(new RegisterClientReloadListenersEvent(resourceManager)); + + var rlEvent = new AddClientReloadListenersEvent(resourceManager); + ModLoader.postEvent(rlEvent); + resourceManager.updateListenersFrom(rlEvent); + ModLoader.postEvent(new EntityRenderersEvent.RegisterLayerDefinitions()); ModLoader.postEvent(new EntityRenderersEvent.RegisterRenderers()); ModLoader.postEvent(new RegisterRenderStateModifiersEvent()); diff --git a/src/main/java/net/neoforged/neoforge/client/ClientNeoForgeMod.java b/src/main/java/net/neoforged/neoforge/client/ClientNeoForgeMod.java index f783a342f5..5a434e6521 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientNeoForgeMod.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientNeoForgeMod.java @@ -27,9 +27,9 @@ import net.neoforged.fml.config.ModConfigs; import net.neoforged.neoforge.client.color.item.FluidContentsTint; import net.neoforged.neoforge.client.entity.animation.json.AnimationLoader; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; import net.neoforged.neoforge.client.event.ModelEvent; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; import net.neoforged.neoforge.client.event.RegisterItemModelsEvent; import net.neoforged.neoforge.client.event.RegisterNamedRenderTypesEvent; @@ -38,10 +38,12 @@ import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent; import net.neoforged.neoforge.client.gui.ConfigurationScreen; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import net.neoforged.neoforge.client.loading.ClientModLoader; import net.neoforged.neoforge.client.model.EmptyModel; import net.neoforged.neoforge.client.model.UnbakedCompositeModel; import net.neoforged.neoforge.client.model.item.DynamicFluidContainerModel; import net.neoforged.neoforge.client.model.obj.ObjLoader; +import net.neoforged.neoforge.client.resources.VanillaClientListeners; import net.neoforged.neoforge.client.textures.NamespacedDirectoryLister; import net.neoforged.neoforge.common.ModConfigSpec; import net.neoforged.neoforge.common.NeoForge; @@ -64,7 +66,9 @@ import net.neoforged.neoforge.common.data.internal.VanillaSoundDefinitionsProvider; import net.neoforged.neoforge.common.util.SelfTest; import net.neoforged.neoforge.data.event.GatherDataEvent; +import net.neoforged.neoforge.internal.BrandingControl; import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; +import net.neoforged.neoforge.resource.NeoForgeReloadListeners; import org.jetbrains.annotations.ApiStatus; @ApiStatus.Internal @@ -131,9 +135,16 @@ static void onRegisterModelLoaders(ModelEvent.RegisterLoaders event) { } @SubscribeEvent - static void onRegisterReloadListeners(RegisterClientReloadListenersEvent event) { - event.registerReloadListener(ObjLoader.INSTANCE); - event.registerReloadListener(AnimationLoader.INSTANCE); + static void onRegisterReloadListeners(AddClientReloadListenersEvent event) { + event.addListener(NeoForgeReloadListeners.CLIENT_MOD_LOADING, ClientModLoader::onResourceReload); + event.addListener(NeoForgeReloadListeners.BRANDING, BrandingControl.resourceManagerReloadListener()); + + // These run before vanilla reload listeners. + event.addDependency(NeoForgeReloadListeners.CLIENT_MOD_LOADING, NeoForgeReloadListeners.BRANDING); + event.addDependency(NeoForgeReloadListeners.BRANDING, VanillaClientListeners.FIRST); + + event.addListener(NeoForgeReloadListeners.OBJ_LOADER, ObjLoader.INSTANCE); + event.addListener(NeoForgeReloadListeners.ENTITY_ANIMATIONS, AnimationLoader.INSTANCE); } @SubscribeEvent diff --git a/src/main/java/net/neoforged/neoforge/client/event/AddClientReloadListenersEvent.java b/src/main/java/net/neoforged/neoforge/client/event/AddClientReloadListenersEvent.java new file mode 100644 index 0000000000..60ab298548 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/event/AddClientReloadListenersEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.event; + +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ReloadableResourceManager; +import net.neoforged.fml.LogicalSide; +import net.neoforged.fml.event.IModBusEvent; +import net.neoforged.neoforge.client.resources.VanillaClientListeners; +import net.neoforged.neoforge.event.SortedReloadListenerEvent; +import org.jetbrains.annotations.ApiStatus; + +/** + * This event allows mods to register client-side reload listeners to the resource manager. + * This event is fired once during the construction of the {@link Minecraft} instance. + *

+ * This event is only fired on the {@linkplain LogicalSide#CLIENT logical client}. + * + * @see {@link AddServerReloadListenersEvent} for registering server-side reload listeners. + */ +public class AddClientReloadListenersEvent extends SortedReloadListenerEvent implements IModBusEvent { + @ApiStatus.Internal + public AddClientReloadListenersEvent(ReloadableResourceManager resourceManager) { + super(resourceManager.getListeners(), AddClientReloadListenersEvent::lookupName); + } + + private static ResourceLocation lookupName(PreparableReloadListener listener) { + ResourceLocation key = VanillaClientListeners.getNameForClass(listener.getClass()); + if (key == null) { + if (listener.getClass().getPackageName().startsWith("net.minecraft")) { + throw new IllegalArgumentException("A key for the reload listener " + listener + " was not provided in VanillaClientListeners!"); + } else { + throw new IllegalArgumentException("A non-vanilla reload listener " + listener + " was added via mixin before the AddClientReloadListenerEvent! Mod-added listeners must go through the event."); + } + } + return key; + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/event/RegisterClientReloadListenersEvent.java b/src/main/java/net/neoforged/neoforge/client/event/RegisterClientReloadListenersEvent.java deleted file mode 100644 index 19b2ae54e8..0000000000 --- a/src/main/java/net/neoforged/neoforge/client/event/RegisterClientReloadListenersEvent.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.client.event; - -import net.minecraft.client.Minecraft; -import net.minecraft.server.packs.resources.PreparableReloadListener; -import net.minecraft.server.packs.resources.ReloadableResourceManager; -import net.neoforged.bus.api.Event; -import net.neoforged.bus.api.ICancellableEvent; -import net.neoforged.fml.LogicalSide; -import net.neoforged.fml.event.IModBusEvent; -import net.neoforged.neoforge.event.AddReloadListenerEvent; -import org.jetbrains.annotations.ApiStatus; - -/** - * Fired to allow mods to register their reload listeners on the client-side resource manager. - * This event is fired once during the construction of the {@link Minecraft} instance. - * - *

For registering reload listeners on the server-side resource manager, see {@link AddReloadListenerEvent}.

- * - *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.

- * - *

This event is fired on the mod-specific event bus, only on the {@linkplain LogicalSide#CLIENT logical client}.

- */ -public class RegisterClientReloadListenersEvent extends Event implements IModBusEvent { - private final ReloadableResourceManager resourceManager; - - @ApiStatus.Internal - public RegisterClientReloadListenersEvent(ReloadableResourceManager resourceManager) { - this.resourceManager = resourceManager; - } - - /** - * Registers the given reload listener to the client-side resource manager. - * - * @param reloadListener the reload listener - */ - public void registerReloadListener(PreparableReloadListener reloadListener) { - resourceManager.registerReloadListener(reloadListener); - } -} diff --git a/src/main/java/net/neoforged/neoforge/client/loading/ClientModLoader.java b/src/main/java/net/neoforged/neoforge/client/loading/ClientModLoader.java index d8c4a35e61..39a17f1275 100644 --- a/src/main/java/net/neoforged/neoforge/client/loading/ClientModLoader.java +++ b/src/main/java/net/neoforged/neoforge/client/loading/ClientModLoader.java @@ -28,7 +28,6 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.NeoForgeConfig; import net.neoforged.neoforge.common.util.LogicalSidedProvider; -import net.neoforged.neoforge.internal.BrandingControl; import net.neoforged.neoforge.internal.CommonModLoader; import net.neoforged.neoforge.logging.CrashReportExtender; import net.neoforged.neoforge.resource.ResourcePackLoader; @@ -63,12 +62,15 @@ public static void begin(final Minecraft minecraft, final PackRepository default if (error == null) { ResourcePackLoader.populatePackRepository(defaultResourcePacks, PackType.CLIENT_RESOURCES, false); DataPackConfig.DEFAULT.addModPacks(ResourcePackLoader.getPackNames(PackType.SERVER_DATA)); - mcResourceManager.registerReloadListener(ClientModLoader::onResourceReload); - mcResourceManager.registerReloadListener(BrandingControl.resourceManagerReloadListener()); } } - private static CompletableFuture onResourceReload(final PreparableReloadListener.PreparationBarrier stage, final ResourceManager resourceManager, final Executor asyncExecutor, final Executor syncExecutor) { + /** + * This method can be bound as a method reference to {@link PreparableReloadListener}. + *

+ * It is used as the entrypoint for client mod loading, which starts when {@link Minecraft} triggers the first resource reload. + */ + public static CompletableFuture onResourceReload(final PreparableReloadListener.PreparationBarrier stage, final ResourceManager resourceManager, final Executor asyncExecutor, final Executor syncExecutor) { return CompletableFuture.runAsync(() -> startModLoading(syncExecutor, asyncExecutor), ModWorkManager.parallelExecutor()) .thenCompose(stage::wait) .thenRunAsync(() -> finishModLoading(syncExecutor, asyncExecutor), ModWorkManager.parallelExecutor()); diff --git a/src/main/java/net/neoforged/neoforge/client/model/UnbakedModelLoader.java b/src/main/java/net/neoforged/neoforge/client/model/UnbakedModelLoader.java index 0f518f18f8..2b4a7c3118 100644 --- a/src/main/java/net/neoforged/neoforge/client/model/UnbakedModelLoader.java +++ b/src/main/java/net/neoforged/neoforge/client/model/UnbakedModelLoader.java @@ -11,17 +11,17 @@ import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; import net.neoforged.neoforge.client.event.ModelEvent; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; /** * A loader for custom {@linkplain UnbakedModel unbaked models}. *

* If you do any caching, you should implement {@link ResourceManagerReloadListener} and register it with - * {@link RegisterClientReloadListenersEvent}. + * {@link AddClientReloadListenersEvent}. * * @see ModelEvent.RegisterLoaders - * @see RegisterClientReloadListenersEvent + * @see AddClientReloadListenersEvent */ public interface UnbakedModelLoader { /** diff --git a/src/main/java/net/neoforged/neoforge/client/resources/VanillaClientListeners.java b/src/main/java/net/neoforged/neoforge/client/resources/VanillaClientListeners.java new file mode 100644 index 0000000000..23acf42d01 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/resources/VanillaClientListeners.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.resources; + +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.client.PeriodicNotificationManager; +import net.minecraft.client.gui.GuiSpriteManager; +import net.minecraft.client.gui.font.FontManager; +import net.minecraft.client.particle.ParticleEngine; +import net.minecraft.client.renderer.CloudRenderer; +import net.minecraft.client.renderer.GpuWarnlistManager; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.ShaderManager; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.resources.FoliageColorReloadListener; +import net.minecraft.client.resources.GrassColorReloadListener; +import net.minecraft.client.resources.MapDecorationTextureManager; +import net.minecraft.client.resources.MobEffectTextureManager; +import net.minecraft.client.resources.PaintingTextureManager; +import net.minecraft.client.resources.SplashManager; +import net.minecraft.client.resources.language.LanguageManager; +import net.minecraft.client.resources.model.EquipmentAssetManager; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.common.util.VanillaClassToKey; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * Keys for vanilla {@link PreparableReloadListener reload listeners}, used to specify dependency ordering in the {@link AddClientReloadListenersEvent}. + *

+ * Due to the volume of vanilla listeners, these keys are automatically generated based on the class name. + * + * @see {@link VanillaServerListeners} for vanilla server listener names. + * @see {@link NeoForgeReloadListeners} for Neo-added listener names. + */ +public class VanillaClientListeners { + private static final Map, ResourceLocation> KNOWN_CLASSES = new LinkedHashMap<>(); + + public static final ResourceLocation LANGUAGE = key(LanguageManager.class); + + public static final ResourceLocation TEXTURES = key(TextureManager.class); + + public static final ResourceLocation SHADERS = key(ShaderManager.class); + + public static final ResourceLocation SOUNDS = key(SoundManager.class); + + public static final ResourceLocation SPLASHES = key(SplashManager.class); + + public static final ResourceLocation FONTS = key(FontManager.class); + + public static final ResourceLocation GRASS_COLOR = key(GrassColorReloadListener.class); + + public static final ResourceLocation FOLIAGE_COLOR = key(FoliageColorReloadListener.class); + + public static final ResourceLocation MODELS = key(ModelManager.class); + + public static final ResourceLocation EQUIPMENT_ASSETS = key(EquipmentAssetManager.class); + + public static final ResourceLocation MAP_DECORATIONS = key(MapDecorationTextureManager.class); + + public static final ResourceLocation BLOCK_RENDERER = key(BlockRenderDispatcher.class); + + public static final ResourceLocation ENTITY_RENDERER = key(EntityRenderDispatcher.class); + + public static final ResourceLocation BLOCK_ENTITY_RENDERER = key(BlockEntityRenderDispatcher.class); + + public static final ResourceLocation PARTICLE_ENGINE = key(ParticleEngine.class); + + public static final ResourceLocation PAINTING_TEXTURES = key(PaintingTextureManager.class); + + public static final ResourceLocation MOB_EFFECT_TEXTURES = key(MobEffectTextureManager.class); + + public static final ResourceLocation GUI_SPRITES = key(GuiSpriteManager.class); + + public static final ResourceLocation LEVEL_RENDERER = key(LevelRenderer.class); + + public static final ResourceLocation CLOUD_RENDERER = key(CloudRenderer.class); + + public static final ResourceLocation GPU_WARNLIST = key(GpuWarnlistManager.class); + + public static final ResourceLocation REGIONAL_COMPLIANCES = key(PeriodicNotificationManager.class); + + /** + * Sentinel field that will always reference the first reload listener in the vanilla order. + */ + public static final ResourceLocation FIRST = LANGUAGE; + + /** + * Sentinel field that will always reference the last reload listener in the vanilla order. + */ + public static final ResourceLocation LAST = REGIONAL_COMPLIANCES; + + private static ResourceLocation key(Class cls) { + if (KNOWN_CLASSES.containsKey(cls)) { + // Prevent duplicate registration, in case we accidentally use the same class in two different fields. + throw new UnsupportedOperationException("Attempted to create two keys for the same class"); + } + + ResourceLocation key = VanillaClassToKey.convert(cls); + KNOWN_CLASSES.put(cls, key); + return key; + } + + @Nullable + @ApiStatus.Internal + public static ResourceLocation getNameForClass(Class cls) { + return KNOWN_CLASSES.get(cls); + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/resources/package-info.java b/src/main/java/net/neoforged/neoforge/client/resources/package-info.java new file mode 100644 index 0000000000..0482861dcc --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/resources/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package net.neoforged.neoforge.client.resources; + +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; diff --git a/src/main/java/net/neoforged/neoforge/common/NeoForgeEventHandler.java b/src/main/java/net/neoforged/neoforge/common/NeoForgeEventHandler.java index d7440f1ea6..070f3b2f34 100644 --- a/src/main/java/net/neoforged/neoforge/common/NeoForgeEventHandler.java +++ b/src/main/java/net/neoforged/neoforge/common/NeoForgeEventHandler.java @@ -26,7 +26,7 @@ import net.neoforged.neoforge.common.loot.LootModifierManager; import net.neoforged.neoforge.common.util.FakePlayerFactory; import net.neoforged.neoforge.common.util.LogicalSidedProvider; -import net.neoforged.neoforge.event.AddReloadListenerEvent; +import net.neoforged.neoforge.event.AddServerReloadListenersEvent; import net.neoforged.neoforge.event.OnDatapackSyncEvent; import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.TagsUpdatedEvent; @@ -39,12 +39,16 @@ import net.neoforged.neoforge.network.payload.RegistryDataMapSyncPayload; import net.neoforged.neoforge.registries.DataMapLoader; import net.neoforged.neoforge.registries.RegistryManager; +import net.neoforged.neoforge.resource.NeoForgeReloadListeners; import net.neoforged.neoforge.server.command.ConfigCommand; import net.neoforged.neoforge.server.command.NeoForgeCommand; import org.jetbrains.annotations.ApiStatus; @ApiStatus.Internal public class NeoForgeEventHandler { + private static LootModifierManager LOOT_MODIFIER_MANAGER; + private static DataMapLoader DATA_MAP_LOADER; + @SubscribeEvent(priority = EventPriority.HIGH) public void onEntityJoinWorld(EntityJoinLevelEvent event) { Entity entity = event.getEntity(); @@ -102,7 +106,7 @@ public void playerLogin(PlayerEvent.PlayerLoggedInEvent event) { @SubscribeEvent public void tagsUpdated(TagsUpdatedEvent event) { if (event.getUpdateCause() == TagsUpdatedEvent.UpdateCause.SERVER_DATA_LOAD) { - DATA_MAPS.apply(); + DATA_MAP_LOADER.apply(); } } @@ -146,25 +150,17 @@ public void onCommandsRegister(RegisterCommandsEvent event) { ConfigCommand.register(event.getDispatcher()); } - private static LootModifierManager INSTANCE; - private static DataMapLoader DATA_MAPS; - @SubscribeEvent - public void onResourceReload(AddReloadListenerEvent event) { - INSTANCE = new LootModifierManager(); - event.addListener(INSTANCE); - event.addListener(DATA_MAPS = new DataMapLoader(event.getConditionContext(), event.getRegistryAccess())); + public void onResourceReload(AddServerReloadListenersEvent event) { + event.addListener(NeoForgeReloadListeners.LOOT_MODIFIERS, LOOT_MODIFIER_MANAGER = new LootModifierManager()); + event.addListener(NeoForgeReloadListeners.DATA_MAPS, DATA_MAP_LOADER = new DataMapLoader(event.getConditionContext(), event.getRegistryAccess())); + event.addListener(NeoForgeReloadListeners.CREATIVE_TABS, CreativeModeTabRegistry.getReloadListener()); } static LootModifierManager getLootModifierManager() { - if (INSTANCE == null) + if (LOOT_MODIFIER_MANAGER == null) throw new IllegalStateException("Can not retrieve LootModifierManager until resources have loaded once."); - return INSTANCE; - } - - @SubscribeEvent - public void resourceReloadListeners(AddReloadListenerEvent event) { - event.addListener(CreativeModeTabRegistry.getReloadListener()); + return LOOT_MODIFIER_MANAGER; } @SubscribeEvent(priority = EventPriority.HIGHEST) diff --git a/src/main/java/net/neoforged/neoforge/common/util/VanillaClassToKey.java b/src/main/java/net/neoforged/neoforge/common/util/VanillaClassToKey.java new file mode 100644 index 0000000000..1308b830f7 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/common/util/VanillaClassToKey.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.common.util; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.include.com.google.common.base.Preconditions; + +@ApiStatus.Internal +public class VanillaClassToKey { + /** + * Converts a vanilla class name into an identifier compliant with the rules set by {@link ResourceLocation}. + *

+ * This conversion is done by translating all uppercase characters into an underscore plus the lowercase version of the original character. + * + * @param cls The class to convert. + * @return A lower_snake_case representation of that class's original PascalCase name. + * @throws IllegalArgumentException if the class is not from Minecraft, or if the class does not have a {@link Class#getSimpleName() simple name}. + */ + public static ResourceLocation convert(Class cls) { + Preconditions.checkArgument(cls.getPackageName().startsWith("net.minecraft"), "Automatic name conversion can only be applied to net.minecraft classes. Provided: " + cls.getName()); + Preconditions.checkArgument(!cls.getSimpleName().isEmpty(), "Automatic name conversion can only happen for identifiable classes (per Class#getSimpleName()). Provided: " + cls.getName()); + + StringBuilder sb = new StringBuilder(); + cls.getSimpleName().chars().mapMulti((value, consumer) -> { + if (Character.isUpperCase((char) value)) { + consumer.accept('_'); + consumer.accept(Character.toLowerCase((char) value)); + } else { + consumer.accept(value); + } + }).forEach(i -> sb.append((char) i)); + + return ResourceLocation.withDefaultNamespace(sb.substring(1)); // The string will be prefixed with an additional `_` since the first character is uppercase. + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/AddReloadListenerEvent.java b/src/main/java/net/neoforged/neoforge/event/AddReloadListenerEvent.java deleted file mode 100644 index 4cc769bf0f..0000000000 --- a/src/main/java/net/neoforged/neoforge/event/AddReloadListenerEvent.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.event; - -import com.google.common.collect.ImmutableList; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import net.minecraft.core.HolderLookup; -import net.minecraft.core.RegistryAccess; -import net.minecraft.server.ReloadableServerResources; -import net.minecraft.server.packs.resources.PreparableReloadListener; -import net.minecraft.server.packs.resources.ResourceManager; -import net.neoforged.bus.api.Event; -import net.neoforged.fml.ModLoader; -import net.neoforged.neoforge.common.NeoForge; -import net.neoforged.neoforge.common.conditions.ICondition; -import net.neoforged.neoforge.resource.ContextAwareReloadListener; - -/** - * The main ResourceManager is recreated on each reload, just after {@link ReloadableServerResources}'s creation. - * - * The event is fired on each reload and lets modders add their own ReloadListeners, for server-side resources. - * The event is fired on the {@link NeoForge#EVENT_BUS} - */ -public class AddReloadListenerEvent extends Event { - private final List listeners = new ArrayList<>(); - private final ReloadableServerResources serverResources; - private final RegistryAccess registryAccess; - - public AddReloadListenerEvent(ReloadableServerResources serverResources, RegistryAccess registryAccess) { - this.serverResources = serverResources; - this.registryAccess = registryAccess; - } - - /** - * @param listener the listener to add to the ResourceManager on reload - */ - public void addListener(PreparableReloadListener listener) { - listeners.add(new WrappedStateAwareListener(listener)); - } - - public List getListeners() { - return ImmutableList.copyOf(listeners); - } - - /** - * @return The ReloableServerResources being reloaded. - */ - public ReloadableServerResources getServerResources() { - return serverResources; - } - - /** - * This context object holds data relevant to the current reload, such as staged tags. - * - * @return The condition context for the currently active reload. - */ - public ICondition.IContext getConditionContext() { - return serverResources.getConditionContext(); - } - - /** - * Provides access to the loaded registries associated with these server resources. - * All built-in and dynamic registries are loaded and frozen by this point. - * - * @return The RegistryAccess context for the currently active reload. - */ - public RegistryAccess getRegistryAccess() { - return registryAccess; - } - - private static class WrappedStateAwareListener extends ContextAwareReloadListener implements PreparableReloadListener { - private final PreparableReloadListener wrapped; - - private WrappedStateAwareListener(final PreparableReloadListener wrapped) { - this.wrapped = wrapped; - } - - @Override - public void injectContext(ICondition.IContext context, HolderLookup.Provider registryLookup) { - if (this.wrapped instanceof ContextAwareReloadListener contextAwareListener) { - contextAwareListener.injectContext(context, registryLookup); - } - } - - @Override - public CompletableFuture reload(final PreparationBarrier stage, final ResourceManager resourceManager, final Executor backgroundExecutor, final Executor gameExecutor) { - if (!ModLoader.hasErrors()) - return wrapped.reload(stage, resourceManager, backgroundExecutor, gameExecutor); - else - return CompletableFuture.completedFuture(null); - } - } -} diff --git a/src/main/java/net/neoforged/neoforge/event/AddServerReloadListenersEvent.java b/src/main/java/net/neoforged/neoforge/event/AddServerReloadListenersEvent.java new file mode 100644 index 0000000000..c68fa0731f --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/AddServerReloadListenersEvent.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ReloadableServerResources; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.common.conditions.ICondition; +import net.neoforged.neoforge.resource.VanillaServerListeners; +import org.jetbrains.annotations.ApiStatus; + +/** + * The main ResourceManager is recreated on each reload, just after {@link ReloadableServerResources}'s creation. + * + * The event is fired on each reload and lets modders add their own ReloadListeners, for server-side resources. + * The event is fired on the {@link NeoForge#EVENT_BUS} + */ +public class AddServerReloadListenersEvent extends SortedReloadListenerEvent { + private final ReloadableServerResources serverResources; + private final RegistryAccess registryAccess; + + @ApiStatus.Internal + public AddServerReloadListenersEvent(ReloadableServerResources serverResources, RegistryAccess registryAccess) { + super(serverResources.listeners(), AddServerReloadListenersEvent::lookupName); + this.serverResources = serverResources; + this.registryAccess = registryAccess; + } + + /** + * @return The {@link ReloadableServerResources} being reloaded. + */ + public ReloadableServerResources getServerResources() { + return serverResources; + } + + /** + * This context object holds data relevant to the current reload, such as staged tags. + * + * @return The condition context for the currently active reload. + */ + public ICondition.IContext getConditionContext() { + return serverResources.getConditionContext(); + } + + /** + * Provides access to the loaded registries associated with these server resources. + * All built-in and dynamic registries are loaded and frozen by this point. + * + * @return The RegistryAccess context for the currently active reload. + */ + public RegistryAccess getRegistryAccess() { + return registryAccess; + } + + private static ResourceLocation lookupName(PreparableReloadListener listener) { + ResourceLocation key = VanillaServerListeners.getNameForClass(listener.getClass()); + if (key == null) { + if (listener.getClass().getPackageName().startsWith("net.minecraft")) { + throw new IllegalArgumentException("A key for the reload listener " + listener + " was not provided in VanillaServerListeners!"); + } else { + throw new IllegalArgumentException("A non-vanilla reload listener " + listener + " was added via mixin before the AddReloadListenerEvent! Mod-added listeners must go through the event."); + } + } + return key; + } +} diff --git a/src/main/java/net/neoforged/neoforge/event/EventHooks.java b/src/main/java/net/neoforged/neoforge/event/EventHooks.java index 31da1af28b..5fc3a9e5db 100644 --- a/src/main/java/net/neoforged/neoforge/event/EventHooks.java +++ b/src/main/java/net/neoforged/neoforge/event/EventHooks.java @@ -34,6 +34,7 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ReloadableServerRegistries; import net.minecraft.server.ReloadableServerResources; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ServerLevel; @@ -179,6 +180,7 @@ import net.neoforged.neoforge.event.tick.LevelTickEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.neoforged.neoforge.resource.ReloadListenerSort; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -798,10 +800,19 @@ public static long onSleepFinished(ServerLevel level, long newTime, long minTime return event.getNewTime(); } + /** + * Fires the {@link AddServerReloadListenersEvent} and returns the sorted list of reload listeners. + * + * @param serverResources The just-created {@link ReloadableServerResources} instance. + * @param registryAccess The registry access from the {@link ReloadableServerRegistries.LoadResult}. + * @return The sorted list of reload listeners. + * + * @throws IllegalArgumentException if {@link ReloadListenerSort#sort(SortedReloadListenerEvent)} detects a cycle. + */ public static List onResourceReload(ReloadableServerResources serverResources, RegistryAccess registryAccess) { - AddReloadListenerEvent event = new AddReloadListenerEvent(serverResources, registryAccess); + AddServerReloadListenersEvent event = new AddServerReloadListenersEvent(serverResources, registryAccess); NeoForge.EVENT_BUS.post(event); - return event.getListeners(); + return ReloadListenerSort.sort(event); } public static void onCommandRegister(CommandDispatcher dispatcher, Commands.CommandSelection environment, CommandBuildContext context) { diff --git a/src/main/java/net/neoforged/neoforge/event/SortedReloadListenerEvent.java b/src/main/java/net/neoforged/neoforge/event/SortedReloadListenerEvent.java new file mode 100644 index 0000000000..239c0ae87e --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/event/SortedReloadListenerEvent.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.event; + +import com.google.common.graph.ElementOrder; +import com.google.common.graph.Graph; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.neoforged.bus.api.Event; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import org.jetbrains.annotations.ApiStatus; + +/** + * Base class for {@link AddServerReloadListenersEvent} and {@link AddClientReloadListenersEvent}. + *

+ * This class holds the sorting logic that allows for the creation of dependency ordering. + */ +public abstract class SortedReloadListenerEvent extends Event { + private final Map registry = new LinkedHashMap<>(); + private final Map keys = new IdentityHashMap<>(); + private final MutableGraph graph = GraphBuilder.directed().nodeOrder(ElementOrder.insertion()).build(); + private final PreparableReloadListener lastVanilla; + + @ApiStatus.Internal + protected SortedReloadListenerEvent(List vanillaListeners, NameLookup lookup) { + // Register the names for all vanilla listeners + for (PreparableReloadListener listener : vanillaListeners) { + ResourceLocation key = lookup.apply(listener); + this.addListener(key, listener); + } + + // Setup the edges for vanilla listeners + for (int i = 1; i < vanillaListeners.size(); i++) { + PreparableReloadListener prev = vanillaListeners.get(i - 1); + PreparableReloadListener listener = vanillaListeners.get(i); + this.graph.putEdge(prev, listener); + } + + this.lastVanilla = vanillaListeners.getLast(); + } + + /** + * Adds a new {@link PreparableReloadListener reload listener} to the resource manager. + *

+ * Unless explicitly specified, this listener will run after all vanilla listeners, in the order it was registered. + * + * @param key The resource location that identifies the reload listener for dependency sorting. + * @param listener The listener to add. + * + * @throws IllegalArgumentException if another listener with that key was already registered. + */ + public void addListener(ResourceLocation key, PreparableReloadListener listener) { + if (this.registry.containsKey(key) || this.registry.containsValue(listener)) { + throw new IllegalArgumentException("Attempted to register two reload listeners for the same key: " + key); + } + this.registry.put(key, listener); + this.keys.put(listener, key); + this.graph.addNode(listener); + } + + /** + * Adds a new dependency entry, such that {@code first} must run before {@code second}. + *

+ * Introduction of dependency cycles (first->second->first) will cause an error when the event is finished. + * + * @param first The key of the reload listener that must run first. + * @param second The key of the reload listener that must run after {@code first}. + * + * @throws IllegalArgumentException if either {@code first} or {@code second} has not been registered via {@link #addListener}. + * + * @see {@link NeoForgeReloadListeners} for Neo's reload listener keys. + * @see {@link VanillaClientListeners} for the keys of vanilla client listeners. + * @see {@link VanillaServerListeners} for the keys of vanilla server listeners. + */ + public void addDependency(ResourceLocation first, ResourceLocation second) { + this.graph.putEdge(this.getOrThrow(first), this.getOrThrow(second)); + } + + /** + * Returns an immutable view of the dependency graph. + */ + public Graph getGraph() { + return this.graph; + } + + /** + * Returns an immutable view of the reload listener registry. + *

+ * The registry is linked, meaning the iteration order depends on the registration order. + */ + public Map getRegistry() { + return Collections.unmodifiableMap(this.registry); + } + + /** + * Returns a {@link NameLookup} for all reload listeners known by this event. + */ + public NameLookup getNameLookup() { + return this::getOrThrow; + } + + /** + * Returns a reference to the last vanilla listener, used during the final sort. + */ + @ApiStatus.Internal + public PreparableReloadListener getLastVanillaListener() { + return this.lastVanilla; + } + + private PreparableReloadListener getOrThrow(ResourceLocation key) { + PreparableReloadListener listener = this.registry.get(key); + if (listener == null) { + throw new IllegalArgumentException("Unknown reload listener: " + key); + } + return listener; + } + + private ResourceLocation getOrThrow(PreparableReloadListener listener) { + ResourceLocation key = this.keys.get(listener); + if (key == null) { + throw new IllegalArgumentException("Unknown reload listener: " + listener); + } + return key; + } + + @FunctionalInterface + public interface NameLookup extends Function { + /** + * Looks up the name for a reload listener. + * + * @throws IllegalArgumentException if there was no name for the listener. + */ + @Override + ResourceLocation apply(PreparableReloadListener t); + } +} diff --git a/src/main/java/net/neoforged/neoforge/resource/NeoForgeReloadListeners.java b/src/main/java/net/neoforged/neoforge/resource/NeoForgeReloadListeners.java new file mode 100644 index 0000000000..1b1992b90a --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/resource/NeoForgeReloadListeners.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.resource; + +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; + +/** + * Keys for Neo-added resource listeners, for use in dependency ordering in the relevant events. + * + * @see {@link VanillaClientListeners} for vanilla client listener names. + * @see {@link VanillaServerListeners} for vanilla server listener names. + */ +public class NeoForgeReloadListeners { + + // Server Listeners + public static final ResourceLocation LOOT_MODIFIERS = key("loot_modifiers"); + + public static final ResourceLocation DATA_MAPS = key("data_maps"); + + public static final ResourceLocation CREATIVE_TABS = key("creative_tabs"); + + // Client Listeners + public static final ResourceLocation CLIENT_MOD_LOADING = key("client_mod_loading"); + + public static final ResourceLocation BRANDING = key("branding"); + + public static final ResourceLocation OBJ_LOADER = key("obj_loader"); + + public static final ResourceLocation ENTITY_ANIMATIONS = key("entity_animations"); + + private static ResourceLocation key(String path) { + return ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, path); + } +} diff --git a/src/main/java/net/neoforged/neoforge/resource/ReloadListenerSort.java b/src/main/java/net/neoforged/neoforge/resource/ReloadListenerSort.java new file mode 100644 index 0000000000..e4e0a29eef --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/resource/ReloadListenerSort.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.resource; + +import com.google.common.graph.Graph; +import com.google.common.graph.Graphs; +import com.google.common.graph.MutableGraph; +import com.google.common.graph.Traverser; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.neoforged.fml.loading.toposort.CyclePresentException; +import net.neoforged.fml.loading.toposort.TopologicalSort; +import net.neoforged.neoforge.event.SortedReloadListenerEvent; +import net.neoforged.neoforge.event.SortedReloadListenerEvent.NameLookup; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class ReloadListenerSort { + /** + * Sorts the listeners and emits the returned list. + *

+ * This method modifies the current state of the graph to ensure that all dangling listeners run after vanilla. + * + * @return An immutable, sorted list of listeners based on the current dependency graph. + * + * @throws IllegalArgumentException if cycles were detected in the dependency graph. + */ + public static List sort(SortedReloadListenerEvent event) { + return sortListeners(event.getNameLookup(), (MutableGraph) event.getGraph(), event.getRegistry(), event.getLastVanillaListener()); + } + + /** + * Implementation for {@link #sort(SortedReloadListenerEvent)}. + * + * @param lookup The {@link SortedReloadListenerEvent#getNameLookup() name lookup} from the event. + * @param graph The mutable graph. The event only exposes a non-mutable graph, but we can just downcast it since we know it's a MutableGraph. + * @param registry The reload listener registry. + * @param lastVanilla A reference to the last vanilla listener in vanilla order. + * @return An immutable, sorted list of listeners based on the current dependency graph. + * + * @throws IllegalArgumentException if cycles were detected in the dependency graph. + */ + public static List sortListeners(NameLookup lookup, MutableGraph graph, Map registry, PreparableReloadListener lastVanilla) { + // For any entries without a dependency, ensure they depend on the last vanilla loader. + for (Map.Entry entry : registry.entrySet()) { + if (needsToBeLinkedToVanilla(lookup, graph, entry.getValue())) { + graph.putEdge(lastVanilla, entry.getValue()); + } + } + + // Then build the index mapping in a way that can be used as a comparator to preserve insertion order. + Object2IntMap insertionOrder = new Object2IntOpenHashMap<>(); + int idx = 0; + for (PreparableReloadListener listener : registry.values()) { + insertionOrder.put(listener, idx++); + } + + // Then do the sort. + try { + List sorted = TopologicalSort.topologicalSort(graph, Comparator.comparingInt(insertionOrder::getInt)); + return Collections.unmodifiableList(sorted); + } catch (CyclePresentException ex) { + // If a cycle is found, we have to transform the information in the exception back into the registered keys. + Set> cycles = ex.getCycles(); + Set> keyedCycles = cycles.stream().map(set -> { + return set.stream().map(lookup::apply).collect(Collectors.toCollection(LinkedHashSet::new)); + }).collect(Collectors.toSet()); + + // Finally, build a real error message and re-throw. + StringBuilder sb = new StringBuilder(); + sb.append("Cycles were detected during reload listener sorting:").append('\n'); + + idx = 0; + for (Set cycle : keyedCycles) { + StringBuilder msg = new StringBuilder(); + + msg.append(idx++).append(": "); + + for (ResourceLocation key : cycle) { + msg.append(key).append("->"); + } + + msg.append(cycle.iterator().next()); + + sb.append(msg); + sb.append('\n'); + } + + throw new IllegalArgumentException(sb.toString()); + } + } + + /** + * A node needs to be linked to vanilla if it is otherwise "dangling" from the vanilla graph. + *

+ * To determine if a node needs to be linked, we perform a forward and backward DFS to detect if there are any links to vanilla nodes. + * If there are no links, we add an edge against the last vanilla listener based on the default order. + * + * @return true if the listener needs to be linked to vanilla. + */ + private static boolean needsToBeLinkedToVanilla(NameLookup lookup, Graph graph, PreparableReloadListener listener) { + if (isVanilla(lookup, listener)) { + return false; + } + + for (PreparableReloadListener node : Traverser.forGraph(graph).depthFirstPreOrder(listener)) { + if (isVanilla(lookup, node)) { + return false; + } + } + + for (PreparableReloadListener node : Traverser.forGraph(Graphs.transpose(graph)).depthFirstPreOrder(listener)) { + if (isVanilla(lookup, node)) { + return false; + } + } + + return true; + } + + private static boolean isVanilla(NameLookup lookup, PreparableReloadListener listener) { + return "minecraft".equals(lookup.apply(listener).getNamespace()); + } +} diff --git a/src/main/java/net/neoforged/neoforge/resource/VanillaServerListeners.java b/src/main/java/net/neoforged/neoforge/resource/VanillaServerListeners.java new file mode 100644 index 0000000000..fafe717d45 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/resource/VanillaServerListeners.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.resource; + +import java.util.LinkedHashMap; +import java.util.Map; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ServerAdvancementManager; +import net.minecraft.server.ServerFunctionLibrary; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.world.item.crafting.RecipeManager; +import net.neoforged.neoforge.common.util.VanillaClassToKey; +import net.neoforged.neoforge.event.AddServerReloadListenersEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * Keys for vanilla {@link PreparableReloadListener reload listeners}, used to specify dependency ordering in the {@link AddServerReloadListenersEvent}. + *

+ * Due to the volume of vanilla listeners, these keys are automatically generated based on the class name. + * + * @see {@link VanillaClientListeners} for vanilla client listener names. + * @see {@link NeoForgeReloadListeners} for Neo-added listener names. + */ +public class VanillaServerListeners { + private static final Map, ResourceLocation> KNOWN_CLASSES = new LinkedHashMap<>(); + + public static final ResourceLocation RECIPES = key(RecipeManager.class); + + public static final ResourceLocation FUNCTIONS = key(ServerFunctionLibrary.class); + + public static final ResourceLocation ADVANCEMENTS = key(ServerAdvancementManager.class); + + /** + * Sentinel field that will always reference the first reload listener in the vanilla order. + */ + public static final ResourceLocation FIRST = RECIPES; + + /** + * Sentinel field that will always reference the last reload listener in the vanilla order. + */ + public static final ResourceLocation LAST = ADVANCEMENTS; + + private static ResourceLocation key(Class cls) { + if (KNOWN_CLASSES.containsKey(cls)) { + // Prevent duplicate registration, in case we accidentally use the same class in two different fields. + throw new UnsupportedOperationException("Attempted to create two keys for the same class"); + } + + ResourceLocation key = VanillaClassToKey.convert(cls); + KNOWN_CLASSES.put(cls, key); + return key; + } + + @Nullable + @ApiStatus.Internal + public static ResourceLocation getNameForClass(Class cls) { + return KNOWN_CLASSES.get(cls); + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index fc21dedc59..815c5def3a 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -316,3 +316,6 @@ public net.minecraft.client.data.models.ItemModelGenerators$TrimMaterialData public net.minecraft.client.data.models.ItemModelGenerators$TrimMaterialData (Ljava/lang/String;Lnet/minecraft/resources/ResourceKey;Ljava/util/Map;)V public-f net.minecraft.client.data.models.ModelProvider getName()Ljava/lang/String; public net.minecraft.client.data.models.ModelProvider$ItemInfoCollector register(Lnet/minecraft/world/item/Item;Lnet/minecraft/client/renderer/item/ClientItem;)V + +# Required to support sorting the client reload listeners. +private-f net.minecraft.server.packs.resources.ReloadableResourceManager listeners diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/client/CustomGlyphProviderTypeTest.java b/tests/src/main/java/net/neoforged/neoforge/debug/client/CustomGlyphProviderTypeTest.java index a93921d745..b30d86794e 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/client/CustomGlyphProviderTypeTest.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/client/CustomGlyphProviderTypeTest.java @@ -18,7 +18,8 @@ import net.minecraft.util.profiling.ProfilerFiller; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.common.asm.enumextension.EnumProxy; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; @@ -28,9 +29,11 @@ public class CustomGlyphProviderTypeTest { public static final EnumProxy REFERENCE_2_PARAMS = new EnumProxy<>( GlyphProviderType.class, "neotests:reference_2", Reference2.CODEC); + public static final ResourceLocation LISTENER_NAME = ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, "glyph_test"); + @TestHolder(description = "Tests if custom GlyphProviderTypes were used for loading resources", enabledByDefault = true) static void setupGlyphProviderTypeTest(DynamicTest test) { - test.framework().modEventBus().addListener((RegisterClientReloadListenersEvent event) -> event.registerReloadListener(new SimplePreparableReloadListener() { + test.framework().modEventBus().addListener((AddClientReloadListenersEvent event) -> event.addListener(LISTENER_NAME, new SimplePreparableReloadListener() { @Override protected Void prepare(ResourceManager p_10796_, ProfilerFiller p_10797_) { return null; diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java index 11caca9757..ee1a95150e 100644 --- a/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java +++ b/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java @@ -12,14 +12,17 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.neoforged.api.distmarker.Dist; -import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; import net.neoforged.neoforge.client.event.RegisterMaterialAtlasesEvent; +import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion; import net.neoforged.testframework.DynamicTest; import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.TestHolder; @ForEachTest(side = Dist.CLIENT, groups = { "client.texture_atlas", "texture_atlas" }) public class TextureAtlasTests { + public static final ResourceLocation LISTENER_NAME = ResourceLocation.fromNamespaceAndPath(NeoForgeVersion.MOD_ID, "atlas_test"); + @TestHolder(description = { "Tests that texture atlases intended for use with Material are correctly registered and loaded" }, enabledByDefault = true) static void testMaterialAtlas(final DynamicTest test) { String modId = test.createModId(); @@ -30,8 +33,8 @@ static void testMaterialAtlas(final DynamicTest test) { event.register(atlasLoc, infoLoc); }); - test.framework().modEventBus().addListener(RegisterClientReloadListenersEvent.class, event -> { - event.registerReloadListener((ResourceManagerReloadListener) manager -> { + test.framework().modEventBus().addListener(AddClientReloadListenersEvent.class, event -> { + event.addListener(LISTENER_NAME, (ResourceManagerReloadListener) manager -> { try { Minecraft.getInstance().getModelManager().getAtlas(atlasLoc); } catch (NullPointerException npe) {