package org.embeddedt.modernfix.dynresources; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.Maps; import it.unimi.dsi.fastutil.objects.AbstractObject2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectSet; import it.unimi.dsi.fastutil.objects.ObjectSets; import it.unimi.dsi.fastutil.objects.ReferenceSets; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.renderer.block.model.ItemModelGenerator; import net.minecraft.client.resources.model.BlockStateModelLoader; import net.minecraft.client.resources.model.ClientItemInfoLoader; import net.minecraft.client.resources.model.MissingBlockModel; import net.minecraft.client.resources.model.ModelDiscovery; import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ResolvedModel; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.Resource; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.neoforged.neoforge.client.model.UnbakedModelParser; import net.neoforged.neoforge.client.model.standalone.StandaloneModelLoader; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.IdMapperAccessor; import org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ModelDiscoveryAccessor; import java.io.Reader; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; public class DynamicModelSystem { private static final FileToIdConverter MODEL_LISTER = FileToIdConverter.json("models"); private static final FileToIdConverter BLOCKSTATE_LISTER = FileToIdConverter.json("blockstates"); public static final boolean DEBUG_DYNAMIC_MODEL_LOADING = true; // Boolean.getBoolean("modernfix.debugDynamicModelLoading"); public static Map createDynamicUnbakedModelMap(Map resourceMap) { LoadingCache unbakedModelCache = CacheBuilder.newBuilder().softValues().maximumSize(1000).build(new CacheLoader<>() { @Override public UnbakedModel load(Identifier key) throws Exception { var resource = resourceMap.get(MODEL_LISTER.idToFile(key)); if (resource == null) { throw new IllegalArgumentException("Model " + key + " does not exist in map"); } if (DEBUG_DYNAMIC_MODEL_LOADING) { ModernFix.LOGGER.info("Loading unbaked model {}", key); } try (Reader reader = resource.openAsReader()) { return UnbakedModelParser.parse(reader); } } }); Set unbakedIdSet = resourceMap.keySet().stream().map(MODEL_LISTER::fileToId).collect(Collectors.toUnmodifiableSet()); return Maps.asMap(unbakedIdSet, key -> key != null ? unbakedModelCache.getUnchecked(key) : null); } public interface SingleBlockStateEntryLoader { BlockStateModelLoader.LoadedModels loadEntry(Identifier identifier, List blockstateResources); } public static Set getAllBlockStates() { return ReferenceSets.unmodifiable(((IdMapperAccessor) Block.BLOCK_STATE_REGISTRY).getReferenceMap().keySet()); } public static BlockStateModelLoader.LoadedModels createDynamicBlockStateLoadedModels(Map> resourceMap, SingleBlockStateEntryLoader entryLoader) { LoadingCache definitionCache = CacheBuilder.newBuilder().softValues().maximumSize(1000).build(new CacheLoader<>() { @Override public BlockStateModelLoader.LoadedModels load(Identifier key) throws Exception { if (DEBUG_DYNAMIC_MODEL_LOADING) { ModernFix.LOGGER.info("Loading blockstate definition for {}", key); } var file = BLOCKSTATE_LISTER.idToFile(key); var resources = resourceMap.getOrDefault(file, List.of()); return entryLoader.loadEntry(file, resources); } }); return new BlockStateModelLoader.LoadedModels(Maps.asMap(getAllBlockStates(), state -> { var identifier = state.getBlock().builtInRegistryHolder().getKey().identifier(); var loadedModels = definitionCache.getUnchecked(identifier); return loadedModels.models().get(state); })); } public record DynamicResolver(Map inputModels, BlockStateModelLoader.LoadedModels loadedModels, ClientItemInfoLoader.LoadedClientInfos loadedClientInfos, StandaloneModelLoader.LoadedModels standaloneModels) { private ResolvedModel resolveModel(Identifier id) { var discovery = new ModelDiscovery(inputModels, MissingBlockModel.missingModel()); discovery.addSpecialModel(ItemModelGenerator.GENERATED_ITEM_MODEL_ID, new ItemModelGenerator()); if (!id.equals(ItemModelGenerator.GENERATED_ITEM_MODEL_ID)) { UnbakedModel unbaked = inputModels.get(id); if (unbaked != null) { var wrapper = discovery.createAndQueueWrapper(id, unbaked); ((ModelDiscoveryAccessor)discovery).mfix$getModelWrappers().put(id, wrapper); } else { ModernFix.LOGGER.warn("Cannot find the root model for {}", id); } } var resolved = discovery.resolve(); return resolved.getOrDefault(id, discovery.missingModel()); } public ModelManager.ResolvedModels resolvedModels() { var resolvedMissingModel = new ModelDiscovery(inputModels, MissingBlockModel.missingModel()).missingModel(); LoadingCache resolvedModelCache = CacheBuilder.newBuilder().softValues().maximumSize(1000).build(new CacheLoader<>() { @Override public ResolvedModel load(Identifier key) { return resolveModel(key); } }); return new ModelManager.ResolvedModels(resolvedMissingModel, Maps.asMap(inputModels.keySet(), resolvedModelCache::getUnchecked)); } } public static class BlockGroupingMap extends AbstractObject2IntMap { private final BlockColors blockColors; private final BlockStateModelLoader.LoadedModels loadedModels; record GroupKey(Object equalityGroup, List coloringValues) {} private final Object2IntMap groupKeyToId; public BlockGroupingMap(BlockColors blockColors, BlockStateModelLoader.LoadedModels loadedModels) { this.blockColors = blockColors; this.loadedModels = loadedModels; this.groupKeyToId = new Object2IntOpenHashMap<>(); } @Override public int size() { return 0; } @Override public ObjectSet> object2IntEntrySet() { return ObjectSets.emptySet(); } @Override public int getInt(Object key) { // TODO: Implement return -1; } } public static Map createDynamicBakedRegistry(Map input, BiFunction baker) { // TODO: support persistence of overrides LoadingCache> bakedCache = CacheBuilder.newBuilder().softValues().maximumSize(1000).build(new CacheLoader<>() { @Override public Optional load(K key) throws Exception { var unbaked = input.get(key); if (unbaked != null) { if (DEBUG_DYNAMIC_MODEL_LOADING) { ModernFix.LOGGER.info("Baking {}", key); } return Optional.ofNullable(baker.apply(key, unbaked)); } else { return Optional.empty(); } } }); return Maps.asMap(input.keySet(), k -> k != null ? bakedCache.getUnchecked(k).orElse(null) : null); } }