Skip to content

Commit

Permalink
Move texture existance checks to a new faster util
Browse files Browse the repository at this point in the history
Instead of using hasResource for existance, take advantage of the fact our existance has many checks per folder instead of each texture being in unique folders. This allows us to lookup all textures in a folder once and cache the results
Still want to see if I can further optimize as zip file listResources requires iterating the entire pack, will look into that in the future
  • Loading branch information
KnightMiner committed Nov 21, 2022
1 parent 46c8454 commit f41c5df
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package slimeknights.tconstruct.library.client.model;

import lombok.extern.log4j.Log4j2;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
import net.minecraftforge.client.event.TextureStitchEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.EventPriority;
import slimeknights.mantle.data.IEarlySafeManagerReloadListener;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

/**
* Logic to handle dynamic texture scans. Instead of calling {@link ResourceManager#hasResource(ResourceLocation)} per texture, we take advantage of the fact
* our models are always looking for many textures in a single folder and do a per folder call to {@link ResourceManager#listResources(String, Predicate)}
*/
@Log4j2
public class DynamicTextureLoader {
/** Start of the path to trim when caching existing textures */
private static final int TRIM_START = "textures/".length();
/** End of the path to trim when caching existing textures */
private static final int TRIM_END = ".png".length();
/** Set of all folders that have been scanned, so we can avoid scanning them twice */
private static final Set<String> SCANNED_FOLDERS = new HashSet<>();
/** Map of discovered textures */
private static final Set<ResourceLocation> EXISTING_TEXTURES = new HashSet<>();
/** Set of all textures that are missing from the resource pack, to avoid logging twice */
private static final Set<ResourceLocation> SKIPPED_TEXTURES = new HashSet<>();
/** Reload listener to clear caches */
private static final IEarlySafeManagerReloadListener RELOAD_LISTENER = manager -> {
clearCache();
};

/** Clears all cached texture names */
private static void clearCache() {
SCANNED_FOLDERS.clear();
EXISTING_TEXTURES.clear();
SKIPPED_TEXTURES.clear();
}

/** Registers this manager */
public static void init(RegisterClientReloadListenersEvent event) {
event.registerReloadListener(RELOAD_LISTENER);
// clear cache on texture stitch, no longer need it then as its too late to lookup textures
MinecraftForge.EVENT_BUS.addListener(EventPriority.NORMAL, false, TextureStitchEvent.Pre.class, e -> clearCache());
}

/** Checks if the given folder is not yet scanned */
private static boolean checkFolderNotScanned(String originalFolder) {
// if we already checked the folder, no work to do
if (SCANNED_FOLDERS.contains(originalFolder)) {
return false;
}
// if a folder has not been scanned yet, check if any of its parent's have been scanned
// list resources will fetch all sub folders, so this saves us calling it multiple times per tool
String folder = originalFolder;
int lastPos = folder.lastIndexOf('/');
while (lastPos != -1) {
folder = folder.substring(0, lastPos);
if (SCANNED_FOLDERS.contains(folder)) {
// if we scanned a parent, no work to do, but mark ourself as scanned, may find more textures here later
SCANNED_FOLDERS.add(originalFolder);
return false;
}
lastPos = folder.lastIndexOf('/');
}
// mark this folder as searched for next time, return true to fetch texture names
SCANNED_FOLDERS.add(originalFolder);
return true;
}

/** Checks if a texture exists */
private static boolean textureExists(ResourceManager manager, String folder, ResourceLocation location) {
if (checkFolderNotScanned(folder)) {
manager.listResources("textures/" + folder, name -> name.endsWith(".png")).stream()
.map(loc -> {
String path = loc.getPath();
return new ResourceLocation(loc.getNamespace(), path.substring(TRIM_START, path.length() - TRIM_END));
})
.forEach(EXISTING_TEXTURES::add);
}
return EXISTING_TEXTURES.contains(location);
}

/** Logs that a dynamic texture is missing, config option to disable */
public static void logMissingTexture(ResourceLocation location) {
if (!SKIPPED_TEXTURES.contains(location)) {
SKIPPED_TEXTURES.add(location);
log.debug("Skipping loading texture '{}' as it does not exist in the resource pack", location);
}
}

/**
* Gets a consumer to add textures to the given collection
* @param allTextures Collection of textures
* @param folder Folder the texture is expected to reside in. Will give inconsistent behavior if the location is not a member of the folder, this is not validated
* @param logMissingTextures If true, log textures that were not found
* @return Texture consumer
*/
public static Predicate<Material> getTextureAdder(String folder, Collection<Material> allTextures, boolean logMissingTextures) {
ResourceManager manager = Minecraft.getInstance().getResourceManager();
return mat -> {
// either must be non-blocks, or must exist. We have fallbacks if it does not exist
ResourceLocation loc = mat.texture();
if (!InventoryMenu.BLOCK_ATLAS.equals(mat.atlasLocation()) || textureExists(manager, folder, loc)) {
allTextures.add(mat);
return true;
}
if (logMissingTextures) {
logMissingTexture(loc);
}
return false;
};
}

/** Gets the folder containing the given texture */
public static String getTextureFolder(ResourceLocation location) {
String path = location.getPath();
int index = path.lastIndexOf('/');
if (index == -1) {
return path;
}
return path.substring(0, index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@
import slimeknights.tconstruct.library.client.materials.MaterialRenderInfo;
import slimeknights.tconstruct.library.client.materials.MaterialRenderInfo.TintedSprite;
import slimeknights.tconstruct.library.client.materials.MaterialRenderInfoLoader;
import slimeknights.tconstruct.library.client.model.DynamicTextureLoader;
import slimeknights.tconstruct.library.materials.definition.MaterialVariantId;
import slimeknights.tconstruct.library.tools.part.IMaterialItem;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand All @@ -62,8 +62,6 @@
@AllArgsConstructor
@Log4j2
public class MaterialModel implements IModelGeometry<MaterialModel> {
/** Set of all textures that are missing from the resource pack, to avoid logging twice */
private static final Set<ResourceLocation> SKIPPED_TEXTURES = new HashSet<>();

/** Shared loader instance */
public static final Loader LOADER = new Loader();
Expand All @@ -84,15 +82,13 @@ public Collection<Material> getTextures(IModelConfiguration owner, Function<Reso
}

/** Checks if a texture exists */
@Deprecated
private static boolean textureExists(ResourceManager manager, ResourceLocation location) {
return manager.hasResource(new ResourceLocation(location.getNamespace(), "textures/" + location.getPath() + ".png"));
}

/**
* Gets a consumer to add textures to the given collection
* @param allTextures Collection of textures
* @return Texture consumer
*/
/** @deprecated use {@link DynamicTextureLoader#getTextureAdder(String, Collection, boolean)} */
@Deprecated
public static Predicate<Material> getTextureAdder(Collection<Material> allTextures, boolean logMissingTextures) {
ResourceManager manager = Minecraft.getInstance().getResourceManager();
return mat -> {
Expand All @@ -101,9 +97,9 @@ public static Predicate<Material> getTextureAdder(Collection<Material> allTextur
if (!InventoryMenu.BLOCK_ATLAS.equals(mat.atlasLocation()) || textureExists(manager, loc)) {
allTextures.add(mat);
return true;
} else if (logMissingTextures && !SKIPPED_TEXTURES.contains(loc)) {
SKIPPED_TEXTURES.add(loc);
log.debug("Skipping loading texture '{}' as it does not exist in the resource pack", loc);
}
if (logMissingTextures) {
DynamicTextureLoader.logMissingTexture(loc);
}
return false;
};
Expand All @@ -123,7 +119,7 @@ public static void getMaterialTextures(Collection<Material> allTextures, IModelC
// if the texture is missing, stop here
if (!MissingTextureAtlasSprite.getLocation().equals(texture.texture())) {
// texture should exist in item/tool, or the validator cannot handle them
Predicate<Material> textureAdder = getTextureAdder(allTextures, Config.CLIENT.logMissingMaterialTextures.get());
Predicate<Material> textureAdder = DynamicTextureLoader.getTextureAdder(DynamicTextureLoader.getTextureFolder(texture.texture()), allTextures, Config.CLIENT.logMissingMaterialTextures.get());
// if no specific material is set, load all materials as dependencies. If just one material, use just that one
if (material == null) {
MaterialRenderInfoLoader.INSTANCE.getAllRenderInfos().forEach(info -> info.getTextureDependencies(textureAdder, texture));
Expand Down Expand Up @@ -286,9 +282,7 @@ private BakedModel bakeDynamic(MaterialVariantId material) {
*/
private static class Loader implements IModelLoader<MaterialModel> {
@Override
public void onResourceManagerReload(ResourceManager resourceManager) {
SKIPPED_TEXTURES.clear();
}
public void onResourceManagerReload(ResourceManager resourceManager) {}

@Override
public MaterialModel read(JsonDeserializationContext deserializationContext, JsonObject modelContents) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import slimeknights.mantle.data.IEarlySafeManagerReloadListener;
import slimeknights.mantle.util.JsonHelper;
import slimeknights.tconstruct.common.config.Config;
import slimeknights.tconstruct.library.client.model.tools.MaterialModel;
import slimeknights.tconstruct.library.client.model.DynamicTextureLoader;
import slimeknights.tconstruct.library.modifiers.ModifierId;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -137,34 +137,60 @@ public void onReloadSafe(ResourceManager manager) {
modifierModels = models;
}

/**
* Gets the path to the texture for a given modifier
* @param modifierRoot Modifier root location
* @param modifierId Specific modifier ID
* @return Path to the modifier
*/
private static Material getModifierTexture(ResourceLocation modifierRoot, ResourceLocation modifierId, String suffix) {
return ForgeHooksClient.getBlockMaterial(new ResourceLocation(modifierRoot.getNamespace(), modifierRoot.getPath() + modifierId.getNamespace() + "_" + modifierId.getPath() + suffix));
/** Record handling the texture adder for each given modifier folder */
private record ModifierTextureLoader(ResourceLocation root, Predicate<Material> textureAdder) {
private ModifierTextureLoader(ResourceLocation root, Collection<Material> textures, boolean logMissingTextures) {
// the texture root may be a folder, in which case that method will trim off the trailing slash
// it may be instead a folder with a prefix, in that case ignore the prefix
this(root, DynamicTextureLoader.getTextureAdder(DynamicTextureLoader.getTextureFolder(root), textures, logMissingTextures));
}

/**
* Gets the path to the texture for a given modifier
* @param modifierRoot Modifier root location
* @param modifierId Specific modifier ID
* @return Path to the modifier
*/
private static Material getModifierTexture(ResourceLocation modifierRoot, ResourceLocation modifierId, String suffix) {
return ForgeHooksClient.getBlockMaterial(new ResourceLocation(modifierRoot.getNamespace(), modifierRoot.getPath() + modifierId.getNamespace() + "_" + modifierId.getPath() + suffix));
}

/** Gets the texture for the given modifier and suffix */
@Nullable
public Material getTexture(ResourceLocation modifier, String suffix) {
Material texture = getModifierTexture(root, modifier, suffix);
if (textureAdder.test(texture)) {
return texture;
}
return null;
}
}

/** Gets the texture loaders for the given modifier textures */
private static List<ModifierTextureLoader> getModifierTextureLoaders(List<ResourceLocation> modifierRoots, Collection<Material> allTextures) {
if (modifierRoots.isEmpty()) {
return Collections.emptyList();
}
return modifierRoots.stream().map(root -> new ModifierTextureLoader(root, allTextures, Config.CLIENT.logMissingModifierTextures.get())).toList();
}

/**
* Gets the texture for the given parameters
* @param modifierRoots List of modifier roots, tries each
* @param textureAdder Functon to check if a texture exists, storing it as needed
* @param modifier Modifier to fetch
* @param suffix Additional suffix for the fetched texture
* @param loaders List of texture loaders, tries each one in sequence until one finds it
* @param modifier Modifier to fetch
* @param suffix Additional suffix for the fetched texture
* @return Texture, or null if missing
*/
@Nullable
private static Material getTexture(List<ResourceLocation> modifierRoots, @Nullable Predicate<Material> textureAdder, ResourceLocation modifier, String suffix) {
if (textureAdder == null) {
private static Material getTexture(List<ModifierTextureLoader> loaders, ResourceLocation modifier, String suffix) {
if (loaders.isEmpty()) {
return null;
}

// try the non-logging ones first
for (ResourceLocation root : modifierRoots) {
Material texture = getModifierTexture(root, modifier, suffix);
if (textureAdder.test(texture)) {
for (ModifierTextureLoader root : loaders) {
Material texture = root.getTexture(modifier, suffix);
if (texture != null) {
return texture;
}
}
Expand All @@ -189,18 +215,16 @@ public static Map<ModifierId,IBakedModifierModel> getModelsForTool(List<Resource
ImmutableMap.Builder<ModifierId,IBakedModifierModel> modelMap = ImmutableMap.builder();

// create two texture adders, so we only log on the final option if missing
Predicate<Material> smallTextureAdder = smallModifierRoots.isEmpty() ? null
: MaterialModel.getTextureAdder(textures, Config.CLIENT.logMissingModifierTextures.get());
Predicate<Material> largeTextureAdder = largeModifierRoots.isEmpty() ? null
: MaterialModel.getTextureAdder(textures, Config.CLIENT.logMissingModifierTextures.get());
List<ModifierTextureLoader> smallTextureLoaders = getModifierTextureLoaders(smallModifierRoots, textures);
List<ModifierTextureLoader> largeTextureLoaders = getModifierTextureLoaders(largeModifierRoots, textures);

// load each modifier
for (Entry<ModifierId, IUnbakedModifierModel> entry : modifierModels.entrySet()) {
ModifierId id = entry.getKey();
IUnbakedModifierModel model = entry.getValue();
IBakedModifierModel toolModel = model.forTool(
name -> getTexture(smallModifierRoots, smallTextureAdder, id, name),
name -> getTexture(largeModifierRoots, largeTextureAdder, id, name));
name -> getTexture(smallTextureLoaders, id, name),
name -> getTexture(largeTextureLoaders, id, name));
if (toolModel != null) {
modelMap.put(id, toolModel);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import slimeknights.tconstruct.common.ClientEventBase;
import slimeknights.tconstruct.common.network.TinkerNetwork;
import slimeknights.tconstruct.library.client.materials.MaterialTooltipCache;
import slimeknights.tconstruct.library.client.model.DynamicTextureLoader;
import slimeknights.tconstruct.library.client.model.tools.MaterialModel;
import slimeknights.tconstruct.library.client.model.tools.ToolModel;
import slimeknights.tconstruct.library.client.modifiers.BreakableDyedModifierModel;
Expand Down Expand Up @@ -75,6 +76,7 @@ public class ToolClientEvents extends ClientEventBase {
static void addResourceListener(RegisterClientReloadListenersEvent manager) {
ModifierModelManager.init(manager);
MaterialTooltipCache.init(manager);
DynamicTextureLoader.init(manager);
manager.registerReloadListener(MODIFIER_RELOAD_LISTENER);
manager.registerReloadListener(PlateArmorModel.RELOAD_LISTENER);
manager.registerReloadListener(SlimeskullArmorModel.RELOAD_LISTENER);
Expand Down

0 comments on commit f41c5df

Please sign in to comment.