Default to showing the whole model registry to mods

This should fix many silent incompatibilities that existed with the
original opt-in approach. To my knowledge, there are no remaining
popular mods on 1.20 that forcefully load all models in this event
(by iterating over entrySet and calling getValue unconditionally)
so doing this should be safe. Additional logging is also added
to provide quick insight into what mod(s) have the slowest handling
of this event.
This commit is contained in:
embeddedt 2025-04-28 10:59:49 -04:00
parent 4bdddf1051
commit 59e3b83d74
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
2 changed files with 135 additions and 82 deletions

View File

@ -1,8 +1,7 @@
package org.embeddedt.modernfix.forge.dynresources;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterators;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
@ -11,12 +10,12 @@ import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.util.ForwardingInclDefaultsMap;
import org.jetbrains.annotations.Nullable;
@ -24,6 +23,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -38,18 +38,22 @@ import java.util.function.BiFunction;
* of the model registry that emulates vanilla keySet behavior.
*/
public class ModelBakeEventHelper {
// TODO: make into config option
private static final Set<String> INCOMPATIBLE_MODS = ImmutableSet.of(
"industrialforegoing",
"mekanism",
"vampirism",
"elevatorid",
"cfm",
"refinedstorage",
"embers",
"buildcraftsilicon",
"buildcrafttransport",
"buildcraftfactory");
private enum UniverseVisibility {
/**
* Mod cannot see any view of the universe of model locations.
*/
NONE,
/**
* Mod can see its own model locations and those of dependencies/dependents.
*/
SELF_AND_DEPS,
/**
* Mod can see every model location.
*/
EVERYTHING
}
private static final Map<String, UniverseVisibility> MOD_VISIBILITY_CONFIGURATION = ImmutableMap.<String, UniverseVisibility>builder()
.build();
private final Map<ResourceLocation, BakedModel> modelRegistry;
private final Set<ResourceLocation> topLevelModelLocations;
private final MutableGraph<String> dependencyGraph;
@ -57,31 +61,36 @@ public class ModelBakeEventHelper {
this.modelRegistry = modelRegistry;
this.topLevelModelLocations = new ObjectLinkedOpenHashSet<>();
// Skip going through ModelLocationCache because most of the accesses will be misses
ForgeRegistries.BLOCKS.getEntries().forEach(entry -> {
BuiltInRegistries.BLOCK.entrySet().forEach(entry -> {
var location = entry.getKey().location();
for(BlockState state : entry.getValue().getStateDefinition().getPossibleStates()) {
topLevelModelLocations.add(BlockModelShaper.stateToModelLocation(location, state));
}
});
ForgeRegistries.ITEMS.getKeys().forEach(key -> topLevelModelLocations.add(new ModelResourceLocation(key, "inventory")));
BuiltInRegistries.ITEM.keySet().forEach(key -> topLevelModelLocations.add(new ModelResourceLocation(key, "inventory")));
this.topLevelModelLocations.addAll(modelRegistry.keySet());
this.dependencyGraph = GraphBuilder.undirected().build();
this.dependencyGraph = buildDependencyGraph();
}
private static MutableGraph<String> buildDependencyGraph() {
MutableGraph<String> dependencyGraph = GraphBuilder.undirected().build();
ModList.get().forEachModContainer((id, mc) -> {
this.dependencyGraph.addNode(id);
dependencyGraph.addNode(id);
for(IModInfo.ModVersion version : mc.getModInfo().getDependencies()) {
this.dependencyGraph.addNode(version.getModId());
dependencyGraph.addNode(version.getModId());
}
});
for(String id : this.dependencyGraph.nodes()) {
for(String id : dependencyGraph.nodes()) {
Optional<? extends ModContainer> mContainer = ModList.get().getModContainerById(id);
if(mContainer.isPresent()) {
for(IModInfo.ModVersion version : mContainer.get().getModInfo().getDependencies()) {
// avoid self-loops
if(!Objects.equals(id, version.getModId()))
this.dependencyGraph.putEdge(id, version.getModId());
dependencyGraph.putEdge(id, version.getModId());
}
}
}
return dependencyGraph;
}
private static final Set<String> WARNED_MOD_IDS = new HashSet<>();
@ -132,73 +141,94 @@ public class ModelBakeEventHelper {
}
public Map<ResourceLocation, BakedModel> wrapRegistry(String modId) {
var config = MOD_VISIBILITY_CONFIGURATION.getOrDefault(modId, UniverseVisibility.EVERYTHING);
if (config == UniverseVisibility.NONE) {
return createWarningRegistry(modId);
}
final Set<String> modIdsToInclude = new HashSet<>();
modIdsToInclude.add(modId);
try {
modIdsToInclude.addAll(this.dependencyGraph.adjacentNodes(modId));
} catch(IllegalArgumentException ignored) { /* sanity check */ }
modIdsToInclude.remove("minecraft");
if(modIdsToInclude.stream().noneMatch(INCOMPATIBLE_MODS::contains))
return createWarningRegistry(modId);
Set<ResourceLocation> ourModelLocations = Sets.filter(this.topLevelModelLocations, loc -> modIdsToInclude.contains(loc.getNamespace()));
Set<ResourceLocation> ourModelLocations;
if (config == UniverseVisibility.SELF_AND_DEPS) {
ourModelLocations = Sets.filter(this.topLevelModelLocations, loc -> modIdsToInclude.contains(loc.getNamespace()));
} else {
ourModelLocations = this.topLevelModelLocations;
}
BakedModel missingModel = modelRegistry.get(ModelBakery.MISSING_MODEL_LOCATION);
return new ForwardingMap<ResourceLocation, BakedModel>() {
@Override
protected Map<ResourceLocation, BakedModel> delegate() {
return modelRegistry;
}
return new EmulatedModelRegistry(modId, modIdsToInclude, missingModel, ourModelLocations);
}
@Override
public BakedModel get(@Nullable Object key) {
BakedModel model = super.get(key);
if(model == null && key != null && modIdsToInclude.contains(((ResourceLocation)key).getNamespace())) {
ModernFix.LOGGER.warn("Model {} is missing, but was requested in model bake event. Returning missing model", key);
return missingModel;
public class EmulatedModelRegistry extends ForwardingMap<ResourceLocation, BakedModel> {
private final Set<String> modIdsToInclude;
private final BakedModel missingModel;
private final Set<ResourceLocation> ourModelLocations;
private final String modId;
private EmulatedModelRegistry(String modId, Set<String> modIdsToInclude, BakedModel missingModel, Set<ResourceLocation> ourModelLocations) {
this.modId = modId;
this.modIdsToInclude = modIdsToInclude;
this.missingModel = missingModel;
this.ourModelLocations = ourModelLocations;
}
@Override
protected Map<ResourceLocation, BakedModel> delegate() {
return modelRegistry;
}
@Override
public BakedModel get(@Nullable Object key) {
BakedModel model = super.get(key);
if(model == null && key != null && modIdsToInclude.contains(((ResourceLocation)key).getNamespace())) {
ModernFix.LOGGER.warn("Model {} is missing, but was requested in model bake event. Returning missing model", key);
return missingModel;
}
return model;
}
@Override
public Set<ResourceLocation> keySet() {
return Collections.unmodifiableSet(ourModelLocations);
}
@Override
public boolean containsKey(@Nullable Object key) {
return ourModelLocations.contains(key) || super.containsKey(key);
}
@Override
public Set<Entry<ResourceLocation, BakedModel>> entrySet() {
return new DynamicModelEntrySet(this, ourModelLocations);
}
@Override
public void replaceAll(BiFunction<? super ResourceLocation, ? super BakedModel, ? extends BakedModel> function) {
ModernFix.LOGGER.warn("Mod '{}' is calling replaceAll on the model registry. Some hacks will be used to keep this fast, but they may not be 100% compatible.", modId);
List<ResourceLocation> locations = new ArrayList<>(ourModelLocations);
for(ResourceLocation location : locations) {
/*
* Fetching every model is insanely slow. So we call the function with a null object first, since it
* probably isn't expecting that. If we get an exception thrown, or it returns nonnull, then we know
* it actually cares about the given model.
*/
boolean needsReplacement;
try {
needsReplacement = function.apply(location, null) != null;
} catch(Throwable e) {
needsReplacement = true;
}
return model;
}
@Override
public Set<ResourceLocation> keySet() {
return ourModelLocations;
}
@Override
public boolean containsKey(@Nullable Object key) {
return ourModelLocations.contains(key) || super.containsKey(key);
}
@Override
public Set<Entry<ResourceLocation, BakedModel>> entrySet() {
return new DynamicModelEntrySet(this, ourModelLocations);
}
@Override
public void replaceAll(BiFunction<? super ResourceLocation, ? super BakedModel, ? extends BakedModel> function) {
ModernFix.LOGGER.warn("Mod '{}' is calling replaceAll on the model registry. Some hacks will be used to keep this fast, but they may not be 100% compatible.", modId);
List<ResourceLocation> locations = new ArrayList<>(keySet());
for(ResourceLocation location : locations) {
/*
* Fetching every model is insanely slow. So we call the function with a null object first, since it
* probably isn't expecting that. If we get an exception thrown, or it returns nonnull, then we know
* it actually cares about the given model.
*/
boolean needsReplacement;
try {
needsReplacement = function.apply(location, null) != null;
} catch(Throwable e) {
needsReplacement = true;
}
if(needsReplacement) {
BakedModel existing = get(location);
BakedModel replacement = function.apply(location, existing);
if(replacement != existing) {
put(location, replacement);
}
if(needsReplacement) {
BakedModel existing = get(location);
BakedModel replacement = function.apply(location, existing);
if(replacement != existing) {
put(location, replacement);
}
}
}
};
}
}
private static class DynamicModelEntrySet extends AbstractSet<Map.Entry<ResourceLocation, BakedModel>> {
@ -212,7 +242,18 @@ public class ModelBakeEventHelper {
@Override
public Iterator<Map.Entry<ResourceLocation, BakedModel>> iterator() {
return Iterators.transform(Iterators.unmodifiableIterator(this.modelLocations.iterator()), DynamicModelEntry::new);
var iter = this.modelLocations.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public Map.Entry<ResourceLocation, BakedModel> next() {
return new DynamicModelEntry(iter.next());
}
};
}
@Override

View File

@ -1,6 +1,7 @@
package org.embeddedt.modernfix.forge.mixin.perf.dynamic_resources;
import com.google.common.base.Stopwatch;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.client.ForgeHooksClient;
@ -17,6 +18,8 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -32,19 +35,28 @@ public class ForgeHooksClientMixin {
ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event);
ModelBakeEventHelper helper = new ModelBakeEventHelper(bakeEvent.getModels());
Method acceptEv = ObfuscationReflectionHelper.findMethod(ModContainer.class, "acceptEvent", Event.class);
Stopwatch globalTimer = Stopwatch.createStarted();
Map<String, Stopwatch> times = new Object2ObjectOpenHashMap<>();
ModList.get().forEachModContainer((id, mc) -> {
Map<ResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id);
ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getModelBakery());
Stopwatch timer = Stopwatch.createStarted();
Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createStarted());
try {
acceptEv.invoke(mc, postedEvent);
} catch(ReflectiveOperationException e) {
e.printStackTrace();
}
timer.stop();
if(timer.elapsed(TimeUnit.SECONDS) >= 1) {
ModernFix.LOGGER.warn("Mod '{}' took {} in the model bake event", id, timer);
}
});
globalTimer.stop();
if (globalTimer.elapsed(TimeUnit.SECONDS) >= 1) {
ModernFix.LOGGER.warn("Posting dynamic ModelEvent.ModifyBakingResult to mods took {}, breakdown below:", globalTimer);
times.entrySet().stream()
.sorted(Comparator.<Map.Entry<String, Stopwatch>, Duration>comparing(e -> e.getValue().elapsed()).reversed())
.filter(e -> e.getValue().elapsed(TimeUnit.MILLISECONDS) > 50)
.forEach(entry -> {
ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString());
});
}
}
}