diff --git a/fabric-resource-loader-v0/build.gradle b/fabric-resource-loader-v0/build.gradle index 3c7ab2966e..51a585259f 100644 --- a/fabric-resource-loader-v0/build.gradle +++ b/fabric-resource-loader-v0/build.gradle @@ -4,8 +4,55 @@ loom { accessWidenerPath = file("src/main/resources/fabric-resource-loader-v0.accesswidener") } +moduleDependencies(project, ['fabric-api-base']) + testDependencies(project, [ ':fabric-lifecycle-events-v1', ':fabric-api-base', + ':fabric-gametest-api-v1', ':fabric-resource-loader-v0' ]) + +// Setup 3 test mods used for testing resource sorting +sourceSets { + testmodA + testmodB + testmodC +} + +[sourceSets.testmodA, sourceSets.testmodB, sourceSets.testmodC].each { sourceSet -> + dependencies { + testmodImplementation sourceSet.output + } + rootProject.dependencies { + testmodImplementation sourceSet.output + } + + tasks.register("${sourceSet.name}Jar", Jar) { + from sourceSet.output + archiveBaseName.set(sourceSet.name) + } +} + +rootProject.allprojects.each { p -> + if (p.extensions.findByName("loom") == null) { + return // Skip over the meta projects + } + + p.loom.mods.register("fabric-resource-loader-v0-testmod-a") { + sourceSet sourceSets.testmodA + } + p.loom.mods.register("fabric-resource-loader-v0-testmod-b") { + sourceSet sourceSets.testmodB + } + p.loom.mods.register("fabric-resource-loader-v0-testmod-c") { + sourceSet sourceSets.testmodC + } +} + +tasks.named("remapTestmodJar", net.fabricmc.loom.task.RemapJarTask) { + nestedJars.from(tasks.testmodAJar) + nestedJars.from(tasks.testmodBJar) + nestedJars.from(tasks.testmodCJar) + addNestedDependencies = true +} diff --git a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModNioResourcePack.java b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModNioResourcePack.java index 428a9faeb5..9c5a84a489 100644 --- a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModNioResourcePack.java +++ b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModNioResourcePack.java @@ -76,6 +76,7 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack { */ private final boolean modBundled; + @Nullable public static ModNioResourcePack create(String id, ModContainer mod, String subPath, ResourceType type, ResourcePackActivationType activationType, boolean modBundled) { List rootPaths = mod.getRootPaths(); List paths; diff --git a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackCreator.java b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackCreator.java index 50242fa976..dace950e41 100644 --- a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackCreator.java +++ b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackCreator.java @@ -16,7 +16,6 @@ package net.fabricmc.fabric.impl.resource.loader; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; @@ -35,6 +34,7 @@ import net.minecraft.text.Text; import net.fabricmc.fabric.api.resource.ModResourcePack; +import net.fabricmc.loader.api.FabricLoader; /** * Represents a resource pack provider for mods and built-in mods resource packs. @@ -134,8 +134,7 @@ public void register(Consumer consumer) { } private void registerModPack(Consumer consumer, @Nullable String subPath, Predicate> parents) { - List packs = new ArrayList<>(); - ModResourcePackUtil.appendModResourcePacks(packs, this.type, subPath); + List packs = ModResourcePackUtil.getModResourcePacks(FabricLoader.getInstance(), this.type, subPath); for (ModResourcePack pack : packs) { ResourcePackProfile profile = ResourcePackProfile.create( diff --git a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter.java b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter.java new file mode 100644 index 0000000000..9789cbea80 --- /dev/null +++ b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 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.resource.loader; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import net.fabricmc.fabric.api.resource.ModResourcePack; +import net.fabricmc.fabric.impl.base.toposort.NodeSorting; +import net.fabricmc.fabric.impl.base.toposort.SortableNode; + +public class ModResourcePackSorter { + private final Object lock = new Object(); + private ModResourcePack[] packs; + /** + * Registered load phases. + */ + private final Map phases = new LinkedHashMap<>(); + /** + * Phases sorted in the correct dependency order. + */ + private final List sortedPhases = new ArrayList<>(); + + ModResourcePackSorter() { + this.packs = new ModResourcePack[0]; + } + + public List getPacks() { + return Collections.unmodifiableList(Arrays.asList(this.packs)); + } + + public void addPack(ModResourcePack pack) { + Objects.requireNonNull(pack, "Can't register a null pack"); + + String modId = pack.getId(); + Objects.requireNonNull(modId, "Can't register a pack without a mod id"); + + synchronized (lock) { + getOrCreatePhase(modId, true).addPack(pack); + rebuildPackList(packs.length + 1); + } + } + + private LoadPhaseData getOrCreatePhase(String id, boolean sortIfCreate) { + LoadPhaseData phase = phases.get(id); + + if (phase == null) { + phase = new LoadPhaseData(id); + phases.put(id, phase); + sortedPhases.add(phase); + + if (sortIfCreate) { + NodeSorting.sort(sortedPhases, "mod resource packs", Comparator.comparing(data -> data.modId)); + } + } + + return phase; + } + + private void rebuildPackList(int newLength) { + // Rebuild pack list. + if (sortedPhases.size() == 1) { + // Special case with a single phase: use the array of the phase directly. + packs = sortedPhases.getFirst().packs; + } else { + ModResourcePack[] newHandlers = new ModResourcePack[newLength]; + int newHandlersIndex = 0; + + for (LoadPhaseData existingPhase : sortedPhases) { + int length = existingPhase.packs.length; + System.arraycopy(existingPhase.packs, 0, newHandlers, newHandlersIndex, length); + newHandlersIndex += length; + } + + packs = newHandlers; + } + } + + public void addLoadOrdering(String firstPhase, String secondPhase, ModResourcePackUtil.Order order) { + Objects.requireNonNull(firstPhase, "Tried to add an ordering for a null phase."); + Objects.requireNonNull(secondPhase, "Tried to add an ordering for a null phase."); + if (firstPhase.equals(secondPhase)) throw new IllegalArgumentException("Tried to add a phase that depends on itself."); + + synchronized (lock) { + LoadPhaseData first = getOrCreatePhase(firstPhase, false); + LoadPhaseData second = getOrCreatePhase(secondPhase, false); + + switch (order) { + case BEFORE -> LoadPhaseData.link(first, second); + case AFTER -> LoadPhaseData.link(second, first); + } + + NodeSorting.sort(this.sortedPhases, "event phases", Comparator.comparing(data -> data.modId)); + rebuildPackList(packs.length); + } + } + + public static class LoadPhaseData extends SortableNode { + final String modId; + ModResourcePack[] packs; + + LoadPhaseData(String modId) { + this.modId = modId; + this.packs = new ModResourcePack[0]; + } + + void addPack(ModResourcePack pack) { + int oldLength = packs.length; + packs = Arrays.copyOf(packs, oldLength + 1); + packs[oldLength] = pack; + } + + @Override + protected String getDescription() { + return modId; + } + } +} diff --git a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackUtil.java b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackUtil.java index 2f20ce58bd..bcf71d5aad 100644 --- a/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackUtil.java +++ b/fabric-resource-loader-v0/src/main/java/net/fabricmc/fabric/impl/resource/loader/ModResourcePackUtil.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; @@ -57,6 +58,7 @@ import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.api.metadata.ModMetadata; /** @@ -65,29 +67,77 @@ public final class ModResourcePackUtil { public static final Gson GSON = new Gson(); private static final Logger LOGGER = LoggerFactory.getLogger(ModResourcePackUtil.class); + private static final String LOAD_ORDER_KEY = "fabric:resource_load_order"; private ModResourcePackUtil() { } /** - * Appends mod resource packs to the given list. + * Returns a list of mod resource packs. * - * @param packs the resource pack list to append * @param type the type of resource * @param subPath the resource pack sub path directory in mods, may be {@code null} */ - public static void appendModResourcePacks(List packs, ResourceType type, @Nullable String subPath) { - for (ModContainer container : FabricLoader.getInstance().getAllMods()) { - if (container.getMetadata().getType().equals("builtin")) { + public static List getModResourcePacks(FabricLoader fabricLoader, ResourceType type, @Nullable String subPath) { + ModResourcePackSorter sorter = new ModResourcePackSorter(); + + Collection containers = fabricLoader.getAllMods(); + List allIds = containers.stream().map(ModContainer::getMetadata).map(ModMetadata::getId).toList(); + + for (ModContainer container : containers) { + ModMetadata metadata = container.getMetadata(); + String id = metadata.getId(); + + if (metadata.getType().equals("builtin")) { + continue; + } + + ModResourcePack pack = ModNioResourcePack.create(id, container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED, true); + + if (pack == null) { continue; } - ModResourcePack pack = ModNioResourcePack.create(container.getMetadata().getId(), container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED, true); + sorter.addPack(pack); + + CustomValue loadOrder = metadata.getCustomValue(LOAD_ORDER_KEY); + if (loadOrder == null) continue; + + if (loadOrder.getType() == CustomValue.CvType.OBJECT) { + CustomValue.CvObject object = loadOrder.getAsObject(); + + addLoadOrdering(object, allIds, sorter, Order.BEFORE, id); + addLoadOrdering(object, allIds, sorter, Order.AFTER, id); + } else { + LOGGER.error("[Fabric] Resource load order should be an object"); + } + } + + return sorter.getPacks(); + } + + public static void addLoadOrdering(CustomValue.CvObject object, List allIds, ModResourcePackSorter sorter, Order order, String currentId) { + List modIds = new ArrayList<>(); + + CustomValue array = object.get(order.jsonKey); + if (array == null) return; - if (pack != null) { - packs.add(pack); + switch (array.getType()) { + case STRING -> modIds.add(array.getAsString()); + case ARRAY -> { + for (CustomValue id : array.getAsArray()) { + if (id.getType() == CustomValue.CvType.STRING) { + modIds.add(id.getAsString()); + } } } + default -> { + LOGGER.error("[Fabric] {} should be a string or an array", order.jsonKey); + return; + } + } + + modIds.stream().filter(allIds::contains).forEach(modId -> sorter.addLoadOrdering(modId, currentId, order)); } public static void refreshAutoEnabledPacks(List enabledProfiles, Map allProfiles) { @@ -247,4 +297,15 @@ public static DataPackSettings createTestServerSettings(List enabled, Li public static ResourcePackManager createClientManager() { return new ResourcePackManager(new VanillaDataPackProvider(new SymlinkFinder((path) -> true)), new ModResourcePackCreator(ResourceType.SERVER_DATA, true)); } + + public enum Order { + BEFORE("before"), + AFTER("after"); + + private final String jsonKey; + + Order(String jsonKey) { + this.jsonKey = jsonKey; + } + } } diff --git a/fabric-resource-loader-v0/src/testmod/java/net/fabricmc/fabric/test/resource/loader/BuiltinPackSortingTest.java b/fabric-resource-loader-v0/src/testmod/java/net/fabricmc/fabric/test/resource/loader/BuiltinPackSortingTest.java new file mode 100644 index 0000000000..4e5a776493 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmod/java/net/fabricmc/fabric/test/resource/loader/BuiltinPackSortingTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 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.test.resource.loader; + +import net.minecraft.recipe.Recipe; +import net.minecraft.recipe.ServerRecipeManager; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.test.TestContext; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.gametest.v1.GameTest; + +public class BuiltinPackSortingTest { + private static final String MOD_ID = "fabric-resource-loader-v0-testmod"; + + private static RegistryKey> recipe(String path) { + return RegistryKey.of(RegistryKeys.RECIPE, Identifier.of(MOD_ID, path)); + } + + @GameTest + public void builtinPackSorting(TestContext context) { + ServerRecipeManager manager = context.getWorld().getRecipeManager(); + + if (manager.get(recipe("disabled_by_b")).isPresent()) { + throw context.createError(Text.literal("disabled_by_b recipe should not have been loaded.")); + } + + if (manager.get(recipe("disabled_by_c")).isPresent()) { + throw context.createError(Text.literal("disabled_by_c recipe should not have been loaded.")); + } + + if (manager.get(recipe("enabled_by_c")).isEmpty()) { + throw context.createError(Text.literal("enabled_by_c recipe should have been loaded.")); + } + + long loadedRecipes = manager.values().stream().filter(r -> r.id().getValue().getNamespace().equals(MOD_ID)).count(); + context.assertTrue(loadedRecipes == 1, Text.literal("Unexpected loaded recipe count: " + loadedRecipes)); + context.complete(); + } +} diff --git a/fabric-resource-loader-v0/src/testmod/resources/fabric.mod.json b/fabric-resource-loader-v0/src/testmod/resources/fabric.mod.json index a8456fa2b8..dc26298593 100644 --- a/fabric-resource-loader-v0/src/testmod/resources/fabric.mod.json +++ b/fabric-resource-loader-v0/src/testmod/resources/fabric.mod.json @@ -6,7 +6,10 @@ "environment": "*", "license": "Apache-2.0", "depends": { - "fabric-resource-loader-v0": "*" + "fabric-resource-loader-v0": "*", + "fabric-resource-loader-v0-testmod-a": "*", + "fabric-resource-loader-v0-testmod-b": "*", + "fabric-resource-loader-v0-testmod-c": "*" }, "entrypoints": { "main": [ @@ -16,6 +19,9 @@ ], "server": [ "net.fabricmc.fabric.test.resource.loader.LanguageTestMod" + ], + "fabric-gametest": [ + "net.fabricmc.fabric.test.resource.loader.BuiltinPackSortingTest" ] }, "mixins": [ diff --git a/fabric-resource-loader-v0/src/testmodA/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json b/fabric-resource-loader-v0/src/testmodA/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json new file mode 100644 index 0000000000..dc912cc8a9 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodA/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "minecraft:acacia_slab" + ], + "result": { + "id": "minecraft:acacia_boat", + "count": 1 + } +} diff --git a/fabric-resource-loader-v0/src/testmodA/resources/fabric.mod.json b/fabric-resource-loader-v0/src/testmodA/resources/fabric.mod.json new file mode 100644 index 0000000000..081d7ce850 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodA/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "fabric-resource-loader-v0-testmod-a", + "name": "Fabric Resource Loader (v0) Test Mod A", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-resource-loader-v0": "*" + }, + "custom": { + "fabric:resource_load_order": { + "before": [ + "fabric-resource-loader-v0-testmod-b", + "mod-that-does-not-exist" + ], + "after": [ + "mod-that-also-does-not-exist" + ] + } + } +} diff --git a/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json new file mode 100644 index 0000000000..9fc0e0f020 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_b.json @@ -0,0 +1,10 @@ +{ + "fabric:load_conditions": [ + { + "condition": "fabric:not", + "value": { + "condition": "fabric:true" + } + } + ] +} diff --git a/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json new file mode 100644 index 0000000000..ec0b2bfe07 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "minecraft:birch_slab" + ], + "result": { + "id": "minecraft:birch_boat", + "count": 1 + } +} diff --git a/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json new file mode 100644 index 0000000000..9fc0e0f020 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodB/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json @@ -0,0 +1,10 @@ +{ + "fabric:load_conditions": [ + { + "condition": "fabric:not", + "value": { + "condition": "fabric:true" + } + } + ] +} diff --git a/fabric-resource-loader-v0/src/testmodB/resources/fabric.mod.json b/fabric-resource-loader-v0/src/testmodB/resources/fabric.mod.json new file mode 100644 index 0000000000..57d663acc2 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodB/resources/fabric.mod.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "id": "fabric-resource-loader-v0-testmod-b", + "name": "Fabric Resource Loader (v0) Test Mod B", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-resource-loader-v0": "*" + } +} diff --git a/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json b/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json new file mode 100644 index 0000000000..9fc0e0f020 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/disabled_by_c.json @@ -0,0 +1,10 @@ +{ + "fabric:load_conditions": [ + { + "condition": "fabric:not", + "value": { + "condition": "fabric:true" + } + } + ] +} diff --git a/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json b/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json new file mode 100644 index 0000000000..348af2d561 --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodC/resources/data/fabric-resource-loader-v0-testmod/recipe/enabled_by_c.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "minecraft:oak_slab" + ], + "result": { + "id": "minecraft:oak_boat", + "count": 1 + } +} diff --git a/fabric-resource-loader-v0/src/testmodC/resources/fabric.mod.json b/fabric-resource-loader-v0/src/testmodC/resources/fabric.mod.json new file mode 100644 index 0000000000..dd2fbb2bda --- /dev/null +++ b/fabric-resource-loader-v0/src/testmodC/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "fabric-resource-loader-v0-testmod-c", + "name": "Fabric Resource Loader (v0) Test Mod C", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-resource-loader-v0": "*" + }, + "custom": { + "fabric:resource_load_order": { + "after": "fabric-resource-loader-v0-testmod-b" + } + } +}