Implement builtin mod resource/data pack sorting ()

* builtin pack sorting 1.21.5

* Fix switch case indentation

* Revert "Fix switch case indentation"

This reverts commit 6714db0fa0.

* Fix formatting
This commit is contained in:
Apollo 2025-02-15 09:39:31 -05:00 committed by GitHub
parent 216530c8b2
commit 504c6f623c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 432 additions and 12 deletions
fabric-resource-loader-v0
build.gradle
src
main/java/net/fabricmc/fabric/impl/resource/loader
testmod
java/net/fabricmc/fabric/test/resource/loader
resources
testmodA/resources
data/fabric-resource-loader-v0-testmod/recipe
fabric.mod.json
testmodB/resources
testmodC/resources
data/fabric-resource-loader-v0-testmod/recipe
fabric.mod.json

View file

@ -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
}

View file

@ -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<Path> rootPaths = mod.getRootPaths();
List<Path> paths;

View file

@ -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.resource.ResourceType;
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 class ModResourcePackCreator implements ResourcePackProvider {
}
private void registerModPack(Consumer<ResourcePackProfile> consumer, @Nullable String subPath, Predicate<Set<String>> parents) {
List<ModResourcePack> packs = new ArrayList<>();
ModResourcePackUtil.appendModResourcePacks(packs, this.type, subPath);
List<ModResourcePack> packs = ModResourcePackUtil.getModResourcePacks(FabricLoader.getInstance(), this.type, subPath);
for (ModResourcePack pack : packs) {
ResourcePackProfile profile = ResourcePackProfile.create(

View file

@ -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<String, LoadPhaseData> phases = new LinkedHashMap<>();
/**
* Phases sorted in the correct dependency order.
*/
private final List<LoadPhaseData> sortedPhases = new ArrayList<>();
ModResourcePackSorter() {
this.packs = new ModResourcePack[0];
}
public List<ModResourcePack> 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<LoadPhaseData> {
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;
}
}
}

View file

@ -21,6 +21,7 @@ import java.io.InputStream;
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.ModResourcePack;
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 @@ import net.fabricmc.loader.api.metadata.ModMetadata;
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<ModResourcePack> packs, ResourceType type, @Nullable String subPath) {
for (ModContainer container : FabricLoader.getInstance().getAllMods()) {
if (container.getMetadata().getType().equals("builtin")) {
public static List<ModResourcePack> getModResourcePacks(FabricLoader fabricLoader, ResourceType type, @Nullable String subPath) {
ModResourcePackSorter sorter = new ModResourcePackSorter();
Collection<ModContainer> containers = fabricLoader.getAllMods();
List<String> 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(container.getMetadata().getId(), container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED, true);
ModResourcePack pack = ModNioResourcePack.create(id, container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED, true);
if (pack != null) {
packs.add(pack);
if (pack == null) {
continue;
}
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<String> allIds, ModResourcePackSorter sorter, Order order, String currentId) {
List<String> modIds = new ArrayList<>();
CustomValue array = object.get(order.jsonKey);
if (array == null) return;
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<ResourcePackProfile> enabledProfiles, Map<String, ResourcePackProfile> allProfiles) {
@ -247,4 +297,15 @@ public final class ModResourcePackUtil {
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;
}
}
}

View file

@ -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<?>> 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();
}
}

View file

@ -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": [

View file

@ -0,0 +1,11 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
"minecraft:acacia_slab"
],
"result": {
"id": "minecraft:acacia_boat",
"count": 1
}
}

View file

@ -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"
]
}
}
}

View file

@ -0,0 +1,10 @@
{
"fabric:load_conditions": [
{
"condition": "fabric:not",
"value": {
"condition": "fabric:true"
}
}
]
}

View file

@ -0,0 +1,11 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
"minecraft:birch_slab"
],
"result": {
"id": "minecraft:birch_boat",
"count": 1
}
}

View file

@ -0,0 +1,10 @@
{
"fabric:load_conditions": [
{
"condition": "fabric:not",
"value": {
"condition": "fabric:true"
}
}
]
}

View file

@ -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": "*"
}
}

View file

@ -0,0 +1,10 @@
{
"fabric:load_conditions": [
{
"condition": "fabric:not",
"value": {
"condition": "fabric:true"
}
}
]
}

View file

@ -0,0 +1,11 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
"minecraft:oak_slab"
],
"result": {
"id": "minecraft:oak_boat",
"count": 1
}
}

View file

@ -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"
}
}
}