Refactor resource loader internals (#3473)

* First step toward fixing resource pack grouping

* Placeholder pack and pack dependency

* Various fixes

* Fix wrong variable in serialization code

* Hide packs in PackScreen and DatapackCommand

* Apparently Japanese people aren't alone in having their currency signs used as special chars...

* Inject directly to Pack

* Add temporary logging, fix bug

* Add proper sorting

* Improve logging

* Fix duplicate name registration

* Fix client pack handling

* Fix FMJ

* Stop using interface injection for internal interface

* Delete unused GroupResourcePack

* Move refreshAutoEnabledPacks to util

* Improve logging

* Make a few things private

* Use vanilla metadata serialization logic

* Improve javadoc

* Add junit test

* Some final refactors

* Update ja_jp.json

---------

Co-authored-by: modmuss <modmuss50@gmail.com>
(cherry picked from commit 707e4d1bfd)
This commit is contained in:
apple502j 2024-01-28 21:55:54 +09:00 committed by modmuss50
parent bbf5165de7
commit 3feeb09499
29 changed files with 931 additions and 589 deletions

View file

@ -1,63 +0,0 @@
/*
* 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.client.resource.loader;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import net.minecraft.resource.AbstractFileResourcePack;
import net.minecraft.resource.InputSupplier;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.metadata.ResourceMetadataReader;
import net.fabricmc.fabric.api.resource.ModResourcePack;
import net.fabricmc.fabric.impl.resource.loader.GroupResourcePack;
/**
* Represents a vanilla built-in resource pack with support for modded content.
*
* <p>Vanilla resources are provided as usual through the original resource pack (if not overridden),
* all other resources will be searched for in the provided modded resource packs.</p>
*/
public class FabricWrappedVanillaResourcePack extends GroupResourcePack {
private final AbstractFileResourcePack originalResourcePack;
public FabricWrappedVanillaResourcePack(AbstractFileResourcePack originalResourcePack, List<ModResourcePack> modResourcePacks) {
// Mod resource packs have higher priority, add them last (so vanilla assets can be overridden)
super(ResourceType.CLIENT_RESOURCES, Stream.concat(Stream.of(originalResourcePack), modResourcePacks.stream()).toList());
this.originalResourcePack = originalResourcePack;
}
@Override
public InputSupplier<InputStream> openRoot(String... pathSegments) {
return this.originalResourcePack.openRoot(pathSegments);
}
@Override
public <T> @Nullable T parseMetadata(ResourceMetadataReader<T> metaReader) throws IOException {
return this.originalResourcePack.parseMetadata(metaReader);
}
@Override
public String getName() {
return this.originalResourcePack.getName();
}
}

View file

@ -1,77 +0,0 @@
/*
* 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.mixin.resource.loader.client;
import java.util.ArrayList;
import java.util.List;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import net.minecraft.client.resource.DefaultClientResourcePackProvider;
import net.minecraft.resource.AbstractFileResourcePack;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourcePackSource;
import net.minecraft.resource.ResourceType;
import net.minecraft.text.Text;
import net.fabricmc.fabric.api.resource.ModResourcePack;
import net.fabricmc.fabric.impl.client.resource.loader.FabricWrappedVanillaResourcePack;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
@Mixin(DefaultClientResourcePackProvider.class)
public class DefaultClientResourcePackProviderMixin {
/**
* Injects into the method which registers/creates vanilla built-in resource packs,
* and replaces the local {@link net.minecraft.resource.ResourcePackProfile.PackFactory}
* instance with our custom wrapper that supports loading from mods.
*/
@ModifyArg(
method = "create(Ljava/lang/String;Lnet/minecraft/resource/ResourcePackProfile$PackFactory;Lnet/minecraft/text/Text;)Lnet/minecraft/resource/ResourcePackProfile;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/resource/ResourcePackProfile;create(Ljava/lang/String;Lnet/minecraft/text/Text;ZLnet/minecraft/resource/ResourcePackProfile$PackFactory;Lnet/minecraft/resource/ResourceType;Lnet/minecraft/resource/ResourcePackProfile$InsertionPosition;Lnet/minecraft/resource/ResourcePackSource;)Lnet/minecraft/resource/ResourcePackProfile;"
),
index = 3
)
private ResourcePackProfile.PackFactory onCreateVanillaBuiltinResourcePack(String name, Text displayName, boolean alwaysEnabled,
ResourcePackProfile.PackFactory packFactory, ResourceType type, ResourcePackProfile.InsertionPosition position, ResourcePackSource source) {
return new ResourcePackProfile.PackFactory() {
@Override
public ResourcePack open(String name) {
return new FabricWrappedVanillaResourcePack((AbstractFileResourcePack) packFactory.open(name), getModResourcePacks(name));
}
@Override
public ResourcePack openWithOverlays(String string, ResourcePackProfile.Metadata metadata) {
// VanillaResourcePackProvider does not handle overlays
return open(name);
}
};
}
/**
* {@return all baked-in mod resource packs that provide resources in the specified subPath}.
*/
private static List<ModResourcePack> getModResourcePacks(String subPath) {
List<ModResourcePack> packs = new ArrayList<>();
ModResourcePackUtil.appendModResourcePacks(packs, ResourceType.CLIENT_RESOURCES, subPath);
return packs;
}
}

View file

