diff --git a/src/main/java/net/fabricmc/fabric/api/resource/IdentifiableResourceReloadListener.java b/src/main/java/net/fabricmc/fabric/api/resource/IdentifiableResourceReloadListener.java new file mode 100644 index 000000000..fc17fc7bb --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/api/resource/IdentifiableResourceReloadListener.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.resource; + +import net.minecraft.resource.ResourceReloadListener; +import net.minecraft.util.Identifier; + +import java.util.Collection; +import java.util.Collections; + +/** + * Interface for "identifiable" resource reload listeners. + * + * "Identifiable" listeners have an unique identifier, which can be depended on, + * and can provide dependencies that they would like to see executed before + * themselves. + * + * {@link ResourceReloadListenerKeys} + */ +public interface IdentifiableResourceReloadListener extends ResourceReloadListener { + /** + * @return The unique identifier of this listener. + */ + Identifier getFabricId(); + + /** + * @return The identifiers of listeners this listener expects to have been + * executed before itself. + */ + default Collection getFabricDependencies() { + return Collections.emptyList(); + } + + /** + * By default, resource reload listeners in Minecraft are all executed on + * the game's main thread while the game is paused. This means they do not + * need to provide any guarantees regarding their thread safety, or usage + * of resources potentially modified by other reload listeners. + * + * "Thread safety", in this context, refers simply to independence; namely + * whether or not, under the condition that all of its dependencies have + * already been processed, this resource reload listener can run without + * accessing or modifying areas it does not control in a non-thread-safe + * manner. + * + * @return Whether or not the listener can be executed in a thread-safe way. + */ + default boolean isListenerThreadSafe() { + return false; + } +} diff --git a/src/main/java/net/fabricmc/fabric/api/resource/ResourceManagerHelper.java b/src/main/java/net/fabricmc/fabric/api/resource/ResourceManagerHelper.java new file mode 100644 index 000000000..917436840 --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/api/resource/ResourceManagerHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.resource; + +import net.fabricmc.fabric.impl.resources.ResourceManagerHelperImpl; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; + +/** + * Helper for working with {@link ResourceManager} instances. + */ +public interface ResourceManagerHelper { + /** + * Add a resource reload listener for a given registry. + * @param listener The resource reload listener. + */ + void addReloadListener(IdentifiableResourceReloadListener listener); + + /** + * Get the ResourceManagerHelper instance for a given resource type. + * @param type The given resource type. + * @return The ResourceManagerHelper instance. + */ + static ResourceManagerHelper get(ResourceType type) { + return ResourceManagerHelperImpl.get(type); + } +} diff --git a/src/main/java/net/fabricmc/fabric/api/resource/ResourceReloadListenerKeys.java b/src/main/java/net/fabricmc/fabric/api/resource/ResourceReloadListenerKeys.java new file mode 100644 index 000000000..bac69bbcf --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/api/resource/ResourceReloadListenerKeys.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.resource; + +import net.minecraft.util.Identifier; + +/** + * This class contains default keys for various Minecraft resource reload listeners. + * + * {@link IdentifiableResourceReloadListener} + */ +public final class ResourceReloadListenerKeys { + // client + public static final Identifier SOUNDS = new Identifier("minecraft:sounds"); + public static final Identifier FONTS = new Identifier("minecraft:fonts"); + public static final Identifier MODELS = new Identifier("minecraft:models"); + public static final Identifier LANGUAGES = new Identifier("minecraft:languages"); + public static final Identifier TEXTURES = new Identifier("minecraft:textures"); + + // server + public static final Identifier TAGS = new Identifier("minecraft:tags"); + public static final Identifier RECIPES = new Identifier("minecraft:recipes"); + public static final Identifier ADVANCEMENTS = new Identifier("minecraft:advancements"); + public static final Identifier FUNCTIONS = new Identifier("minecraft:functions"); + public static final Identifier LOOT_TABLES = new Identifier("minecraft:loot_tables"); + + private ResourceReloadListenerKeys() { + + } +} diff --git a/src/main/java/net/fabricmc/fabric/impl/resources/ResourceManagerHelperImpl.java b/src/main/java/net/fabricmc/fabric/impl/resources/ResourceManagerHelperImpl.java new file mode 100644 index 000000000..61f984af4 --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/impl/resources/ResourceManagerHelperImpl.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.resources; + +import com.google.common.collect.Lists; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.resource.ResourceReloadListener; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.*; + +public class ResourceManagerHelperImpl implements ResourceManagerHelper { + private static final Map registryMap = new HashMap<>(); + private static final Logger LOGGER = LogManager.getLogger(); + + private final List addedListeners = new ArrayList<>(); + + public static ResourceManagerHelper get(ResourceType type) { + return registryMap.computeIfAbsent(type, (t) -> new ResourceManagerHelperImpl()); + } + + public static void sort(ResourceType type, List listeners) { + ResourceManagerHelperImpl instance = registryMap.get(type); + if (instance != null) { + instance.sort(listeners); + } + } + + protected void sort(List listeners) { + listeners.removeAll(addedListeners); + + // General rules: + // - We *do not* touch the ordering of vanilla listeners. Ever. + // While dependency values are provided where possible, we cannot + // trust them 100%. Only code doesn't lie. + // - We addReloadListener all custom listeners after vanilla listeners. Same reasons. + + List listenersToAdd = Lists.newArrayList(addedListeners); + Set resolvedIds = new HashSet<>(); + for (ResourceReloadListener listener : listeners) { + if (listener instanceof IdentifiableResourceReloadListener) { + resolvedIds.add(((IdentifiableResourceReloadListener) listener).getFabricId()); + } + } + + int lastSize = -1; + while (listeners.size() != lastSize) { + lastSize = listeners.size(); + + Iterator it = listenersToAdd.iterator(); + while (it.hasNext()) { + IdentifiableResourceReloadListener listener = it.next(); + if (resolvedIds.containsAll(listener.getFabricDependencies())) { + resolvedIds.add(listener.getFabricId()); + listeners.add(listener); + it.remove(); + } + } + } + + for (IdentifiableResourceReloadListener listener : listenersToAdd) { + LOGGER.warn("Could not resolve dependencies for listener: " + listener.getFabricId() + "!"); + } + } + + @Override + public void addReloadListener(IdentifiableResourceReloadListener listener) { + addedListeners.add(listener); + } +} diff --git a/src/main/java/net/fabricmc/fabric/mixin/resources/MixinKeyedResourceReloadListener.java b/src/main/java/net/fabricmc/fabric/mixin/resources/MixinKeyedResourceReloadListener.java new file mode 100644 index 000000000..fa2163e01 --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/mixin/resources/MixinKeyedResourceReloadListener.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.resources; + +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceReloadListenerKeys; +import net.minecraft.client.audio.SoundLoader; +import net.minecraft.client.font.FontRendererManager; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.render.block.BlockRenderManager; +import net.minecraft.client.render.item.ItemRenderer; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.resource.language.LanguageManager; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.recipe.RecipeManager; +import net.minecraft.server.ServerAdvancementLoader; +import net.minecraft.server.function.CommandFunctionManager; +import net.minecraft.tag.TagManager; +import net.minecraft.util.Identifier; +import net.minecraft.world.loot.LootManager; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.Collection; +import java.util.Collections; + +public class MixinKeyedResourceReloadListener { + @Mixin({ + /* public */ + SoundLoader.class, FontRendererManager.class, BakedModelManager.class, LanguageManager.class, TextureManager.class, + /* private */ + WorldRenderer.class, BlockRenderManager.class, ItemRenderer.class + }) + public static abstract class Client implements IdentifiableResourceReloadListener { + private Collection fabric_idDeps; + private Identifier fabric_id; + + @Override + @SuppressWarnings({ "ConstantConditions", "RedundantCast" }) + public Collection getFabricDependencies() { + if (fabric_idDeps == null) { + Object self = (Object) this; + if (self instanceof BakedModelManager || self instanceof WorldRenderer) { + fabric_idDeps = Collections.singletonList(ResourceReloadListenerKeys.TEXTURES); + } else if (self instanceof ItemRenderer || self instanceof BlockRenderManager) { + fabric_idDeps = Collections.singletonList(ResourceReloadListenerKeys.MODELS); + } else { + fabric_idDeps = Collections.emptyList(); + } + } + + return fabric_idDeps; + } + + @Override + @SuppressWarnings({ "ConstantConditions", "RedundantCast" }) + public Identifier getFabricId() { + if (fabric_id == null) { + Object self = (Object) this; + + if (self instanceof SoundLoader) { + fabric_id = ResourceReloadListenerKeys.SOUNDS; + } else if (self instanceof FontRendererManager) { + fabric_id = ResourceReloadListenerKeys.FONTS; + } else if (self instanceof BakedModelManager) { + fabric_id = ResourceReloadListenerKeys.MODELS; + } else if (self instanceof LanguageManager) { + fabric_id = ResourceReloadListenerKeys.LANGUAGES; + } else if (self instanceof TextureManager) { + fabric_id = ResourceReloadListenerKeys.TEXTURES; + } else { + fabric_id = new Identifier("minecraft", "private/" + self.getClass().getSimpleName()); + } + } + + return fabric_id; + } + } + + @Mixin({ + /* public */ + RecipeManager.class, ServerAdvancementLoader.class, CommandFunctionManager.class, LootManager.class, TagManager.class + /* private */ + }) + public static abstract class Server implements IdentifiableResourceReloadListener { + private Collection fabric_idDeps; + private Identifier fabric_id; + + @Override + @SuppressWarnings({ "ConstantConditions", "RedundantCast" }) + public Collection getFabricDependencies() { + if (fabric_idDeps == null) { + Object self = (Object) this; + + if (self instanceof TagManager) { + fabric_idDeps = Collections.emptyList(); + } else { + fabric_idDeps = Collections.singletonList(ResourceReloadListenerKeys.TAGS); + } + } + + return fabric_idDeps; + } + + @Override + @SuppressWarnings({ "ConstantConditions", "RedundantCast" }) + public Identifier getFabricId() { + if (fabric_id == null) { + Object self = (Object) this; +; + if (self instanceof RecipeManager) { + fabric_id = ResourceReloadListenerKeys.RECIPES; + } else if (self instanceof ServerAdvancementLoader) { + fabric_id = ResourceReloadListenerKeys.ADVANCEMENTS; + } else if (self instanceof CommandFunctionManager) { + fabric_id = ResourceReloadListenerKeys.FUNCTIONS; + } else if (self instanceof LootManager) { + fabric_id = ResourceReloadListenerKeys.LOOT_TABLES; + } else if (self instanceof TagManager) { + fabric_id = ResourceReloadListenerKeys.TAGS; + } else { + fabric_id = new Identifier("minecraft", "private/" + self.getClass().getSimpleName()); + } + } + + return fabric_id; + } + } +} diff --git a/src/main/java/net/fabricmc/fabric/mixin/resources/MixinReloadableResourceManagerImpl.java b/src/main/java/net/fabricmc/fabric/mixin/resources/MixinReloadableResourceManagerImpl.java new file mode 100644 index 000000000..c3b864b18 --- /dev/null +++ b/src/main/java/net/fabricmc/fabric/mixin/resources/MixinReloadableResourceManagerImpl.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.resources; + +import net.fabricmc.fabric.impl.resources.ResourceManagerHelperImpl; +import net.minecraft.resource.*; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(ReloadableResourceManagerImpl.class) +public class MixinReloadableResourceManagerImpl { + @Shadow + private List listeners; + @Shadow + private ResourceType type; + + @Inject(at = @At("HEAD"), method = "reload") + public void reload(List packs, CallbackInfo info) { + ResourceManagerHelperImpl.sort(type, listeners); + } +} diff --git a/src/main/resources/net.fabricmc.fabric.mixins.client.json b/src/main/resources/net.fabricmc.fabric.mixins.client.json index 9e3227ff0..20abc037a 100644 --- a/src/main/resources/net.fabricmc.fabric.mixins.client.json +++ b/src/main/resources/net.fabricmc.fabric.mixins.client.json @@ -21,6 +21,7 @@ "registry.client.MixinBlockColorMap", "registry.client.MixinItemColorMap", "registry.client.MixinItemModelMap", + "resources.MixinKeyedResourceReloadListener$Client", "resources.MixinMinecraftGame" ], "injectors": { diff --git a/src/main/resources/net.fabricmc.fabric.mixins.common.json b/src/main/resources/net.fabricmc.fabric.mixins.common.json index a3f34a861..137fd6dfa 100644 --- a/src/main/resources/net.fabricmc.fabric.mixins.common.json +++ b/src/main/resources/net.fabricmc.fabric.mixins.common.json @@ -27,7 +27,9 @@ "registry.MixinIdRegistry", "registry.MixinServerPlayNetworkHandler", "registry.MixinWorldSaveHandler", + "resources.MixinKeyedResourceReloadListener$Server", "resources.MixinMinecraftServer", + "resources.MixinReloadableResourceManagerImpl", "tools.MixinItemStack", "tools.MixinMiningToolItem" ], diff --git a/src/test/java/net/fabricmc/fabric/resources/ResourceReloadModClient.java b/src/test/java/net/fabricmc/fabric/resources/ResourceReloadModClient.java new file mode 100644 index 000000000..f81db2fa0 --- /dev/null +++ b/src/test/java/net/fabricmc/fabric/resources/ResourceReloadModClient.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.resources; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; + +import java.util.Collection; +import java.util.Collections; + +public class ResourceReloadModClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + ResourceManagerHelper.get(ResourceType.ASSETS).addReloadListener(new IdentifiableResourceReloadListener() { + @Override + public Identifier getFabricId() { + return new Identifier("fabric:rrmc2"); + } + + @Override + public Collection getFabricDependencies() { + return Collections.singletonList(new Identifier("fabric:rrmc1")); + } + + @Override + public void onResourceReload(ResourceManager var1) { + System.out.println("Reloading (should run as #2)"); + } + }); + + ResourceManagerHelper.get(ResourceType.ASSETS).addReloadListener(new IdentifiableResourceReloadListener() { + @Override + public Identifier getFabricId() { + return new Identifier("fabric:rrmc1"); + } + + @Override + public void onResourceReload(ResourceManager var1) { + System.out.println("Reloading (should run as #1)"); + } + }); + + ResourceManagerHelper.get(ResourceType.ASSETS).addReloadListener(new IdentifiableResourceReloadListener() { + @Override + public Identifier getFabricId() { + return new Identifier("fabric:rrmc_should_not_resolve"); + } + + @Override + public Collection getFabricDependencies() { + return Collections.singletonList(new Identifier("fabric:rrmc_nonexistent")); + } + + @Override + public void onResourceReload(ResourceManager var1) { + } + }); + } +}