Add Fabric Model Loading API support

This commit is contained in:
embeddedt 2024-12-12 22:49:12 -05:00
parent e7e065f809
commit a0cc8bfbd2
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
4 changed files with 197 additions and 17 deletions

View File

@ -20,6 +20,8 @@ dependencies {
}
modCompileOnly "curse.maven:spark-361579:${rootProject.spark_version}"
modCompileOnly fabricApi.module("fabric-model-loading-api-v1", rootProject.fabric_api_version)
// Remove the next line if you don't want to depend on the API
// modApi "me.shedaniel:architectury:${rootProject.architectury_version}"
}

View File

@ -43,6 +43,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.ModernFix;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.lang.ref.WeakReference;
@ -50,6 +51,7 @@ import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -97,6 +99,9 @@ public class DynamicModelProvider {
private final Map<ModelResourceLocation, BakedModel> mrlModelOverrides = new ConcurrentHashMap<>();
private final Map<ResourceLocation, ItemModel> itemStackModelOverrides = new ConcurrentHashMap<>();
private final Map<ResourceLocation, BakedModel> standaloneModelOverrides = new ConcurrentHashMap<>();
private final Map<ModelResourceLocation, UnbakedBlockStateModel> unbakedBlockStateModelOverrides = new ConcurrentHashMap<>();
private final List<DynamicModelProvider.DynamicModelPlugin> pluginList = new ArrayList<>();
private static final boolean DEBUG_DYNAMIC_MODEL_LOADING = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@ -129,6 +134,12 @@ public class DynamicModelProvider {
this.itemModelGenerator = new ItemModelGenerator();
this.missingModel = this.bakeModel(this.unbakedMissingModel, () -> "missing");
this.missingItemModel = new MissingItemModel(this.missingModel);
try {
Class.forName("net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin");
pluginList.add(new FabricDynamicModelHandler(this));
} catch(Exception ignored) {
// Fabric API likely not present
}
}
public BakedModel getMissingBakedModel() {
@ -329,7 +340,21 @@ public class DynamicModelProvider {
ModernFix.LOGGER.error("Failed to load blockstate definition {} from pack '{}'", location, resource.sourcePackId(), e);
}
}
return Optional.of(BlockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions, this.unbakedMissingModel));
var loadedModels = new HashMap<>(BlockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions, this.unbakedMissingModel).models());
if (!pluginList.isEmpty()) {
loadedModels.replaceAll((mrl, oldModel) -> {
UnbakedBlockStateModel ubm = oldModel.model();
for (var plugin : pluginList) {
ubm = plugin.modifyBlockModelOnLoad(ubm, mrl, oldModel.state());
}
if (ubm == oldModel.model()) {
return oldModel;
} else {
return new BlockStateModelLoader.LoadedModel(oldModel.state(), ubm);
}
});
}
return Optional.of(new BlockStateModelLoader.LoadedModels(loadedModels));
}
private BakedModel bakeModel(UnbakedModel model, ModelDebugName name) {
@ -364,15 +389,18 @@ public class DynamicModelProvider {
if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) {
return this.loadStandaloneModel(location.id());
} else {
var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id());
Optional<UnbakedBlockStateModel> unbakedModelOpt = optLoadedModels.map(loadedModels -> {
var loadedModel = loadedModels.models().get(location);
if(loadedModel != null) {
return loadedModel.model();
} else {
return null;
}
});
Optional<UnbakedBlockStateModel> unbakedModelOpt = Optional.ofNullable(this.unbakedBlockStateModelOverrides.get(location));
if (unbakedModelOpt.isEmpty()) {
var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id());
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::toString);
});
@ -389,12 +417,14 @@ public class DynamicModelProvider {
});
}
private Optional<UnbakedModel> loadBlockModel(ResourceLocation location) {
private Optional<UnbakedModel> loadBlockModelDefault(ResourceLocation location) {
if (DEBUG_DYNAMIC_MODEL_LOADING) {
ModernFix.LOGGER.info("Loading block model '{}'", location);
}
if (location.equals(ItemModelGenerator.GENERATED_ITEM_MODEL_ID)) {
return Optional.of(this.itemModelGenerator);
} else if (location.equals(MissingBlockModel.LOCATION)) {
return Optional.of(this.unbakedMissingModel);
}
var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "models/" + location.getPath() + ".json"));
if(resource.isPresent()) {
@ -411,6 +441,15 @@ public class DynamicModelProvider {
}
}
private Optional<UnbakedModel> loadBlockModel(ResourceLocation location) {
Optional<UnbakedModel> value = loadBlockModelDefault(location);
for (var plugin : this.pluginList) {
value = plugin.modifyModelOnLoad(value, location);
}
return value;
}
private Optional<ClientItem> loadClientItemProperties(ResourceLocation location) {
if (DEBUG_DYNAMIC_MODEL_LOADING) {
ModernFix.LOGGER.info("Loading client item '{}'", location);
@ -460,6 +499,10 @@ public class DynamicModelProvider {
return this.loadedStandaloneModels.getUnchecked(location).orElse(this.missingModel);
}
public void addUnbakedBlockStateOverride(ModelResourceLocation location, UnbakedBlockStateModel model) {
this.unbakedBlockStateModelOverrides.put(location, model);
}
private class DynamicBaker implements ModelBaker {
private final ModelDebugName modelDebugName;
@ -469,7 +512,7 @@ public class DynamicModelProvider {
@Override
public BakedModel bake(ResourceLocation location, ModelState transform) {
return DynamicModelProvider.this.loadBlockModel(location).map(unbakedModel -> {
return DynamicModelProvider.this.loadedBlockModels.getUnchecked(location).map(unbakedModel -> {
DynamicModelProvider.this.resolver.clearResolver();
unbakedModel.resolveDependencies(DynamicModelProvider.this.resolver);
return UnbakedModel.bakeWithTopModelValues(unbakedModel, this, transform);
@ -526,4 +569,9 @@ public class DynamicModelProvider {
public interface ModelManagerExtension {
DynamicModelProvider mfix$getModelProvider();
}
public interface DynamicModelPlugin {
Optional<UnbakedModel> modifyModelOnLoad(Optional<UnbakedModel> model, ResourceLocation id);
UnbakedBlockStateModel modifyBlockModelOnLoad(UnbakedBlockStateModel model, ModelResourceLocation id, BlockState state);
}
}

View File

@ -0,0 +1,133 @@
package org.embeddedt.modernfix.dynamicresources;
import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.ModernFix;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class FabricDynamicModelHandler implements DynamicModelProvider.DynamicModelPlugin {
private final List<ModelLoadingPlugin> pluginList;
// Borrowed from Fabric API, this dispatching logic is extremely trivial
private static final ResourceLocation[] MODEL_MODIFIER_PHASES = new ResourceLocation[] { ModelModifier.OVERRIDE_PHASE, ModelModifier.DEFAULT_PHASE, ModelModifier.WRAP_PHASE, ModelModifier.WRAP_LAST_PHASE };
private final Event<ModelModifier.OnLoad> onLoadModifiers = EventFactory.createWithPhases(ModelModifier.OnLoad.class, modifiers -> (model, context) -> {
for (ModelModifier.OnLoad modifier : modifiers) {
try {
model = modifier.modifyModelOnLoad(model, context);
} catch (Exception exception) {
ModernFix.LOGGER.error("Failed to modify unbaked model on load", exception);
}
}
return model;
}, MODEL_MODIFIER_PHASES);
private final Event<ModelModifier.OnLoadBlock> onLoadBlockModifiers = EventFactory.createWithPhases(ModelModifier.OnLoadBlock.class, modifiers -> (model, context) -> {
for (ModelModifier.OnLoadBlock modifier : modifiers) {
try {
model = modifier.modifyModelOnLoad(model, context);
} catch (Exception exception) {
ModernFix.LOGGER.error("Failed to modify unbaked block model on load", exception);
}
}
return model;
}, MODEL_MODIFIER_PHASES);
public FabricDynamicModelHandler(DynamicModelProvider provider) {
this.pluginList = ModelLoadingPlugin.getAll();
var context = new PluginContext(provider);
for (var plugin : this.pluginList) {
plugin.initialize(context);
}
context.fireResolvers();
}
@Override
public Optional<UnbakedModel> modifyModelOnLoad(Optional<UnbakedModel> model, ResourceLocation id) {
return Optional.ofNullable(this.onLoadModifiers.invoker().modifyModelOnLoad(model.orElse(null), () -> id));
}
@Override
public UnbakedBlockStateModel modifyBlockModelOnLoad(UnbakedBlockStateModel model, ModelResourceLocation id, BlockState state) {
return this.onLoadBlockModifiers.invoker().modifyModelOnLoad(model, new ModelModifier.OnLoadBlock.Context() {
@Override
public ModelResourceLocation id() {
return id;
}
@Override
public BlockState state() {
return state;
}
});
}
private class PluginContext implements ModelLoadingPlugin.Context {
private final DynamicModelProvider provider;
private final Map<Block, BlockStateResolver> resolvers = new HashMap<>();
private PluginContext(DynamicModelProvider provider) {
this.provider = provider;
}
@Override
public void addModels(ResourceLocation... ids) {
/* no-op on dynamic model loader */
}
@Override
public void addModels(Collection<? extends ResourceLocation> ids) {
/* no-op on dynamic model loader */
}
@Override
public void registerBlockStateResolver(Block block, BlockStateResolver resolver) {
resolvers.put(block, resolver);
}
public void fireResolvers() {
resolvers.forEach((block, resolver) -> {
resolver.resolveBlockStates(new BlockStateResolver.Context() {
@Override
public Block block() {
return block;
}
@Override
public void setModel(BlockState state, UnbakedBlockStateModel model) {
provider.addUnbakedBlockStateOverride(BlockModelShaper.stateToModelLocation(state), model);
}
});
});
}
@Override
public Event<ModelModifier.OnLoad> modifyModelOnLoad() {
return onLoadModifiers;
}
@Override
public Event<ModelModifier.OnLoadBlock> modifyBlockModelOnLoad() {
return onLoadBlockModifiers;
}
}
}

View File

@ -36,13 +36,10 @@ dependencies {
modCompileOnly(fabricApi.module("fabric-model-loading-api-v1", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' }
modCompileOnly(fabricApi.module("fabric-resource-loader-v0", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' }
modCompileOnly(fabricApi.module("fabric-data-generation-api-v1", rootProject.fabric_api_version)) { exclude group: 'net.fabricmc', module: 'fabric-loader' }
modCompileOnly("com.terraformersmc:modmenu:${rootProject.modmenu_version}") { transitive false }
modCompileOnly "curse.maven:spark-361579:${rootProject.spark_version}"
if(project.use_fabric_api_at_runtime.toBoolean()) {
modImplementation("com.terraformersmc:modmenu:${rootProject.modmenu_version}") { transitive false }
modImplementation "curse.maven:spark-361579:${rootProject.spark_version}"
modRuntimeOnly("net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}") { exclude group: 'net.fabricmc', module: 'fabric-loader' }
} else {
modCompileOnly("com.terraformersmc:modmenu:${rootProject.modmenu_version}") { transitive false }
modCompileOnly "curse.maven:spark-361579:${rootProject.spark_version}"
}
// Remove the next line if you don't want to depend on the API