@ -99,7 +99,7 @@ public class GameOptionsMixin {
for (ResourcePackProfile profile : profiles) {
// Always add "Fabric Mods" pack to enabled resource packs.
if (profile.getSource() == ModResourcePackCreator.RESOURCE_PACK_SOURCE) {
if (profile.getName().equals(ModResourcePackCreator.FABRIC)) {
resourcePacks.add(profile.getName());
continue;
}

View file

@ -0,0 +1,63 @@
/*
* 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.mixin.resource.loader.client;
import java.util.ArrayList;
import java.util.List;
import com.llamalad7.mixinextras.sugar.Local;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import net.minecraft.client.MinecraftClient;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
/**
* Mixins to the anonymous class in #write method.
*/
@Mixin(targets = "net/minecraft/client/option/GameOptions$3")
public class GameOptionsWriteVisitorMixin {
@Unique
private static List<String> toPackListString(List<String> packs) {
List<String> copy = new ArrayList<>(packs.size());
ResourcePackManager manager = MinecraftClient.getInstance().getResourcePackManager();
for (String pack : packs) {
ResourcePackProfile profile = manager.getProfile(pack);
// Nonexistent pack profiles should be handled in the same way as vanilla
if (profile == null || !((FabricResourcePackProfile) profile).fabric_isHidden()) copy.add(pack);
}
return copy;
}
@SuppressWarnings("unchecked")
@ModifyArg(method = "visitObject", at = @At(value = "INVOKE", target = "Ljava/util/function/Function;apply(Ljava/lang/Object;)Ljava/lang/Object;"))
private <T> T skipHiddenPacks(T value, @Local String key) {
if ("resourcePacks".equals(key) && value instanceof List) {
return (T) toPackListString((List<String>) value);
}
return value;
}
}

View file

@ -0,0 +1,61 @@
/*
* 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.mixin.resource.loader.client;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.spongepowered.asm.mixin.Final;
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 net.minecraft.client.gui.screen.pack.ResourcePackOrganizer;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
@Mixin(ResourcePackOrganizer.class)
public class ResourcePackOrganizerMixin {
@Shadow
@Final
List<ResourcePackProfile> enabledPacks;
@Shadow
@Final
List<ResourcePackProfile> disabledPacks;
/**
* Do not list hidden packs in either enabledPacks or disabledPacks.
* They are managed entirely by ResourcePackManager on save, and are invisible to client.
*/
@Inject(method = "<init>", at = @At("TAIL"))
private void removeHiddenPacksInit(Runnable updateCallback, Function iconIdSupplier, ResourcePackManager resourcePackManager, Consumer applier, CallbackInfo ci) {
this.enabledPacks.removeIf(profile -> ((FabricResourcePackProfile) profile).fabric_isHidden());
this.disabledPacks.removeIf(profile -> ((FabricResourcePackProfile) profile).fabric_isHidden());
}
@Inject(method = "refresh", at = @At("TAIL"))
private void removeHiddenPacksRefresh(CallbackInfo ci) {
this.enabledPacks.removeIf(profile -> ((FabricResourcePackProfile) profile).fabric_isHidden());
this.disabledPacks.removeIf(profile -> ((FabricResourcePackProfile) profile).fabric_isHidden());
}
}

View file

@ -3,12 +3,13 @@
"package": "net.fabricmc.fabric.mixin.resource.loader.client",
"compatibilityLevel": "JAVA_17",
"client": [
"VanillaResourcePackProviderMixin",
"DefaultClientResourcePackProviderMixin",
"CreateWorldScreenMixin",
"FontManagerMixin",
"GameOptionsMixin",
"KeyedResourceReloadListenerClientMixin"
"KeyedResourceReloadListenerClientMixin",
"ResourcePackOrganizerMixin",
"VanillaResourcePackProviderMixin",
"GameOptionsWriteVisitorMixin"
],
"injectors": {
"defaultRequire": 1

View file

@ -29,4 +29,6 @@ public interface ModResourcePack extends ResourcePack {
* resource pack.
*/
ModMetadata getFabricModMetadata();
ModResourcePack createOverlay(String overlay);
}

View file

@ -17,27 +17,48 @@
/**
* The Resource Loader, version 0.
*
* <p><h3>Quick note about vocabulary in Resource Loader and Minecraft:</h3>
* <ul>
* <li>Resource Pack refers to both client-sided resource pack and data pack.</li>
* <li>Virtual Resource Pack refers to a resource pack that may be generated at runtime, or simply doesn't exist directly on disk.</li>
* <li>Group Resource Pack refers to a virtual resource pack that groups multiple resource packs together.</li>
* </ul>
* </p>
* <p><h3>Quick note about vocabulary in Minecraft:</h3></p>
* <p>Resource Pack refers to both client-sided resource pack and data pack.</p>
*
* <p><h3>Modded Resource Pack Handling</h3></p>
* <p>The Resource Loader will create a resource pack for each mod that provides resources in {@code assets} or {@code data}
* sub-directories.
* Those mod resource packs are grouped into a single always-enabled group resource pack which is shown in the resource pack screen.</p>
* <p>There are two types of resource packs that mods can provide.</p>
*
* <p><h4>Bundled Resource Pack</h4></p>
* <p>Mods can "bundle" resource packs with the mod by putting files inside the {@code assets} or {@code data}
* sub-directories of the {@code resources} directory. They are always enabled, initially loaded after the vanilla pack,
* and cannot be disabled. Individual mods' packs are hidden to users in the Resource Pack option screen or the {@code /datapack}
* command. Instead, a placeholder pack, named "Fabric Mods", will be displayed. This pack, with the ID {@code fabric}, provides
* no resource itself; it is merely a marker used internally.</p>
*
* <p><h4>Built-in Mod Resource Pack</h4></p>
* <p>The Resource Loader adds manually registered mod resource packs. Those resource packs are registered with
* {@link net.fabricmc.fabric.api.resource.ResourceManagerHelper#registerBuiltinResourcePack(net.minecraft.util.Identifier, net.fabricmc.loader.api.ModContainer, net.fabricmc.fabric.api.resource.ResourcePackActivationType)}</p>
* <p>The Resource Loader adds manually registered mod resource packs. Those resource packs are located inside the
* {@code resources/resourcepacks/} directory. For example, a built-in data pack with the ID {@code example:test} should be placed
* under {@code resources/resourcepacks/test/data/}. The packs are then registered with
* {@link net.fabricmc.fabric.api.resource.ResourceManagerHelper#registerBuiltinResourcePack(net.minecraft.util.Identifier, net.fabricmc.loader.api.ModContainer, net.fabricmc.fabric.api.resource.ResourcePackActivationType)}.
* Users can manually enable or disable the packs, unless it is specified to be always enabled.</p>
*
* <p><h4>Vanilla Built-in Resource Packs</h4></p>
* <p>The Resource Loader will inject resources into the Programmer Art and High Contrast resource packs for each mod
* that provides resources in the {@code programmer_art} or {@code high_contrast} top-level directory of the mod
* whose structure is similar to a normal resource pack.</p>
* <p><h4>Programmer Art and High Contrast Support</h4></p>
* <p>Bundled resource packs support Programmer Art and High Contrast vanilla resource packs. Simply place assets
* under {@code resources/programmer_art/assets/} or {@code resources/high_contrast/assets/}, respectively.
* Internally, these are treated as a separate internal pack, loaded just after the respective vanilla pack.
* Toggling the vanilla packs automatically toggles the bundled ones as well; you cannot separately enable or disable them.</p>
*
* <p><h4>Example</h4></p>
* <p>Mod A ({@code mod_a} provides a bundled resource pack with both Programmer Art and High Contrast support.
* Mod B ({@code mod_b}) provides a bundled resource pack and one built-in resource pack, Extra ({@code mod_b:extra}).
* When neither the Programmer Art nor High Contrast is enabled, the user sees "Vanilla", "Fabric Mods", and "Extra" in the
* Resource Packs screen. Internally, between Fabric Mods and Extra packs, two hidden packs exist: {@code mod_a} and {@code mod_b}.</p>
*
* <p>Suppose the user then enables both the Programmer Art and High Contrast, and the Resource Packs screen lists
* "Vanilla", "Fabric", Programmer Art, "Extra", and High Contrast. Internally, there are 4 hidden packs:</p>
*
* <ul>
* <li>{@code mod_a} and {@code mod_b} between "Fabric" and Programmer Art.</li>
* <li>{@code mod_a_programmer_art}, containing Mod A's Programmer Art assets, just after Programmer Art pack.</li>
* <li>{@code mod_a_high_contrast}, containing Mod A's High Contrast assets, just after High Contrast pack.</li>
* </ul>
*
* <p>Note that while the current behavior is to sort bundled resource packs by mod ID in descending order (A to Z), this may change over time.</p>
*
* <p><h3>Resource Reload Listener</h3></p>
* <p>The Resource Loader allows mods to register resource reload listeners through

View file

@ -1,86 +0,0 @@
/*
* 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.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.List;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import net.minecraft.SharedConstants;
import net.minecraft.resource.AbstractFileResourcePack;
import net.minecraft.resource.InputSupplier;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.metadata.ResourceMetadataReader;
import net.fabricmc.fabric.api.resource.ModResourcePack;
import net.fabricmc.loader.api.FabricLoader;
/**
* The Fabric mods resource pack, holds all the mod resource packs as one pack.
*/
public class FabricModResourcePack extends GroupResourcePack {
public FabricModResourcePack(ResourceType type, List<ModResourcePack> packs) {
super(type, packs);
}
@Override
public InputSupplier<InputStream> openRoot(String... pathSegments) {
String fileName = String.join("/", pathSegments);
if ("pack.mcmeta".equals(fileName)) {
String description = "pack.description.modResources";
String fallback = "Mod resources.";
String pack = String.format("{\"pack\":{\"pack_format\":" + SharedConstants.getGameVersion().getResourceVersion(type) + ",\"description\":{\"translate\":\"%s\",\"fallback\":\"%s.\"}}}", description, fallback);
return () -> IOUtils.toInputStream(pack, Charsets.UTF_8);
} else if ("pack.png".equals(fileName)) {
return FabricLoader.getInstance().getModContainer("fabric-resource-loader-v0")
.flatMap(container -> container.getMetadata().getIconPath(512).flatMap(container::findPath))
.map(path -> (InputSupplier<InputStream>) (() -> Files.newInputStream(path)))
.orElse(null);
}
return null;
}
@Override
public <T> @Nullable T parseMetadata(ResourceMetadataReader<T> metaReader) throws IOException {
InputSupplier<InputStream> inputSupplier = this.openRoot("pack.mcmeta");
if (inputSupplier != null) {
try (InputStream input = inputSupplier.get()) {
return AbstractFileResourcePack.parseMetadata(metaReader, input);
}
} else {
return null;
}
}
@Override
public String getName() {
return "fabric";
}
@Override
public boolean isAlwaysStable() {
return true;
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.Set;
import java.util.function.Predicate;
/**
* Fabric addition to ResourcePackProfile.
* @see ModResourcePackCreator
*/
public interface FabricResourcePackProfile {
/**
* Returns whether the pack is internal and hidden from end users.
*/
default boolean fabric_isHidden() {
return false;
}
/**
* Returns whether every parent is enabled. If this is not empty, the pack's status
* is synced to that of the parent pack(s), where the pack gets enabled if and only
* if each of the parent is enabled. Note that non-Fabric packs always return {@code true}.
*
* @return whether every parent is enabled.
*/
default boolean fabric_parentsEnabled(Set<String> enabled) {
return true;
}
default void fabric_setParentsPredicate(Predicate<Set<String>> predicate) {
}
}

View file

@ -1,123 +0,0 @@
/*
* 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.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.resource.InputSupplier;
import net.minecraft.resource.NamespaceResourceManager;
import net.minecraft.resource.Resource;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.metadata.ResourceMetadata;
import net.minecraft.util.Identifier;
/**
* Represents a group resource pack, holds multiple resource packs as one.
*/
public abstract class GroupResourcePack implements ResourcePack {
protected final ResourceType type;
protected final List<? extends ResourcePack> packs;
protected final Map<String, List<ResourcePack>> namespacedPacks = new Object2ObjectOpenHashMap<>();
public GroupResourcePack(ResourceType type, List<? extends ResourcePack> packs) {
this.type = type;
this.packs = packs;
this.packs.forEach(pack -> pack.getNamespaces(this.type)
.forEach(namespace -> this.namespacedPacks.computeIfAbsent(namespace, value -> new ArrayList<>())
.add(pack)));
}
@Override
public InputSupplier<InputStream> open(ResourceType type, Identifier id) {
List<? extends ResourcePack> packs = this.namespacedPacks.get(id.getNamespace());
if (packs != null) {
// Last to first, since higher priority packs are at the end
for (int i = packs.size() - 1; i >= 0; i--) {
ResourcePack pack = packs.get(i);
InputSupplier<InputStream> supplier = pack.open(type, id);
if (supplier != null) {
return supplier;
}
}
}
return null;
}
@Override
public void findResources(ResourceType type, String namespace, String prefix, ResultConsumer consumer) {
List<? extends ResourcePack> packs = this.namespacedPacks.get(namespace);
if (packs == null) {
return;
}
// First to last, since later calls override previously returned data
for (ResourcePack pack : packs) {
pack.findResources(type, namespace, prefix, consumer);
}
}
@Override
public Set<String> getNamespaces(ResourceType type) {
return this.namespacedPacks.keySet();
}
public void appendResources(ResourceType type, Identifier id, List<Resource> resources) {
List<? extends ResourcePack> packs = this.namespacedPacks.get(id.getNamespace());
if (packs == null) {
return;
}
Identifier metadataId = NamespaceResourceManager.getMetadataPath(id);
// Last to first, since higher priority packs are at the end
for (int i = packs.size() - 1; i >= 0; i--) {
ResourcePack pack = packs.get(i);
InputSupplier<InputStream> supplier = pack.open(type, id);
if (supplier != null) {
InputSupplier<ResourceMetadata> metadataSupplier = () -> {
InputSupplier<InputStream> rawMetadataSupplier = pack.open(this.type, metadataId);
return rawMetadataSupplier != null ? NamespaceResourceManager.loadMetadata(rawMetadataSupplier) : ResourceMetadata.NONE;
};
resources.add(new Resource(pack, supplier, metadataSupplier));
}
}
}
public String getFullName() {
return this.getName() + " (" + this.packs.stream().map(ResourcePack::getName).collect(Collectors.joining(", ")) + ")";
}
@Override
public void close() {
this.packs.forEach(ResourcePack::close);
}
}

View file

@ -33,6 +33,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
@ -59,14 +60,18 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
private static final FileSystem DEFAULT_FS = FileSystems.getDefault();
private final String id;
private final ModMetadata modInfo;
private final ModContainer mod;
private final List<Path> basePaths;
private final ResourceType type;
private final AutoCloseable closer;
private final ResourcePackActivationType activationType;
private final Map<ResourceType, Set<String>> namespaces;
/**
* Whether the pack is bundled and loaded by default, as opposed to registered built-in packs.
* @see ModResourcePackUtil#appendModResourcePacks(List, ResourceType, String)
*/
private final boolean modBundled;
public static ModNioResourcePack create(String id, ModContainer mod, String subPath, ResourceType type, ResourcePackActivationType activationType) {
public static ModNioResourcePack create(String id, ModContainer mod, String subPath, ResourceType type, ResourcePackActivationType activationType, boolean modBundled) {
List<Path> rootPaths = mod.getRootPaths();
List<Path> paths;
@ -89,19 +94,27 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
if (paths.isEmpty()) return null;
ModNioResourcePack ret = new ModNioResourcePack(id, mod.getMetadata(), paths, type, null, activationType);
ModNioResourcePack ret = new ModNioResourcePack(id, mod, paths, type, activationType, modBundled);
return ret.getNamespaces(type).isEmpty() ? null : ret;
}
private ModNioResourcePack(String id, ModMetadata modInfo, List<Path> paths, ResourceType type, AutoCloseable closer, ResourcePackActivationType activationType) {
private ModNioResourcePack(String id, ModContainer mod, List<Path> paths, ResourceType type, ResourcePackActivationType activationType, boolean modBundled) {
this.id = id;
this.modInfo = modInfo;
this.mod = mod;
this.basePaths = paths;
this.type = type;
this.closer = closer;
this.activationType = activationType;
this.namespaces = readNamespaces(paths, modInfo.getId());
this.modBundled = modBundled;
this.namespaces = readNamespaces(paths, mod.getMetadata().getId());
}
@Override
public ModNioResourcePack createOverlay(String overlay) {
// See DirectoryResourcePack.
return new ModNioResourcePack(id, mod, basePaths.stream().map(
path -> path.resolve(overlay)
).toList(), type, activationType, modBundled);
}
static Map<ResourceType, Set<String>> readNamespaces(List<Path> paths, String modId) {
@ -188,8 +201,8 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
return () -> Files.newInputStream(path);
}
if (ModResourcePackUtil.containsDefault(this.modInfo, filename)) {
return () -> ModResourcePackUtil.openDefault(this.modInfo, this.type, filename);
if (ModResourcePackUtil.containsDefault(filename, this.modBundled)) {
return () -> ModResourcePackUtil.openDefault(this.mod, this.type, filename);
}
return null;
@ -239,7 +252,7 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
}
});
} catch (IOException e) {
LOGGER.warn("findResources at " + path + " in namespace " + namespace + ", mod " + modInfo.getId() + " failed!", e);
LOGGER.warn("findResources at " + path + " in namespace " + namespace + ", mod " + mod.getMetadata().getId() + " failed!", e);
}
}
}
@ -251,25 +264,18 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
@Override
public <T> T parseMetadata(ResourceMetadataReader<T> metaReader) throws IOException {
try (InputStream is = openFile("pack.mcmeta").get()) {
try (InputStream is = Objects.requireNonNull(openFile("pack.mcmeta")).get()) {
return AbstractFileResourcePack.parseMetadata(metaReader, is);
}
}
@Override
public void close() {
if (closer != null) {
try {
closer.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
public ModMetadata getFabricModMetadata() {
return modInfo;
return mod.getMetadata();
}
public ResourcePackActivationType getActivationType() {
@ -281,6 +287,11 @@ public class ModNioResourcePack implements ResourcePack, ModResourcePack {
return id;
}
@Override
public boolean isAlwaysStable() {
return this.modBundled;
}
private static boolean exists(Path path) {
// NIO Files.exists is notoriously slow when checking the file system
return path.getFileSystem() == DEFAULT_FS ? path.toFile().exists() : Files.exists(path);

View file

@ -18,15 +18,17 @@ package net.fabricmc.fabric.impl.resource.loader;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import net.minecraft.resource.OverlayResourcePack;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourcePackProvider;
import net.minecraft.resource.ResourcePackSource;
import net.minecraft.resource.ResourceType;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.fabricmc.fabric.api.resource.ModResourcePack;
@ -35,6 +37,22 @@ import net.fabricmc.fabric.api.resource.ModResourcePack;
* Represents a resource pack provider for mods and built-in mods resource packs.
*/
public class ModResourcePackCreator implements ResourcePackProvider {
/**
* The ID of the root resource pack profile for bundled packs.
*/
public static final String FABRIC = "fabric";
private static final String PROGRAMMER_ART = "programmer_art";
private static final String HIGH_CONTRAST = "high_contrast";
public static final Set<String> POST_CHANGE_HANDLE_REQUIRED = Set.of(FABRIC, PROGRAMMER_ART, HIGH_CONTRAST);
@VisibleForTesting
public static final Predicate<Set<String>> BASE_PARENT = enabled -> enabled.contains(FABRIC);
@VisibleForTesting
public static final Predicate<Set<String>> PROGRAMMER_ART_PARENT = enabled -> enabled.contains(FABRIC) && enabled.contains(PROGRAMMER_ART);
@VisibleForTesting
public static final Predicate<Set<String>> HIGH_CONTRAST_PARENT = enabled -> enabled.contains(FABRIC) && enabled.contains(HIGH_CONTRAST);
/**
* This can be used to check if a pack profile is for mod-provided packs.
*/
public static final ResourcePackSource RESOURCE_PACK_SOURCE = new ResourcePackSource() {
@Override
public Text decorate(Text packName) {
@ -72,49 +90,51 @@ public class ModResourcePackCreator implements ResourcePackProvider {
4. User resource packs
*/
consumer.accept(ResourcePackProfile.create(
FABRIC,
Text.translatable("pack.name.fabricMods"),
true,
new PlaceholderResourcePack.Factory(this.type),
this.type,
ResourcePackProfile.InsertionPosition.TOP,
RESOURCE_PACK_SOURCE
));
// Build a list of mod resource packs.
List<ModResourcePack> packs = new ArrayList<>();
ModResourcePackUtil.appendModResourcePacks(packs, type, null);
registerModPack(consumer, null, BASE_PARENT);
if (!packs.isEmpty()) {
// Make the resource pack profile for mod resource packs.
// Mod resource packs must always be enabled to avoid issues, and they are inserted
// on top to ensure that they are applied after vanilla built-in resource packs.
MutableText title = Text.translatable("pack.name.fabricMods");
ResourcePackProfile resourcePackProfile = ResourcePackProfile.create("fabric", title, true, new ResourcePackProfile.PackFactory() {
@Override
public ResourcePack open(String name) {
return new FabricModResourcePack(type, packs);
}
@Override
public ResourcePack openWithOverlays(String name, ResourcePackProfile.Metadata metadata) {
final ResourcePack basePack = open(name);
final List<String> overlays = metadata.overlays();
if (overlays.isEmpty()) {
return basePack;
}
final List<ResourcePack> overlayedPacks = new ArrayList<>(overlays.size());
for (String overlay : overlays) {
List<ModResourcePack> innerPacks = new ArrayList<>();
ModResourcePackUtil.appendModResourcePacks(innerPacks, type, overlay);
overlayedPacks.add(new FabricModResourcePack(type, innerPacks));
}
return new OverlayResourcePack(basePack, overlayedPacks);
}
}, type, ResourcePackProfile.InsertionPosition.TOP, RESOURCE_PACK_SOURCE);
if (resourcePackProfile != null) {
consumer.accept(resourcePackProfile);
}
if (this.type == ResourceType.CLIENT_RESOURCES) {
// Programmer Art/High Contrast data packs can never be enabled.
registerModPack(consumer, PROGRAMMER_ART, PROGRAMMER_ART_PARENT);
registerModPack(consumer, HIGH_CONTRAST, HIGH_CONTRAST_PARENT);
}
// Register all built-in resource packs provided by mods.
ResourceManagerHelperImpl.registerBuiltinResourcePacks(this.type, consumer);
}
private void registerModPack(Consumer<ResourcePackProfile> consumer, @Nullable String subPath, Predicate<Set<String>> parents) {
List<ModResourcePack> packs = new ArrayList<>();
ModResourcePackUtil.appendModResourcePacks(packs, this.type, subPath);
for (ModResourcePack pack : packs) {
Text displayName = subPath == null
? Text.translatable("pack.name.fabricMod", pack.getFabricModMetadata().getName())
: Text.translatable("pack.name.fabricMod.subPack", pack.getFabricModMetadata().getName(), Text.translatable("resourcePack." + subPath + ".name"));
ResourcePackProfile profile = ResourcePackProfile.create(
subPath == null ? pack.getName() : pack.getName() + "_" + subPath,
displayName,
subPath == null,
new ModResourcePackFactory(pack),
this.type,
ResourcePackProfile.InsertionPosition.TOP,
RESOURCE_PACK_SOURCE
);
if (profile != null) {
((FabricResourcePackProfile) profile).fabric_setParentsPredicate(parents);
consumer.accept(profile);
}
}
}
}

View file

@ -0,0 +1,48 @@
/*
* 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.List;
import net.minecraft.resource.OverlayResourcePack;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.fabricmc.fabric.api.resource.ModResourcePack;
public record ModResourcePackFactory(ModResourcePack pack) implements ResourcePackProfile.PackFactory {
@Override
public ResourcePack open(String name) {
return pack;
}
@Override
public ResourcePack openWithOverlays(String name, ResourcePackProfile.Metadata metadata) {
if (metadata.overlays().isEmpty()) {
return pack;
} else {
List<ResourcePack> overlays = new ArrayList<>(metadata.overlays().size());
for (String overlay : metadata.overlays()) {
overlays.add(pack.createOverlay(overlay));
}
return new OverlayResourcePack(pack, overlays);
}
}
}

View file

@ -16,12 +16,19 @@
package net.fabricmc.fabric.impl.resource.loader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.google.common.base.Charsets;
@ -29,6 +36,8 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.SharedConstants;
import net.minecraft.resource.DataConfiguration;
@ -37,6 +46,7 @@ import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.featuretoggle.FeatureFlags;
import net.minecraft.resource.metadata.PackResourceMetadata;
import net.minecraft.text.Text;
import net.fabricmc.fabric.api.resource.ModResourcePack;
@ -49,7 +59,8 @@ import net.fabricmc.loader.api.metadata.ModMetadata;
* Internal utilities for managing resource packs.
*/
public final class ModResourcePackUtil {
private static final Gson GSON = new Gson();
public static final Gson GSON = new Gson();
private static final Logger LOGGER = LoggerFactory.getLogger(ModResourcePackUtil.class);
private ModResourcePackUtil() {
}
@ -67,7 +78,7 @@ public final class ModResourcePackUtil {
continue;
}
ModResourcePack pack = ModNioResourcePack.create(container.getMetadata().getId(), container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED);
ModResourcePack pack = ModNioResourcePack.create(container.getMetadata().getId(), container, subPath, type, ResourcePackActivationType.ALWAYS_ENABLED, true);
if (pack != null) {
packs.add(pack);
@ -75,25 +86,76 @@ public final class ModResourcePackUtil {
}
}
public static boolean containsDefault(ModMetadata info, String filename) {
return "pack.mcmeta".equals(filename);
public static void refreshAutoEnabledPacks(List<ResourcePackProfile> enabledProfiles, Map<String, ResourcePackProfile> allProfiles) {
LOGGER.debug("[Fabric] Starting internal pack sorting with: {}", enabledProfiles.stream().map(ResourcePackProfile::getName).toList());
enabledProfiles.removeIf(profile -> ((FabricResourcePackProfile) profile).fabric_isHidden());
LOGGER.debug("[Fabric] Removed all internal packs, result: {}", enabledProfiles.stream().map(ResourcePackProfile::getName).toList());
ListIterator<ResourcePackProfile> it = enabledProfiles.listIterator();
Set<String> seen = new LinkedHashSet<>();
while (it.hasNext()) {
ResourcePackProfile profile = it.next();
seen.add(profile.getName());
for (ResourcePackProfile p : allProfiles.values()) {
FabricResourcePackProfile fp = (FabricResourcePackProfile) p;
if (fp.fabric_isHidden() && fp.fabric_parentsEnabled(seen) && seen.add(p.getName())) {
it.add(p);
LOGGER.debug("[Fabric] cur @ {}, auto-enabled {}, currently enabled: {}", profile.getName(), p.getName(), seen);
}
}
}
LOGGER.debug("[Fabric] Final sorting result: {}", enabledProfiles.stream().map(ResourcePackProfile::getName).toList());
}
public static InputStream openDefault(ModMetadata info, ResourceType type, String filename) {
public static boolean containsDefault(String filename, boolean modBundled) {
return "pack.mcmeta".equals(filename) || (modBundled && "pack.png".equals(filename));
}
public static InputStream getDefaultIcon() throws IOException {
Optional<Path> loaderIconPath = FabricLoader.getInstance().getModContainer("fabric-resource-loader-v0")
.flatMap(resourceLoaderContainer -> resourceLoaderContainer.getMetadata().getIconPath(512).flatMap(resourceLoaderContainer::findPath));
if (loaderIconPath.isPresent()) {
return Files.newInputStream(loaderIconPath.get());
}
// Should never happen in practice
return null;
}
public static InputStream openDefault(ModContainer container, ResourceType type, String filename) throws IOException {
switch (filename) {
case "pack.mcmeta":
String description = Objects.requireNonNullElse(info.getName(), "");
String description = Objects.requireNonNullElse(container.getMetadata().getName(), "");
String metadata = serializeMetadata(SharedConstants.getGameVersion().getResourceVersion(type), description);
return IOUtils.toInputStream(metadata, Charsets.UTF_8);
case "pack.png":
Optional<Path> path = container.getMetadata().getIconPath(512).flatMap(container::findPath);
if (path.isPresent()) {
return Files.newInputStream(path.get());
} else {
return getDefaultIcon();
}
default:
return null;
}
}
public static PackResourceMetadata getMetadataPack(int packVersion, Text description) {
return new PackResourceMetadata(description, packVersion, Optional.empty());
}
public static JsonObject getMetadataPackJson(int packVersion, Text description) {
return PackResourceMetadata.SERIALIZER.toJson(getMetadataPack(packVersion, description));
}
public static String serializeMetadata(int packVersion, String description) {
JsonObject pack = new JsonObject();
pack.addProperty("pack_format", packVersion);
pack.addProperty("description", description);
// This seems to be still manually deserialized
JsonObject pack = getMetadataPackJson(packVersion, Text.literal(description));
JsonObject metadata = new JsonObject();
metadata.add("pack", pack);
return GSON.toJson(metadata);
@ -123,8 +185,13 @@ public final class ModResourcePackUtil {
// This ensures that any built-in registered data packs by mods which needs to be enabled by default are
// as the data pack screen automatically put any data pack as disabled except the Default data pack.
for (ResourcePackProfile profile : moddedResourcePacks) {
if (profile.getSource() == ModResourcePackCreator.RESOURCE_PACK_SOURCE) {
enabled.add(profile.getName());
continue;
}
try (ResourcePack pack = profile.createResourcePack()) {
if (pack instanceof FabricModResourcePack || (pack instanceof ModNioResourcePack && ((ModNioResourcePack) pack).getActivationType().isEnabledByDefault())) {
if (pack instanceof ModNioResourcePack && ((ModNioResourcePack) pack).getActivationType().isEnabledByDefault()) {
enabled.add(profile.getName());
} else {
disabled.add(profile.getName());

View file

@ -0,0 +1,110 @@
/*
* 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.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import net.minecraft.SharedConstants;
import net.minecraft.resource.InputSupplier;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourceType;
import net.minecraft.resource.metadata.PackResourceMetadata;
import net.minecraft.resource.metadata.ResourceMetadataMap;
import net.minecraft.resource.metadata.ResourceMetadataReader;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public record PlaceholderResourcePack(ResourceType type) implements ResourcePack {
private static final Text DESCRIPTION_TEXT = Text.translatable("pack.description.modResources");
public PackResourceMetadata getMetadata() {
return ModResourcePackUtil.getMetadataPack(
SharedConstants.getGameVersion().getResourceVersion(type),
DESCRIPTION_TEXT
);
}
@Nullable
@Override
public InputSupplier<InputStream> openRoot(String... segments) {
if (segments.length > 0) {
switch (segments[0]) {
case "pack.mcmeta":
return () -> {
String metadata = ModResourcePackUtil.GSON.toJson(PackResourceMetadata.SERIALIZER.toJson(getMetadata()));
return IOUtils.toInputStream(metadata, StandardCharsets.UTF_8);
};
case "pack.png":
return ModResourcePackUtil::getDefaultIcon;
}
}
return null;
}
/**
* This pack has no actual contents.
*/
@Nullable
@Override
public InputSupplier<InputStream> open(ResourceType type, Identifier id) {
return null;
}
@Override
public void findResources(ResourceType type, String namespace, String prefix, ResultConsumer consumer) {
}
@Override
public Set<String> getNamespaces(ResourceType type) {
return Collections.emptySet();
}
@Nullable
@Override
public <T> T parseMetadata(ResourceMetadataReader<T> metaReader) {
return ResourceMetadataMap.of(PackResourceMetadata.SERIALIZER, getMetadata()).get(metaReader);
}
@Override
public String getName() {
return ModResourcePackCreator.FABRIC;
}
@Override
public void close() {
}
public record Factory(ResourceType type) implements ResourcePackProfile.PackFactory {
@Override
public ResourcePack open(String name) {
return new PlaceholderResourcePack(this.type);
}
@Override
public ResourcePack openWithOverlays(String name, ResourcePackProfile.Metadata metadata) {
return open(name);
}
}
}

View file

@ -74,8 +74,8 @@ public class ResourceManagerHelperImpl implements ResourceManagerHelper {
List<Path> paths = container.getRootPaths();
String separator = paths.get(0).getFileSystem().getSeparator();
subPath = subPath.replace("/", separator);
ModNioResourcePack resourcePack = ModNioResourcePack.create(id.toString(), container, subPath, ResourceType.CLIENT_RESOURCES, activationType);
ModNioResourcePack dataPack = ModNioResourcePack.create(id.toString(), container, subPath, ResourceType.SERVER_DATA, activationType);
ModNioResourcePack resourcePack = ModNioResourcePack.create(id.toString(), container, subPath, ResourceType.CLIENT_RESOURCES, activationType, false);
ModNioResourcePack dataPack = ModNioResourcePack.create(id.toString(), container, subPath, ResourceType.SERVER_DATA, activationType, false);
if (resourcePack == null && dataPack == null) return false;
if (resourcePack != null) {

View file

@ -28,7 +28,6 @@ import net.minecraft.resource.ResourcePackSource;
* See {@link net.fabricmc.fabric.mixin.resource.loader.ResourcePackProfileMixin ResourcePackProfileMixin}.
*
* <p>The sources are later read for use in {@link FabricResource} and {@link FabricResourceImpl}.
* See {@link net.fabricmc.fabric.mixin.resource.loader.NamespaceResourceManagerMixin NamespaceResourceManagerMixin}.
*/
public final class ResourcePackSourceTracker {
// Use a weak hash map so that if resource packs would be deleted, this won't keep them alive.

View file

@ -0,0 +1,68 @@
/*
* 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.mixin.resource.loader;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Stream;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.server.command.DatapackCommand;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
/**
* Disables enabling/disabling internal data packs.
* Listing them is still allowed, but they do not appear in suggestions.
*/
@Mixin(DatapackCommand.class)
public class DatapackCommandMixin {
@Unique
private static final DynamicCommandExceptionType INTERNAL_PACK_EXCEPTION = new DynamicCommandExceptionType(
packName -> Text.stringifiedTranslatable("commands.datapack.fabric.internal", packName));
@Redirect(method = "method_13136", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourcePackManager;getEnabledNames()Ljava/util/Collection;"))
private static Collection<String> filterEnabledPackSuggestions(ResourcePackManager dataPackManager) {
return dataPackManager.getEnabledProfiles().stream().filter(profile -> !((FabricResourcePackProfile) profile).fabric_isHidden()).map(ResourcePackProfile::getName).toList();
}
@WrapOperation(method = "method_13120", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;filter(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;", ordinal = 0))
private static Stream<ResourcePackProfile> filterDisabledPackSuggestions(Stream<ResourcePackProfile> instance, Predicate<? super ResourcePackProfile> predicate, Operation<Stream<ResourcePackProfile>> original) {
return original.call(instance, predicate).filter(profile -> !((FabricResourcePackProfile) profile).fabric_isHidden());
}
@Inject(method = "getPackContainer", at = @At(value = "INVOKE", target = "Ljava/util/Collection;contains(Ljava/lang/Object;)Z", shift = At.Shift.BEFORE))
private static void errorOnInternalPack(CommandContext<ServerCommandSource> context, String name, boolean enable, CallbackInfoReturnable<ResourcePackProfile> cir, @Local ResourcePackProfile profile) throws CommandSyntaxException {
if (((FabricResourcePackProfile) profile).fabric_isHidden()) throw INTERNAL_PACK_EXCEPTION.create(profile.getName());
}
}

View file

@ -1,63 +0,0 @@
/*
* 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.mixin.resource.loader;
import java.io.InputStream;
import java.util.List;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import net.minecraft.resource.InputSupplier;
import net.minecraft.resource.NamespaceResourceManager;
import net.minecraft.resource.Resource;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourceType;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.resource.loader.GroupResourcePack;
/**
* Patches getAllResources and method_41265 to work with GroupResourcePack.
*/
@Mixin(NamespaceResourceManager.class)
public class NamespaceResourceManagerMixin {
private final ThreadLocal<List<Resource>> fabric$getAllResources$resources = new ThreadLocal<>();
@Inject(method = "getAllResources",
at = @At(value = "INVOKE", target = "Ljava/util/List;size()I"),
locals = LocalCapture.CAPTURE_FAILHARD)
private void onGetAllResources(Identifier id, CallbackInfoReturnable<List<Resource>> cir, Identifier metadataId, List<Resource> resources) {
this.fabric$getAllResources$resources.set(resources);
}
@Redirect(method = "getAllResources",
at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourcePack;open(Lnet/minecraft/resource/ResourceType;Lnet/minecraft/util/Identifier;)Lnet/minecraft/resource/InputSupplier;"))
private InputSupplier<InputStream> onResourceAdd(ResourcePack pack, ResourceType type, Identifier id) {
if (pack instanceof GroupResourcePack) {
((GroupResourcePack) pack).appendResources(type, id, this.fabric$getAllResources$resources.get());
return null;
}
return pack.open(type, id);
}
}

View file

@ -1,46 +0,0 @@
/*
* 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.mixin.resource.loader;
import java.util.List;
import java.util.stream.Collectors;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.resource.ReloadableResourceManagerImpl;
import net.minecraft.resource.ResourcePack;
import net.fabricmc.fabric.impl.resource.loader.GroupResourcePack;
@Mixin(ReloadableResourceManagerImpl.class)
public class ReloadableResourceManagerImplMixin {
// private static synthetic method_29491(Ljava/util/List;)Ljava/lang/Object;
// Supplier lambda in beginMonitoredReload method.
@Inject(method = "method_29491", at = @At("HEAD"), cancellable = true)
private static void getResourcePackNames(List<ResourcePack> packs, CallbackInfoReturnable<String> cir) {
cir.setReturnValue(packs.stream().map(pack -> {
if (pack instanceof GroupResourcePack) {
return ((GroupResourcePack) pack).getFullName();
} else {
return pack.getName();
}
}).collect(Collectors.joining(", ")));
}
}

View file

@ -27,8 +27,6 @@ import net.fabricmc.fabric.impl.resource.loader.ResourcePackSourceTracker;
/**
* Implements {@link FabricResource} (resource source getter/setter)
* for vanilla's basic {@link Resource} used for most game resources.
*
* @see NamespaceResourceManagerMixin the usage site for this mixin
*/
@Mixin(Resource.class)
class ResourceMixin implements FabricResource {

View file

@ -16,16 +16,25 @@
package net.fabricmc.fabric.mixin.resource.loader;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.llamalad7.mixinextras.sugar.Local;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.resource.FileResourcePackProvider;
import net.minecraft.resource.ResourcePackManager;
@ -34,14 +43,22 @@ import net.minecraft.resource.ResourcePackProvider;
import net.minecraft.resource.ResourcePackSource;
import net.minecraft.resource.ResourceType;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackCreator;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
@Mixin(ResourcePackManager.class)
public abstract class ResourcePackManagerMixin<T extends ResourcePackProfile> {
public abstract class ResourcePackManagerMixin {
@Unique
private static final Logger LOGGER = LoggerFactory.getLogger("ResourcePackManagerMixin");
@Shadow
@Final
@Mutable
private Set<ResourcePackProvider> providers;
public Set<ResourcePackProvider> providers;
@Shadow
private Map<String, ResourcePackProfile> profiles;
@Inject(method = "<init>", at = @At("RETURN"))
public void construct(ResourcePackProvider[] resourcePackProviders, CallbackInfo info) {
@ -65,4 +82,25 @@ public abstract class ResourcePackManagerMixin<T extends ResourcePackProfile> {
providers.add(new ModResourcePackCreator(ResourceType.SERVER_DATA));
}
}
@Inject(method = "buildEnabledProfiles", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/ImmutableList;copyOf(Ljava/util/Collection;)Lcom/google/common/collect/ImmutableList;", shift = At.Shift.BEFORE))
private void handleAutoEnableDisable(Collection<String> enabledNames, CallbackInfoReturnable<List<ResourcePackProfile>> cir, @Local List<ResourcePackProfile> enabledAfterFirstRun) {
ModResourcePackUtil.refreshAutoEnabledPacks(enabledAfterFirstRun, this.profiles);
}
@Inject(method = "enable", at = @At(value = "INVOKE", target = "Ljava/util/List;add(Ljava/lang/Object;)Z", shift = At.Shift.AFTER))
private void handleAutoEnable(String profile, CallbackInfoReturnable<Boolean> cir, @Local List<ResourcePackProfile> newlyEnabled) {
if (ModResourcePackCreator.POST_CHANGE_HANDLE_REQUIRED.contains(profile)) {
ModResourcePackUtil.refreshAutoEnabledPacks(newlyEnabled, this.profiles);
}
}
@Inject(method = "disable", at = @At(value = "INVOKE", target = "Ljava/util/List;remove(Ljava/lang/Object;)Z"))
private void handleAutoDisable(String profile, CallbackInfoReturnable<Boolean> cir, @Local List<ResourcePackProfile> enabled) {
if (ModResourcePackCreator.POST_CHANGE_HANDLE_REQUIRED.contains(profile)) {
Set<String> currentlyEnabled = enabled.stream().map(ResourcePackProfile::getName).collect(Collectors.toSet());
enabled.removeIf(p -> !((FabricResourcePackProfile) p).fabric_parentsEnabled(currentlyEnabled));
LOGGER.debug("[Fabric] Internal pack auto-removed upon disabling {}, result: {}", profile, enabled.stream().map(ResourcePackProfile::getName).toList());
}
}
}

View file

@ -16,9 +16,13 @@
package net.fabricmc.fabric.mixin.resource.loader;
import java.util.Set;
import java.util.function.Predicate;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@ -27,6 +31,7 @@ import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourcePackSource;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.ResourcePackSourceTracker;
/**
@ -37,13 +42,32 @@ import net.fabricmc.fabric.impl.resource.loader.ResourcePackSourceTracker;
* @see ResourcePackSourceTracker
*/
@Mixin(ResourcePackProfile.class)
abstract class ResourcePackProfileMixin {
abstract class ResourcePackProfileMixin implements FabricResourcePackProfile {
@Unique
private static final Predicate<Set<String>> DEFAULT_PARENT_PREDICATE = parents -> true;
@Shadow
@Final
private ResourcePackSource source;
@Unique
private Predicate<Set<String>> parentsPredicate = DEFAULT_PARENT_PREDICATE;
@Inject(method = "createResourcePack", at = @At("RETURN"))
private void onCreateResourcePack(CallbackInfoReturnable<ResourcePack> info) {
ResourcePackSourceTracker.setSource(info.getReturnValue(), source);
}
@Override
public boolean fabric_isHidden() {
return parentsPredicate != DEFAULT_PARENT_PREDICATE;
}
@Override
public boolean fabric_parentsEnabled(Set<String> enabled) {
return parentsPredicate.test(enabled);
}
@Override
public void fabric_setParentsPredicate(Predicate<Set<String>> predicate) {
this.parentsPredicate = predicate;
}
}

View file

@ -3,5 +3,7 @@
"pack.source.fabricmod": "Fabric mod",
"pack.source.builtinMod": "built-in: %s",
"pack.name.fabricMod": "Fabric Mod \"%s\"",
"pack.name.fabricMods": "Fabric Mods"
"pack.name.fabricMods": "Fabric Mods",
"pack.name.fabricMod.subPack": "Fabric Mod \"%s\" (%s)",
"commands.datapack.fabric.internal": "Cannot enable or disable Fabric internal pack \"%s\"."
}

View file

@ -1,5 +1,6 @@
{
"pack.source.fabricmod": "Fabric mod",
"pack.source.builtinMod": "ビルトイン: %s",
"pack.name.fabricMods": "Fabric Mod"
"pack.name.fabricMods": "Fabric Mod",
"commands.datapack.fabric.internal": "「%s」はFabric内部で管理されているため、有効化・無効化できません"
}

View file

@ -3,11 +3,10 @@
"package": "net.fabricmc.fabric.mixin.resource.loader",
"compatibilityLevel": "JAVA_17",
"mixins": [
"DatapackCommandMixin",
"KeyedResourceReloadListenerMixin",
"LifecycledResourceManagerImplMixin",
"MinecraftServerMixin",
"NamespaceResourceManagerMixin",
"ReloadableResourceManagerImplMixin",
"ResourceMixin",
"ResourcePackManagerMixin",
"ResourcePackProfileMixin",

View file

@ -0,0 +1,248 @@
/*
* 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.unit;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import net.minecraft.Bootstrap;
import net.minecraft.SharedConstants;
import net.minecraft.resource.ResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.FabricResourcePackProfile;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackCreator;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
public class ModResourcePackUtilTests {
private static final Gson GSON = new Gson();
@BeforeAll
static void beforeAll() {
SharedConstants.createGameVersion();
Bootstrap.initialize();
}
@Test
void testRefreshAutoEnabledPacks() {
// Vanilla uses tree map, and we test the behavior
Map<String, ResourcePackProfile> profiles = new TreeMap<>();
Map<String, ResourcePackProfile> modAProfiles = new TreeMap<>();
Map<String, ResourcePackProfile> modBProfiles = new TreeMap<>();
Map<String, ResourcePackProfile> allProfiles = new TreeMap<>();
ResourcePackProfile vanilla = mockProfile(profiles, "vanilla", null);
ResourcePackProfile fabric = mockProfile(profiles, ModResourcePackCreator.FABRIC, null);
ResourcePackProfile modA = mockProfile(modAProfiles, "mod_a", ModResourcePackCreator.BASE_PARENT);
ResourcePackProfile modAProg = mockProfile(modAProfiles, "mod_a_programmer_art", ModResourcePackCreator.PROGRAMMER_ART_PARENT);
ResourcePackProfile modAHigh = mockProfile(modAProfiles, "mod_a_high_contrast", ModResourcePackCreator.HIGH_CONTRAST_PARENT);
ResourcePackProfile modB = mockProfile(modBProfiles, "mod_b", ModResourcePackCreator.BASE_PARENT);
ResourcePackProfile modBProg = mockProfile(modBProfiles, "mod_b_programmer_art", ModResourcePackCreator.PROGRAMMER_ART_PARENT);
ResourcePackProfile modBHigh = mockProfile(modBProfiles, "mod_b_high_contrast", ModResourcePackCreator.HIGH_CONTRAST_PARENT);
ResourcePackProfile programmerArt = mockProfile(profiles, "programmer_art", null);
ResourcePackProfile highContrast = mockProfile(profiles, "high_contrast", null);
ResourcePackProfile userPackA = mockProfile(profiles, "user_pack_a", null);
ResourcePackProfile userPackB = mockProfile(profiles, "user_pack_b", null);
modAProfiles.putAll(profiles);
modBProfiles.putAll(profiles);
allProfiles.putAll(modAProfiles);
allProfiles.putAll(modBProfiles);
testRefreshAutoEnabledPacks(
profiles,
List.of(vanilla, fabric),
List.of(vanilla, fabric),
"keep (no mods)"
);
testRefreshAutoEnabledPacks(
profiles,
List.of(vanilla, fabric, userPackA),
List.of(vanilla, fabric, userPackA),
"keep (no mods, keep user pack)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA),
List.of(vanilla, fabric, modA),
"keep (mod A only)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, programmerArt, modAProg),
List.of(vanilla, fabric, modA, programmerArt, modAProg),
"keep (programmer_art)"
);
testRefreshAutoEnabledPacks(
allProfiles,
List.of(vanilla, fabric, modA, modB, programmerArt, modAProg, modBProg),
List.of(vanilla, fabric, modA, modB, programmerArt, modAProg, modBProg),
"keep (mod A and mod B, programmer_art)"
);
testRefreshAutoEnabledPacks(
allProfiles,
List.of(vanilla, fabric, modA, modB, programmerArt, modAProg, modBProg, highContrast, modAHigh, modBHigh),
List.of(vanilla, fabric, modA, modB, programmerArt, modAProg, modBProg, highContrast, modAHigh, modBHigh),
"keep (mod A and mod B, both)"
);
testRefreshAutoEnabledPacks(
allProfiles,
List.of(vanilla, fabric, modA, modB, highContrast, modAHigh, modBHigh, programmerArt, modAProg, modBProg),
List.of(vanilla, fabric, modA, modB, highContrast, modAHigh, modBHigh, programmerArt, modAProg, modBProg),
"keep (remembers programmer_art-high_contrast order)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric),
List.of(vanilla, fabric, modA),
"fix (adding missing mods)"
);
testRefreshAutoEnabledPacks(
allProfiles,
List.of(vanilla, fabric, userPackA),
List.of(vanilla, fabric, modA, modB, userPackA),
"fix (adding missing mods at the right place)"
);
testRefreshAutoEnabledPacks(
allProfiles,
List.of(vanilla, fabric, modB, modA),
List.of(vanilla, fabric, modA, modB),
"fix (mod A and B, sorting)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, userPackB, modA, userPackA),
List.of(vanilla, fabric, modA, userPackB, userPackA),
"fix (user pack goes last)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, programmerArt),
List.of(vanilla, fabric, modA, programmerArt, modAProg),
"fix (adding 1 met dep)"
);
testRefreshAutoEnabledPacks(
modBProfiles,
List.of(vanilla, fabric, modB, highContrast),
List.of(vanilla, fabric, modB, highContrast, modBHigh),
"fix (adding 1 met dep, part 2)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, programmerArt, highContrast),
List.of(vanilla, fabric, modA, programmerArt, modAProg, highContrast, modAHigh),
"fix (adding 2 met deps)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, programmerArt, modAProg, highContrast),
List.of(vanilla, fabric, modA, programmerArt, modAProg, highContrast, modAHigh),
"fix (adding 2 met deps + preexisting)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, modAProg, modAHigh),
List.of(vanilla, fabric, modA),
"fix (removing 2 unmet deps)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modA, programmerArt, modAProg, modAHigh),
List.of(vanilla, fabric, modA, programmerArt, modAProg),
"fix (removing 1 unmet dep)"
);
testRefreshAutoEnabledPacks(
modBProfiles,
List.of(vanilla, fabric, modB, highContrast, modBProg, modBHigh),
List.of(vanilla, fabric, modB, highContrast, modBHigh),
"fix (removing 1 unmet dep, part 2)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modAProg, programmerArt, modA),
List.of(vanilla, fabric, modA, programmerArt, modAProg),
"reorder (bundled comes just after parents)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, modAProg, userPackA, programmerArt, modA, userPackB),
List.of(vanilla, fabric, modA, userPackA, programmerArt, modAProg, userPackB),
"reorder (keep user pack order)"
);
testRefreshAutoEnabledPacks(
modAProfiles,
List.of(vanilla, fabric, userPackB, modA, programmerArt, userPackA, modAProg),
List.of(vanilla, fabric, modA, userPackB, programmerArt, modAProg, userPackA),
"reorder (no user pack between parent-bundled)"
);
}
private ResourcePackProfile mockProfile(Map<String, ResourcePackProfile> profiles, String id, @Nullable Predicate<Set<String>> parents) {
ResourcePackProfile profile = ResourcePackProfile.of(
id,
null,
false,
null,
null,
null,
false,
ModResourcePackCreator.RESOURCE_PACK_SOURCE
);
if (parents != null) ((FabricResourcePackProfile) profile).fabric_setParentsPredicate(parents);
profiles.put(id, profile);
return profile;
}
private void testRefreshAutoEnabledPacks(Map<String, ResourcePackProfile> profiles, List<ResourcePackProfile> before, List<ResourcePackProfile> after, String reason) {
List<ResourcePackProfile> processed = new ArrayList<>(before);
ModResourcePackUtil.refreshAutoEnabledPacks(processed, profiles);
assertEquals(
after.stream().map(ResourcePackProfile::getName).toList(),
processed.stream().map(ResourcePackProfile::getName).toList(),
() -> "Testing %s; input %s".formatted(reason, before.stream().map(ResourcePackProfile::getName).toList())
);
}
@Test
void testSerializeMetadata() {
// Test various metadata serialization issues (#2407)
testMetadataSerialization("");
testMetadataSerialization("Quotes: \"\" \"");
testMetadataSerialization("Backslash: \\ \\\\");
}
private void testMetadataSerialization(String description) throws JsonParseException {
String metadata = ModResourcePackUtil.serializeMetadata(1, description);
JsonObject json = assertDoesNotThrow(() -> GSON.fromJson(metadata, JsonObject.class), () -> "Failed to serialize " + description);
String parsedDescription = json.get("pack").getAsJsonObject().get("description").getAsString();
assertEquals(description, parsedDescription, "Parsed description differs from original one");
}
}

View file

@ -16,9 +16,6 @@
package net.fabricmc.fabric.test.resource.loader;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,7 +25,6 @@ import net.minecraft.util.Identifier;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
import net.fabricmc.loader.api.FabricLoader;
public class BuiltinResourcePackTestMod implements ModInitializer {
@ -36,8 +32,6 @@ public class BuiltinResourcePackTestMod implements ModInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger(BuiltinResourcePackTestMod.class);
private static final Gson GSON = new Gson();
@Override
public void onInitialize() {
// Should always be present as it's **this** mod.
@ -49,27 +43,5 @@ public class BuiltinResourcePackTestMod implements ModInitializer {
.map(container -> ResourceManagerHelper.registerBuiltinResourcePack(new Identifier(MODID, "test2"),
container, ResourcePackActivationType.NORMAL))
.filter(success -> !success).ifPresent(success -> LOGGER.warn("Could not register built-in resource pack."));
// Test various metadata serialization issues (#2407)
testMetadataSerialization("");
testMetadataSerialization("Quotes: \"\" \"");
testMetadataSerialization("Backslash: \\ \\\\");
}
private void testMetadataSerialization(String description) {
String metadata = ModResourcePackUtil.serializeMetadata(1, description);
JsonObject json;
try {
json = GSON.fromJson(metadata, JsonObject.class);
} catch (JsonParseException exc) {
throw new AssertionError("Metadata parsing test for description \"%s\" failed".formatted(description), exc);
}
String parsedDescription = json.get("pack").getAsJsonObject().get("description").getAsString();
if (!description.equals(parsedDescription)) {
throw new AssertionError("Metadata parsing test for description failed: expected \"%s\", got \"%s\"".formatted(description, parsedDescription));
}
}
}