diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockModelShaperMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockModelShaperMixin.java new file mode 100644 index 00000000..609e8fc7 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockModelShaperMixin.java @@ -0,0 +1,75 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; + +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.IModelHoldingBlockState; +import org.embeddedt.modernfix.dynamicresources.ModelLocationCache; +import org.embeddedt.modernfix.util.DynamicOverridableMap; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(BlockModelShaper.class) +@ClientOnlyMixin +public class BlockModelShaperMixin { + @Shadow @Final private ModelManager modelManager; + + @Shadow + private Map modelByStateCache; + + @Inject(method = { "", "replaceCache" }, at = @At("RETURN")) + private void replaceModelMap(CallbackInfo ci) { + // replace the backing map for mods which will access it + this.modelByStateCache = new DynamicOverridableMap<>(BlockState.class, state -> modelManager.getModel(ModelLocationCache.get(state))); + // Clear the cached models on blockstate objects + for(Block block : BuiltInRegistries.BLOCK) { + for(BlockState state : block.getStateDefinition().getPossibleStates()) { + if(state instanceof IModelHoldingBlockState modelHolder) { + modelHolder.mfix$setModel(null); + } + } + } + } + + private BakedModel cacheBlockModel(BlockState state) { + // Do all model system accesses in the unlocked path + ModelResourceLocation mrl = ModelLocationCache.get(state); + BakedModel model = mrl == null ? null : modelManager.getModel(mrl); + if (model == null) { + model = modelManager.getMissingModel(); + } + + return model; + } + + /** + * @author embeddedt + * @reason get the model from the dynamic model provider + */ + @Overwrite + public BakedModel getBlockModel(BlockState state) { + if(state instanceof IModelHoldingBlockState modelHolder) { + BakedModel model = modelHolder.mfix$getModel(); + + if(model != null) { + return model; + } + + model = this.cacheBlockModel(state); + modelHolder.mfix$setModel(model); + return model; + } else { + return this.cacheBlockModel(state); + } + } +} + diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockStateBaseMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockStateBaseMixin.java new file mode 100644 index 00000000..1948eca2 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/BlockStateBaseMixin.java @@ -0,0 +1,27 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; + +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.world.level.block.state.BlockBehaviour; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.IModelHoldingBlockState; +import org.spongepowered.asm.mixin.Mixin; + +import java.lang.ref.SoftReference; + +@Mixin(BlockBehaviour.BlockStateBase.class) +@ClientOnlyMixin +public class BlockStateBaseMixin implements IModelHoldingBlockState { + private volatile SoftReference mfix$model; + + @Override + public BakedModel mfix$getModel() { + var ref = mfix$model; + return ref != null ? ref.get() : null; + } + + @Override + public void mfix$setModel(BakedModel model) { + mfix$model = model != null ? new SoftReference<>(model) : null; + } +} + diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java new file mode 100644 index 00000000..b0d2e073 --- /dev/null +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java @@ -0,0 +1,102 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.geom.EntityModelSet; +import net.minecraft.client.renderer.block.model.BlockModel; +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.renderer.item.ItemModel; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.client.resources.model.UnbakedModel; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.block.state.BlockState; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.dynamicresources.DynamicModelProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ModelManager.class) +@ClientOnlyMixin +public class ModelManagerMixin { + @Shadow private BakedModel missingModel; + @Shadow private ItemModel missingItemModel; + @Shadow private EntityModelSet entityModelSet; + @Unique + private DynamicModelProvider mfix$modelProvider; + + @Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockModels(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) + private CompletableFuture> deferBlockModelLoad(ResourceManager manager, Executor executor) { + return CompletableFuture.completedFuture(Map.of()); + } + + @Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader;loadBlockStates(Lnet/minecraft/client/resources/model/UnbakedModel;Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) + private CompletableFuture deferBlockStateLoad(UnbakedModel unbakedModel, ResourceManager resourceManager, Executor executor) { + return CompletableFuture.completedFuture(new BlockStateModelLoader.LoadedModels(Map.of())); + } + + /** + * @author embeddedt + * @reason disable map creation + */ + @Overwrite + private static Map createBlockStateToModelDispatch(Map map, BakedModel bakedModel) { + return Map.of(); + } + + @Inject(method = "apply", at = @At("RETURN")) + private void createModelProvider(ModelManager.ReloadState reloadState, ProfilerFiller profiler, CallbackInfo ci) { + this.mfix$modelProvider = new DynamicModelProvider( + null, // TODO + this.missingModel, + this.missingItemModel, + Minecraft.getInstance().getResourceManager(), + this.entityModelSet, + reloadState.atlasPreparations() + ); + } + + /** + * @author embeddedt + * @reason use dynamic model system + */ + @Overwrite + public BakedModel getModel(ModelResourceLocation modelLocation) { + if(this.mfix$modelProvider != null) { + return this.mfix$modelProvider.getModel(modelLocation); + } else { + return this.missingModel; + } + } + + /** + * @author embeddedt + * @reason use dynamic model system + */ + @Overwrite + public ItemModel getItemModel(ResourceLocation resourceLocation) { + return this.mfix$modelProvider.getItemModel(resourceLocation); + } + + /** + * @author embeddedt + * @reason use dynamic model system + */ + @Overwrite + public ClientItem.Properties getItemProperties(ResourceLocation resourceLocation) { + return this.mfix$modelProvider.getClientItemProperties(resourceLocation); + } +} diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java index ff809650..e5205f1f 100644 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java +++ b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java @@ -4,15 +4,30 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.client.model.geom.EntityModelSet; import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.renderer.block.model.BlockModelDefinition; +import net.minecraft.client.renderer.block.model.ItemModelGenerator; +import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel; +import net.minecraft.client.renderer.item.ClientItem; +import net.minecraft.client.renderer.item.ItemModel; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.resources.model.AtlasSet; import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.BlockModelRotation; import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.MissingBlockModel; +import net.minecraft.client.resources.model.ModelBaker; import net.minecraft.client.resources.model.ModelBakery; +import net.minecraft.client.resources.model.ModelDebugName; import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.client.resources.model.ModelState; +import net.minecraft.client.resources.model.SpriteGetter; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.Resource; @@ -38,7 +53,6 @@ import java.util.stream.Collectors; * Handles loading models dynamically, rather than at startup time. */ public class DynamicModelProvider { - /* private final LoadingCache> loadedStateDefinitions = CacheBuilder.newBuilder() .expireAfterAccess(3, TimeUnit.MINUTES) @@ -65,19 +79,6 @@ public class DynamicModelProvider { } }); - private final LoadingCache> loadedUnbakedModels = - CacheBuilder.newBuilder() - .expireAfterAccess(3, TimeUnit.MINUTES) - .maximumSize(1000) - .concurrencyLevel(8) - .softValues() - .build(new CacheLoader<>() { - @Override - public Optional load(ModelResourceLocation key) { - return loadModel(key); - } - }); - private final LoadingCache> loadedBakedModels = CacheBuilder.newBuilder() .expireAfterAccess(3, TimeUnit.MINUTES) @@ -91,35 +92,72 @@ public class DynamicModelProvider { } }); - private final Map initialBakedRegistry; + private final LoadingCache> loadedClientItemProperties = + CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(1000) + .concurrencyLevel(8) + .softValues() + .build(new CacheLoader<>() { + @Override + public Optional load(ResourceLocation key) { + return loadClientItemProperties(key); + } + }); + + private final LoadingCache> loadedItemModels = + CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(1000) + .concurrencyLevel(8) + .softValues() + .build(new CacheLoader<>() { + @Override + public Optional load(ResourceLocation key) { + return loadItemModel(key); + } + }); + private final BakedModel missingModel; + private final ItemModel missingItemModel; private final UnbakedModel unbakedMissingModel; private final Function> stateMapper; private final ResourceManager resourceManager; - private final BlockStateModelLoader blockStateModelLoader; private final ModelBakery.TextureGetter textureGetter; - private final DynamicMap fakeUnbakedModelMap; private final DynamicResolver resolver; + private final EntityModelSet entityModelSet; + private final ItemModelGenerator itemModelGenerator; - public DynamicModelProvider(Map initialBakedRegistry, BakedModel missingModel, ResourceManager resourceManager, Map atlasMap) { - this.initialBakedRegistry = initialBakedRegistry; + public DynamicModelProvider(Map initialBakedRegistry, BakedModel missingModel, + ItemModel missingItemModel, ResourceManager resourceManager, EntityModelSet entityModelSet, + Map atlasMap) { this.missingModel = missingModel; - this.textureGetter = (mrl, material) -> { - var atlas = atlasMap.get(material.atlasLocation()); - var sprite = atlas.getSprite(material.texture()); - if (sprite != null) { - return sprite; - } else { - ModernFix.LOGGER.warn("Unable to find sprite '{}' referenced by model '{}'", material.texture(), mrl); - return atlas.missing(); + this.missingItemModel = missingItemModel; + this.entityModelSet = entityModelSet; + var missing = atlasMap.get(TextureAtlas.LOCATION_BLOCKS).missing(); + this.textureGetter = new ModelBakery.TextureGetter() { + @Override + public TextureAtlasSprite get(ModelDebugName modelDebugName, Material material) { + var atlas = atlasMap.get(material.atlasLocation()); + var sprite = atlas.getSprite(material.texture()); + if (sprite != null) { + return sprite; + } else { + ModernFix.LOGGER.warn("Unable to find sprite '{}' referenced by model '{}'", material.texture(), modelDebugName.get()); + return missing; + } + } + + @Override + public TextureAtlasSprite reportMissingReference(ModelDebugName modelDebugName, String string) { + return missing; } }; this.stateMapper = BlockStateModelLoader.definitionLocationToBlockMapper(); this.resourceManager = resourceManager; this.unbakedMissingModel = MissingBlockModel.missingModel(); - this.blockStateModelLoader = new BlockStateModelLoader(this.unbakedMissingModel); - this.fakeUnbakedModelMap = new DynamicMap<>(ResourceLocation.class, key -> this.loadedBlockModels.getUnchecked(key).orElse(null)); this.resolver = new DynamicResolver(); + this.itemModelGenerator = new ItemModelGenerator(); } private Optional loadBlockStateDefinition(ResourceLocation location) { @@ -138,35 +176,56 @@ public class DynamicModelProvider { ModernFix.LOGGER.error("Failed to load blockstate definition {} from pack '{}'", location, resource.sourcePackId(), e); } } - return Optional.of(this.blockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions)); + return Optional.of(BlockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions, this.unbakedMissingModel)); } private BakedModel bakeModel(UnbakedModel model, ModelResourceLocation location) { synchronized (this) { this.resolver.clearResolver(); model.resolveDependencies(this.resolver); - var modelBakery = new ModelBakery(Map.of(location, model), this.fakeUnbakedModelMap, this.unbakedMissingModel); - modelBakery.bakeModels(this.textureGetter); - return modelBakery.getBakedTopLevelModels().get(location); + var modelBaker = new DynamicBaker(location::toString); + return UnbakedModel.bakeWithTopModelValues(model, modelBaker, BlockModelRotation.X0_Y0); + } + } + + private BakedModel bakeModel(UnbakedBlockStateModel model, ModelResourceLocation location) { + synchronized (this) { + this.resolver.clearResolver(); + model.resolveDependencies(this.resolver); + var modelBaker = new DynamicBaker(location::toString); + return model.bake(modelBaker); } } private Optional loadBakedModel(ModelResourceLocation location) { - var unbakedModel = this.loadedUnbakedModels.getUnchecked(location); - return unbakedModel.map(model -> this.bakeModel(model, location)); + if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) { + return this.loadedBlockModels.getUnchecked(location.id()).map(unbakedModel -> { + return this.bakeModel(unbakedModel, location); + }); + } else { + var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id()); + Optional unbakedModelOpt = optLoadedModels.map(loadedModels -> { + var loadedModel = loadedModels.models().get(location); + if(loadedModel != null) { + return loadedModel.model(); + } else { + return null; + } + }); + return unbakedModelOpt.map(unbakedModel -> { + return this.bakeModel(unbakedModel, location); + }); + } } private Optional loadBlockModel(ResourceLocation location) { - if(location.equals(SpecialModels.BUILTIN_GENERATED)) { - return Optional.of(SpecialModels.GENERATED_MARKER); - } else if(location.equals(SpecialModels.BUILTIN_BLOCK_ENTITY)) { - return Optional.of(SpecialModels.BLOCK_ENTITY_MARKER); + if (location.equals(ItemModelGenerator.GENERATED_ITEM_MODEL_ID)) { + return Optional.of(this.itemModelGenerator); } var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "models/" + location.getPath() + ".json")); if(resource.isPresent()) { try(Reader reader = resource.get().openAsReader()) { BlockModel blockModel = BlockModel.fromStream(reader); - blockModel.name = location.toString(); return Optional.of(blockModel); } catch(Exception e) { ModernFix.LOGGER.error("Failed to load block model {} from '{}'", location, resource.get().sourcePackId(), e); @@ -178,34 +237,71 @@ public class DynamicModelProvider { } } - private Optional loadModel(ModelResourceLocation location) { - if (location.variant().equals(ModelResourceLocation.INVENTORY_VARIANT)) { - return this.loadedBlockModels.getUnchecked(ResourceLocation.fromNamespaceAndPath(location.id().getNamespace(), "item/" + location.id().getPath())); - } else if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) { - return this.loadedBlockModels.getUnchecked(location.id()); + private Optional loadClientItemProperties(ResourceLocation location) { + var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "items/" + location.getPath() + ".json")); + if(resource.isPresent()) { + try(Reader reader = resource.get().openAsReader()) { + ClientItem clientItem = ClientItem.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow(); + return Optional.of(clientItem); + } catch(Exception e) { + ModernFix.LOGGER.error("Failed to load block model {} from '{}'", location, resource.get().sourcePackId(), e); + return Optional.empty(); + } } else { - var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id()); - return optLoadedModels.map(loadedModels -> { - var loadedModel = loadedModels.models().get(location); - if(loadedModel != null) { - return loadedModel.model(); - } else { - return null; - } - }); + ModernFix.LOGGER.warn("Client item '{}' does not exist in any resource packs", location); + return Optional.empty(); } } + private Optional loadItemModel(ResourceLocation location) { + return this.loadedClientItemProperties.getUnchecked(location).map(clientItem -> { + var bakingContext = new ItemModel.BakingContext(new DynamicBaker(location::toString), this.entityModelSet, this.missingItemModel); + return clientItem.model().bake(bakingContext); + }); + } + public BakedModel getModel(ModelResourceLocation location) { return this.loadedBakedModels.getUnchecked(location).orElse(this.missingModel); } - */ + public ClientItem.Properties getClientItemProperties(ResourceLocation location) { + return this.loadedClientItemProperties.getUnchecked(location).map(ClientItem::properties).orElse(ClientItem.Properties.DEFAULT); + } + + public ItemModel getItemModel(ResourceLocation location) { + return this.loadedItemModels.getUnchecked(location).orElse(this.missingItemModel); + } + + private class DynamicBaker implements ModelBaker { + private final ModelDebugName modelDebugName; + + private DynamicBaker(ModelDebugName modelDebugName) { + this.modelDebugName = modelDebugName; + } + + @Override + public BakedModel bake(ResourceLocation location, ModelState transform) { + return DynamicModelProvider.this.loadBlockModel(location).map(unbakedModel -> { + DynamicModelProvider.this.resolver.clearResolver(); + unbakedModel.resolveDependencies(DynamicModelProvider.this.resolver); + return UnbakedModel.bakeWithTopModelValues(unbakedModel, this, transform); + }).orElse(DynamicModelProvider.this.missingModel); + } + + @Override + public SpriteGetter sprites() { + return DynamicModelProvider.this.textureGetter.bind(this.modelDebugName); + } + + @Override + public ModelDebugName rootName() { + return this.modelDebugName; + } + } /** * Based on the Mojang impl but with some changes to make it slightly more efficient. */ - /* private class DynamicResolver implements UnbakedModel.Resolver { private final Set stack = new ObjectOpenHashSet<>(4); private final Set resolvedModels = new ObjectOpenHashSet<>(); @@ -236,6 +332,4 @@ public class DynamicModelProvider { this.resolvedModels.clear(); } } - - */ } diff --git a/common/src/main/resources/modernfix.accesswidener b/common/src/main/resources/modernfix.accesswidener index 66afefd0..01de1fe9 100644 --- a/common/src/main/resources/modernfix.accesswidener +++ b/common/src/main/resources/modernfix.accesswidener @@ -42,6 +42,11 @@ accessible class net/minecraft/client/resources/model/ModelBakery$BakedCacheKey accessible method net/minecraft/client/resources/model/ModelBakery$BakedCacheKey (Lnet/minecraft/resources/ResourceLocation;Lcom/mojang/math/Transformation;Z)V accessible class net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl accessible method net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/ModelBakery$TextureGetter;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V +accessible class net/minecraft/client/resources/model/ModelManager$ReloadState +accessible method net/minecraft/client/resources/model/BlockStateModelLoader definitionLocationToBlockMapper ()Ljava/util/function/Function; +accessible method net/minecraft/client/resources/model/BlockStateModelLoader loadBlockStateDefinitionStack (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/level/block/state/StateDefinition;Ljava/util/List;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedModels; +accessible class net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition +accessible method net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition (Ljava/lang/String;Lnet/minecraft/client/renderer/block/model/BlockModelDefinition;)V accessible class net/minecraft/world/level/chunk/PalettedContainer$Data accessible field net/minecraft/server/MinecraftServer resources Lnet/minecraft/server/MinecraftServer$ReloadableResources; accessible class net/minecraft/server/MinecraftServer$ReloadableResources