Update to latest Fabric

- Overhaul model loading to work with the new API. This allows for
   using the emissive texture system in a more generic way, which is
   nice!

 - Convert some of our custom models to use Fabric's model hooks (i.e.
   emitItemQuads). We don't make use of this right now, but might be
   useful for rendering tools with enchantment glints.

   Note this does /not/ change any of the turtle block entity rendering
   code to use Fabric/Forge's model code. This will be a change we want
   to make in the future.

 - Some cleanup of our config API. This fixes us printing lots of
   warnings when creating a new config file on Fabric (same bug also
   occurs on Forge, but that's a loader problem).

 - Fix a few warnings
This commit is contained in:
Jonathan Coates 2023-07-18 19:26:11 +01:00
parent c2988366d8
commit 24d74f5c80
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
28 changed files with 458 additions and 396 deletions

View File

@ -44,7 +44,7 @@ ## Using
dependencies {
// Vanilla (i.e. for multi-loader systems)
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api")
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api:$cctVersion")
// Forge Gradle
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$cctVersion")
@ -57,6 +57,19 @@ ## Using
}
```
When using ForgeGradle, you may also need to add the following:
```groovy
minecraft {
runs {
configureEach {
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
}
}
}
```
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
exposing more features.

View File

@ -7,13 +7,13 @@
# Minecraft
# MC version is specified in gradle.properties, as we need that in settings.gradle.
# Remember to update corresponding versions in fabric.mod.json/mods.toml
fabric-api = "0.80.0+1.19.4"
fabric-loader = "0.14.19"
fabric-api = "0.86.0+1.19.4"
fabric-loader = "0.14.21"
forge = "45.0.42"
forgeSpi = "6.0.0"
mixin = "0.8.5"
parchment = "2023.03.12"
parchmentMc = "1.19.3"
parchment = "2023.06.26"
parchmentMc = "1.19.4"
# Normal dependencies
asm = "9.3"

View File

@ -35,6 +35,7 @@ public enum TurtleToolDurability implements StringRepresentable {
/**
* The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s.
*/
@SuppressWarnings("deprecation")
public static final StringRepresentable.EnumCodec<TurtleToolDurability> CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
TurtleToolDurability(String serialisedName) {

View File

@ -12,7 +12,6 @@
import dev.emi.emi.api.EmiPlugin;
import dev.emi.emi.api.EmiRegistry;
import dev.emi.emi.api.stack.Comparison;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import java.util.function.BiPredicate;
@ -36,7 +35,7 @@ public void register(EmiRegistry registry) {
private static final Comparison pocketComparison = compareStacks((left, right) ->
left.getItem() instanceof PocketComputerItem && PocketComputerItem.getUpgrade(left) == PocketComputerItem.getUpgrade(right));
private static <T extends Item> Comparison compareStacks(BiPredicate<ItemStack, ItemStack> test) {
private static Comparison compareStacks(BiPredicate<ItemStack, ItemStack> test) {
return Comparison.of((left, right) -> {
ItemStack leftStack = left.getItemStack(), rightStack = right.getItemStack();
return leftStack.getItem() == rightStack.getItem() && test.test(leftStack, rightStack);

View File

@ -26,13 +26,15 @@
* <p>
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
*/
public final class ModelTransformer {
public static final int[] ORDER = new int[]{ 3, 2, 1, 0 };
public class ModelTransformer {
@SuppressWarnings("MutablePublicArray") // It's not nice, but is efficient.
public static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
public static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
private final Matrix4f transformation;
private final boolean invert;
protected final Matrix4f transformation;
protected final boolean invert;
private @Nullable TransformedQuads cache;
public ModelTransformer(Transformation transformation) {
@ -60,7 +62,7 @@ private BakedQuad transformQuad(BakedQuad quad) {
for (var i = 0; i < 4; i++) {
var inStart = STRIDE * i;
// Reverse the order of the quads if we're inverting
var outStart = STRIDE * (invert ? ORDER[i] : i);
var outStart = STRIDE * (invert ? INVERSE_ORDER[i] : i);
System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
// Apply the matrix to our position

View File

@ -212,7 +212,7 @@ private static void putBulkQuadInvert(VertexConsumer buffer, PoseStack.Pose pose
var normal = matrix.transform(new Vector4f(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f)).normalize();
var vertices = quad.getVertices();
for (var vertex : ModelTransformer.ORDER) {
for (var vertex : ModelTransformer.INVERSE_ORDER) {
var i = vertex * ModelTransformer.STRIDE;
var x = Float.intBitsToFloat(vertices[i]);

View File

@ -286,8 +286,8 @@ private Stream<String> getExpectedKeys() {
turtleUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
getConfigEntries(ConfigSpec.serverSpec).map(ConfigFile.Entry::translationKey),
getConfigEntries(ConfigSpec.clientSpec).map(ConfigFile.Entry::translationKey)
ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
).flatMap(x -> x);
}
@ -321,16 +321,4 @@ private void addConfigEntry(ConfigFile.Entry value, String text) {
add(value.translationKey(), text);
add(value.translationKey() + ".tooltip", value.comment());
}
private static Stream<ConfigFile.Entry> getConfigEntries(ConfigFile spec) {
return spec.entries().flatMap(LanguageProvider::getConfigEntries);
}
private static Stream<ConfigFile.Entry> getConfigEntries(ConfigFile.Entry entry) {
if (entry instanceof ConfigFile.Value<?>) return Stream.of(entry);
if (entry instanceof ConfigFile.Group group) {
return Stream.concat(Stream.of(entry), group.children().flatMap(LanguageProvider::getConfigEntries));
}
throw new IllegalStateException("Invalid config entry " + entry);
}
}

View File

@ -54,12 +54,6 @@ non-sealed interface Value<T> extends Entry, Supplier<T> {
* A group of config entries.
*/
non-sealed interface Group extends Entry {
/**
* Get all entries in this group.
*
* @return All child entries.
*/
Stream<Entry> children();
}
/**

View File

@ -7,7 +7,6 @@
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
/**
@ -38,12 +37,6 @@ public void setValue(Iterable<K> key, V value) {
getChild(key).current = value;
}
public Stream<V> children() {
return children == null
? Stream.empty()
: children.values().stream().map(x -> x.current).filter(Objects::nonNull);
}
public Stream<V> stream() {
return Stream.concat(
current == null ? Stream.empty() : Stream.of(current),

View File

@ -1,16 +1,7 @@
{
"parent": "minecraft:block/block",
"parent": "minecraft:block/orientable",
"render_type": "cutout",
"textures": {
"particle": "#front"
},
"display": {
"firstperson_righthand": {
"rotation": [ 0, 135, 0 ],
"translation": [ 0, 0, 0 ],
"scale": [ 0.40, 0.40, 0.40 ]
}
},
"computercraft:emissive_texture": "cursor",
"elements": [
{
"from": [ 0, 0, 0 ],

View File

@ -4,6 +4,7 @@
package dan200.computercraft.core;
import com.google.common.base.Splitter;
import dan200.computercraft.api.filesystem.WritableMount;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
@ -346,10 +347,10 @@ public final void start(Map<?, ?> tests) throws LuaException {
var details = (Map<?, ?>) entry.getValue();
var def = (String) details.get("definition");
var parts = name.split("\0");
var parts = Splitter.on('\0').splitToList(name);
var builder = root;
for (var i = 0; i < parts.length - 1; i++) builder = builder.get(parts[i]);
builder.runs(parts[parts.length - 1], def, () -> {
for (var i = 0; i < parts.size() - 1; i++) builder = builder.get(parts.get(i));
builder.runs(parts.get(parts.size() - 1), def, () -> {
// Run it
lock.lockInterruptibly();
try {

View File

@ -5,8 +5,7 @@
package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.model.EmissiveComputerModel;
import dan200.computercraft.client.model.turtle.TurtleModelLoader;
import dan200.computercraft.client.model.CustomModelLoader;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
@ -15,7 +14,7 @@
import dan200.computercraft.shared.platform.NetworkHandler;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
@ -40,9 +39,12 @@ public static void init() {
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMainThread();
ModelLoadingRegistry.INSTANCE.registerModelProvider((manager, out) -> ClientRegistry.registerExtraModels(out));
ModelLoadingRegistry.INSTANCE.registerResourceProvider(loader -> (path, ctx) -> TurtleModelLoader.load(loader, path));
ModelLoadingRegistry.INSTANCE.registerResourceProvider(loader -> (path, ctx) -> EmissiveComputerModel.load(loader, path));
PreparableModelLoadingPlugin.register(CustomModelLoader::prepare, (state, context) -> {
ClientRegistry.registerExtraModels(context::addModels);
context.resolveModel().register(ctx -> state.loadModel(ctx.id()));
context.modifyModelAfterBake().register((model, ctx) -> state.wrapModel(ctx, model));
});
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_NORMAL.get(), RenderType.cutout());
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_COMMAND.get(), RenderType.cutout());
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_ADVANCED.get(), RenderType.cutout());

View File

@ -4,24 +4,33 @@
package dan200.computercraft.client.model;
import net.fabricmc.fabric.api.renderer.v1.model.FabricBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* A {@link BakedModel} formed from two or more other models stitched together.
*/
public class CompositeBakedModel extends CustomBakedModel {
public class CompositeBakedModel extends ForwardingBakedModel {
private final boolean isVanillaAdapter;
private final List<BakedModel> models;
public CompositeBakedModel(List<BakedModel> models) {
super(models.get(0));
wrapped = models.get(0);
isVanillaAdapter = models.stream().allMatch(FabricBakedModel::isVanillaAdapter);
this.models = models;
}
@ -29,6 +38,11 @@ public static BakedModel of(List<BakedModel> models) {
return models.size() == 1 ? models.get(0) : new CompositeBakedModel(models);
}
@Override
public boolean isVanillaAdapter() {
return isVanillaAdapter;
}
@Override
public List<BakedQuad> getQuads(@Nullable BlockState blockState, @Nullable Direction face, RandomSource rand) {
@SuppressWarnings({ "unchecked", "rawtypes" })
@ -39,6 +53,16 @@ public List<BakedQuad> getQuads(@Nullable BlockState blockState, @Nullable Direc
return new ConcatListView(quads);
}
@Override
public void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
for (var model : models) model.emitBlockQuads(blockView, state, pos, randomSupplier, context);
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
for (var model : models) model.emitItemQuads(stack, randomSupplier, context);
}
private static final class ConcatListView extends AbstractList<BakedQuad> {
private final List<BakedQuad>[] quads;

View File

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import java.util.List;
import java.util.function.Supplier;
/**
* A subclass of {@link ForwardingBakedModel} which doesn't forward rendering.
*/
public abstract class CustomBakedModel extends ForwardingBakedModel {
public CustomBakedModel(BakedModel wrapped) {
this.wrapped = wrapped;
}
@Override
public abstract List<BakedQuad> getQuads(@Nullable BlockState blockState, @Nullable Direction face, RandomSource rand);
@Override
public final void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
context.bakedModelConsumer().accept(this);
}
@Override
public final void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
context.bakedModelConsumer().accept(this);
}
}

View File

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.model.turtle.UnbakedTurtleModel;
import dan200.computercraft.mixin.client.BlockModelAccessor;
import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/**
* Provides custom model loading for various CC models.
* <p>
* This is used from a {@link PreparableModelLoadingPlugin}, which {@linkplain #prepare(ResourceManager, Executor) loads
* data from disk} in parallel with other loader plugins, and then hooks into the model loading pipeline
* ({@link #loadModel(ResourceLocation)}, {@link #wrapModel(ModelModifier.AfterBake.Context, BakedModel)}).
*
* @see EmissiveBakedModel
* @see UnbakedTurtleModel
*/
public final class CustomModelLoader {
private static final Logger LOG = LoggerFactory.getLogger(CustomModelLoader.class);
private static final FileToIdConverter converter = FileToIdConverter.json("models");
private final Map<ResourceLocation, UnbakedModel> models = new HashMap<>();
private final Map<ResourceLocation, String> emissiveModels = new HashMap<>();
private CustomModelLoader() {
}
public static CompletableFuture<CustomModelLoader> prepare(ResourceManager resources, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
var loader = new CustomModelLoader();
for (var resource : resources.listResources("models", x -> x.getNamespace().equals(ComputerCraftAPI.MOD_ID) && x.getPath().endsWith(".json")).entrySet()) {
loader.loadModel(resource.getKey(), resource.getValue());
}
return loader;
}, executor);
}
private void loadModel(ResourceLocation path, Resource resource) {
var id = converter.fileToId(path);
try {
JsonObject model;
try (Reader reader = resource.openAsReader()) {
model = GsonHelper.parse(reader).getAsJsonObject();
}
var loader = GsonHelper.getAsString(model, "loader", null);
if (loader != null) {
var unbaked = switch (loader) {
case ComputerCraftAPI.MOD_ID + ":turtle" -> UnbakedTurtleModel.parse(model);
default -> throw new JsonParseException("Unknown model loader " + loader);
};
models.put(id, unbaked);
}
var emissive = GsonHelper.getAsString(model, "computercraft:emissive_texture", null);
if (emissive != null) emissiveModels.put(id, emissive);
} catch (IllegalArgumentException | IOException | JsonParseException e) {
LOG.error("Couldn't parse model file {} from {}", id, path, e);
}
}
/**
* Load a custom model. This searches for CC models with a custom {@code loader} field.
*
* @param path The path of the model to load.
* @return The unbaked model that has been loaded, or {@code null} if the model should be loaded as a vanilla model.
*/
public @Nullable UnbakedModel loadModel(ResourceLocation path) {
return path.getNamespace().equals(ComputerCraftAPI.MOD_ID) ? models.get(path) : null;
}
/**
* Wrap a baked model.
* <p>
* This just finds models which specify an emissive texture ({@code computercraft:emissive_texture} in the JSON) and
* wraps them in a {@link EmissiveBakedModel}.
*
* @param ctx The current model loading context.
* @param baked The baked model to wrap.
* @return The wrapped model.
*/
public BakedModel wrapModel(ModelModifier.AfterBake.Context ctx, BakedModel baked) {
if (!ctx.id().getNamespace().equals(ComputerCraftAPI.MOD_ID)) return baked;
if (!(ctx.sourceModel() instanceof BlockModel model)) return baked;
var emissive = getEmissive(ctx.id(), model);
return emissive == null ? baked : EmissiveBakedModel.wrap(baked, ctx.textureGetter().apply(model.getMaterial(emissive)));
}
private @Nullable String getEmissive(ResourceLocation id, BlockModel model) {
while (true) {
var emissive = emissiveModels.get(id);
if (emissive != null) return emissive;
id = ((BlockModelAccessor) model).computercraft$getParentLocation();
model = ((BlockModelAccessor) model).computercraft$getParent();
if (id == null || model == null) return null;
}
}
}

View File

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import net.fabricmc.fabric.api.renderer.v1.RendererAccess;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import java.util.function.Supplier;
/**
* Wraps an arbitrary {@link BakedModel} to render a single texture as emissive.
* <p>
* While Fabric has a quite advanced rendering extension API (including support for custom materials), but unlike Forge
* it doesn't expose this in the model JSON (though externals mods like <a href="https://github.com/vram-guild/json-model-extensions/">JMX</a>
* do handle this).
* <p>
* Instead, we support emissive quads by injecting a {@linkplain CustomModelLoader custom model loader} which wraps the
* baked model in a {@link EmissiveBakedModel}, which renders specific quads as emissive.
*/
public final class EmissiveBakedModel extends ForwardingBakedModel {
private final TextureAtlasSprite emissiveTexture;
private final RenderMaterial defaultMaterial;
private final RenderMaterial emissiveMaterial;
private EmissiveBakedModel(BakedModel wrapped, TextureAtlasSprite emissiveTexture, RenderMaterial defaultMaterial, RenderMaterial emissiveMaterial) {
this.wrapped = wrapped;
this.emissiveTexture = emissiveTexture;
this.defaultMaterial = defaultMaterial;
this.emissiveMaterial = emissiveMaterial;
}
public static BakedModel wrap(BakedModel model, TextureAtlasSprite emissiveTexture) {
var renderer = RendererAccess.INSTANCE.getRenderer();
return renderer == null ? model : new EmissiveBakedModel(
model,
emissiveTexture,
renderer.materialFinder().find(),
renderer.materialFinder().emissive(true).find()
);
}
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
emitQuads(context, state, randomSupplier.get());
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
emitQuads(context, null, randomSupplier.get());
}
private void emitQuads(RenderContext context, @Nullable BlockState state, RandomSource random) {
var emitter = context.getEmitter();
for (var faceIdx = 0; faceIdx <= ModelHelper.NULL_FACE_ID; faceIdx++) {
var cullFace = ModelHelper.faceFromIndex(faceIdx);
var quads = wrapped.getQuads(state, cullFace, random);
var count = quads.size();
for (var i = 0; i < count; i++) {
final var q = quads.get(i);
emitter.fromVanilla(q, q.getSprite() == emissiveTexture ? emissiveMaterial : defaultMaterial, cullFace);
emitter.emit();
}
}
}
}

View File

@ -1,172 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.google.gson.JsonObject;
import com.mojang.datafixers.util.Either;
import dan200.computercraft.api.ComputerCraftAPI;
import net.fabricmc.fabric.api.client.model.ModelProviderException;
import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
import net.fabricmc.fabric.api.renderer.v1.RendererAccess;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.fabricmc.fabric.api.renderer.v1.model.FabricBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.RandomSource;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Wraps a computer's {@link BlockModel}/{@link BakedModel} to render the computer's cursor as an emissive quad.
* <p>
* While Fabric has a quite advanced rendering extension API (including support for custom materials), but unlike Forge
* it doesn't expose this in the model JSON (though externals mods like <a href="https://github.com/vram-guild/json-model-extensions/">JMX</a>
* do handle this).
* <p>
* Instead, we support emissive quads by injecting a custom {@linkplain ModelResourceProvider model loader/provider}
* which targets a hard-coded list of computer models, and wraps the returned model in a custom
* {@linkplain FabricBakedModel} implementation which renders specific quads as emissive.
* <p>
* See also the <code>assets/computercraft/models/block/computer_on.json</code> model, which is the base for all
* emissive computer models.
*/
public final class EmissiveComputerModel {
private static final Set<String> MODELS = Set.of(
"item/computer_advanced",
"block/computer_advanced_on",
"block/computer_advanced_blinking",
"item/computer_command",
"block/computer_command_on",
"block/computer_command_blinking",
"item/computer_normal",
"block/computer_normal_on",
"block/computer_normal_blinking"
);
private EmissiveComputerModel() {
}
public static @Nullable UnbakedModel load(ResourceManager resources, ResourceLocation path) throws ModelProviderException {
if (!path.getNamespace().equals(ComputerCraftAPI.MOD_ID) || !MODELS.contains(path.getPath())) return null;
JsonObject json;
try (var reader = resources.openAsReader(new ResourceLocation(path.getNamespace(), "models/" + path.getPath() + ".json"))) {
json = GsonHelper.parse(reader).getAsJsonObject();
} catch (IOException e) {
throw new ModelProviderException("Failed loading model " + path, e);
}
// Parse a subset of the model JSON
var parent = new ResourceLocation(GsonHelper.getAsString(json, "parent"));
Map<String, Either<Material, String>> textures = new HashMap<>();
if (json.has("textures")) {
var jsonObject = GsonHelper.getAsJsonObject(json, "textures");
for (var entry : jsonObject.entrySet()) {
var texture = entry.getValue().getAsString();
textures.put(entry.getKey(), texture.startsWith("#")
? Either.right(texture.substring(1))
: Either.left(new Material(InventoryMenu.BLOCK_ATLAS, new ResourceLocation(texture)))
);
}
}
return new Unbaked(parent, textures);
}
/**
* An {@link UnbakedModel} which wraps the returned model using {@link Baked}.
* <p>
* This subclasses {@link BlockModel} to allow using these models as a parent of other models.
*/
private static final class Unbaked extends BlockModel {
Unbaked(ResourceLocation parent, Map<String, Either<Material, String>> materials) {
super(parent, List.of(), materials, null, null, ItemTransforms.NO_TRANSFORMS, List.of());
}
@Override
public BakedModel bake(ModelBaker baker, Function<Material, TextureAtlasSprite> spriteGetter, ModelState state, ResourceLocation location) {
var baked = super.bake(baker, spriteGetter, state, location);
if (!hasTexture("cursor")) return baked;
var render = RendererAccess.INSTANCE.getRenderer();
if (render == null) return baked;
return new Baked(
baked,
spriteGetter.apply(getMaterial("cursor")),
render.materialFinder().find(),
render.materialFinder().emissive(0, true).find()
);
}
}
/**
* A {@link FabricBakedModel} which renders quads using the {@code "cursor"} texture as emissive.
*/
private static final class Baked extends ForwardingBakedModel {
private final TextureAtlasSprite cursor;
private final RenderMaterial defaultMaterial;
private final RenderMaterial emissiveMaterial;
Baked(BakedModel wrapped, TextureAtlasSprite cursor, RenderMaterial defaultMaterial, RenderMaterial emissiveMaterial) {
this.wrapped = wrapped;
this.cursor = cursor;
this.defaultMaterial = defaultMaterial;
this.emissiveMaterial = emissiveMaterial;
}
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
emitQuads(context, state, randomSupplier.get());
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
emitQuads(context, null, randomSupplier.get());
}
private void emitQuads(RenderContext context, @Nullable BlockState state, RandomSource random) {
var emitter = context.getEmitter();
for (var faceIdx = 0; faceIdx <= ModelHelper.NULL_FACE_ID; faceIdx++) {
var cullFace = ModelHelper.faceFromIndex(faceIdx);
var quads = wrapped.getQuads(state, cullFace, random);
var count = quads.size();
for (var i = 0; i < count; i++) {
final var q = quads.get(i);
emitter.fromVanilla(q, q.getSprite() == cursor ? emissiveMaterial : defaultMaterial, cullFace);
emitter.emit();
}
}
}
}
}

View File

@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.mojang.math.Transformation;
import dan200.computercraft.client.model.turtle.ModelTransformer;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.core.Direction;
import org.joml.Vector3f;
/**
* Extends {@link ModelTransformer} to also work as a {@link RenderContext.QuadTransform}.
*/
public class FabricModelTransformer extends ModelTransformer implements RenderContext.QuadTransform {
public FabricModelTransformer(Transformation transformation) {
super(transformation);
}
@Override
public boolean transform(MutableQuadView quad) {
var vec3 = new Vector3f();
for (var i = 0; i < 4; i++) {
quad.copyPos(i, vec3);
transformation.transformPosition(vec3);
quad.pos(i, vec3);
}
if (invert) {
swapQuads(quad, 0, 3);
swapQuads(quad, 1, 2);
}
var face = quad.nominalFace();
if (face != null) quad.nominalFace(Direction.rotate(transformation, face));
return true;
}
private static void swapQuads(MutableQuadView quad, int a, int b) {
float aX = quad.x(a), aY = quad.y(a), aZ = quad.z(a), aU = quad.u(a), aV = quad.v(a);
float bX = quad.x(b), bY = quad.y(b), bZ = quad.z(b), bU = quad.u(b), bV = quad.v(b);
quad.pos(b, aX, aY, aZ).uv(b, aU, aV);
quad.pos(a, bX, bY, bZ).uv(a, bU, bV);
}
}

View File

@ -6,30 +6,49 @@
import com.mojang.math.Transformation;
import dan200.computercraft.client.model.turtle.ModelTransformer;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import javax.annotation.Nullable;
import java.util.List;
import java.util.function.Supplier;
/**
* A {@link BakedModel} which applies a transformation matrix to its underlying quads.
*
* @see ModelTransformer
*/
public class TransformedBakedModel extends CustomBakedModel {
private final ModelTransformer transformation;
public class TransformedBakedModel extends ForwardingBakedModel {
private final FabricModelTransformer transformation;
public TransformedBakedModel(BakedModel model, Transformation transformation) {
super(model);
this.transformation = new ModelTransformer(transformation);
wrapped = model;
this.transformation = new FabricModelTransformer(transformation);
}
@Override
public List<BakedQuad> getQuads(@Nullable BlockState blockState, @Nullable Direction face, RandomSource rand) {
return transformation.transform(wrapped.getQuads(blockState, face, rand));
}
@Override
public final void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
super.emitBlockQuads(blockView, state, pos, randomSupplier, context);
context.popTransform();
}
@Override
public final void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
context.pushTransform(transformation);
super.emitItemQuads(stack, randomSupplier, context);
context.popTransform();
}
}

View File

@ -1,82 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model.turtle;
import dan200.computercraft.api.ComputerCraftAPI;
import net.fabricmc.fabric.api.client.model.ModelProviderException;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* A model "loader" (the concept doesn't quite exist in the same way as it does on Forge) for turtle item models.
* <p>
* This reads in the associated model file (typically {@code computercraft:block/turtle_xxx}) and wraps it in a
* {@link TurtleModel}.
*/
public final class TurtleModelLoader {
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
private TurtleModelLoader() {
}
public static @Nullable UnbakedModel load(ResourceManager resources, ResourceLocation path) throws ModelProviderException {
if (!path.getNamespace().equals(ComputerCraftAPI.MOD_ID)) return null;
if (!path.getPath().equals("item/turtle_normal") && !path.getPath().equals("item/turtle_advanced")) {
return null;
}
try (var reader = resources.openAsReader(new ResourceLocation(path.getNamespace(), "models/" + path.getPath() + ".json"))) {
var modelContents = GsonHelper.parse(reader).getAsJsonObject();
var loader = GsonHelper.getAsString(modelContents, "loader", null);
if (!Objects.equals(loader, ComputerCraftAPI.MOD_ID + ":turtle")) return null;
var model = new ResourceLocation(GsonHelper.getAsString(modelContents, "model"));
return new Unbaked(model);
} catch (IOException e) {
throw new ModelProviderException("Failed loading model " + path, e);
}
}
public static final class Unbaked implements UnbakedModel {
private final ResourceLocation model;
private Unbaked(ResourceLocation model) {
this.model = model;
}
@Override
public Collection<ResourceLocation> getDependencies() {
return List.of(model, COLOUR_TURTLE_MODEL);
}
@Override
public void resolveParents(Function<ResourceLocation, UnbakedModel> function) {
function.apply(model).resolveParents(function);
function.apply(COLOUR_TURTLE_MODEL).resolveParents(function);
}
@Override
public BakedModel bake(ModelBaker bakery, Function<Material, TextureAtlasSprite> spriteGetter, ModelState transform, ResourceLocation location) {
var mainModel = bakery.bake(model, transform);
if (mainModel == null) throw new NullPointerException(model + " failed to bake");
var colourModel = bakery.bake(COLOUR_TURTLE_MODEL, transform);
if (colourModel == null) throw new NullPointerException(COLOUR_TURTLE_MODEL + " failed to bake");
return new TurtleModel(mainModel, colourModel);
}
}
}

View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model.turtle;
import com.google.gson.JsonObject;
import dan200.computercraft.api.ComputerCraftAPI;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.GsonHelper;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
/**
* A {@link UnbakedModel} for {@link TurtleModel}s.
* <p>
* This reads in the associated model file (typically {@code computercraft:block/turtle_xxx}) and wraps it in a
* {@link TurtleModel}.
*/
public final class UnbakedTurtleModel implements UnbakedModel {
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
private final ResourceLocation model;
private UnbakedTurtleModel(ResourceLocation model) {
this.model = model;
}
public static UnbakedModel parse(JsonObject json) {
var model = new ResourceLocation(GsonHelper.getAsString(json, "model"));
return new UnbakedTurtleModel(model);
}
@Override
public Collection<ResourceLocation> getDependencies() {
return List.of(model, COLOUR_TURTLE_MODEL);
}
@Override
public void resolveParents(Function<ResourceLocation, UnbakedModel> function) {
function.apply(model).resolveParents(function);
function.apply(COLOUR_TURTLE_MODEL).resolveParents(function);
}
@Override
public BakedModel bake(ModelBaker bakery, Function<Material, TextureAtlasSprite> spriteGetter, ModelState transform, ResourceLocation location) {
var mainModel = bakery.bake(model, transform);
if (mainModel == null) throw new NullPointerException(model + " failed to bake");
var colourModel = bakery.bake(COLOUR_TURTLE_MODEL, transform);
if (colourModel == null) throw new NullPointerException(COLOUR_TURTLE_MODEL + " failed to bake");
return new TurtleModel(mainModel, colourModel);
}
}

View File

@ -8,7 +8,6 @@
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import dan200.computercraft.shared.platform.NetworkHandler;
import net.fabricmc.fabric.api.client.model.BakedModelManagerHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
@ -23,7 +22,7 @@ public void sendToServer(NetworkMessage<ServerNetworkContext> message) {
@Override
public BakedModel getModel(ModelManager manager, ResourceLocation location) {
var model = BakedModelManagerHelper.getModel(manager, location);
var model = manager.getModel(location);
return model == null ? manager.getMissingModel() : model;
}
}

View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.resources.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import javax.annotation.Nullable;
@Mixin(BlockModel.class)
public interface BlockModelAccessor {
@Accessor("parentLocation")
@Nullable
ResourceLocation computercraft$getParentLocation();
@Accessor("parent")
@Nullable
BlockModel computercraft$getParent();
}

View File

@ -7,6 +7,7 @@
"defaultRequire": 1
},
"client": [
"BlockModelAccessor",
"BlockRenderDispatcherMixin",
"DebugScreenOverlayMixin",
"GameRendererMixin",

View File

@ -73,7 +73,7 @@ public void lootTable(List<LootTableProvider.SubProviderEntry> tables) {
for (var table : tables) {
generator.addProvider((FabricDataOutput out) -> new SimpleFabricLootTableProvider(out, table.paramSet()) {
@Override
public void accept(BiConsumer<ResourceLocation, LootTable.Builder> exporter) {
public void generate(BiConsumer<ResourceLocation, LootTable.Builder> exporter) {
table.provider().get().generate(exporter);
}
});

View File

@ -62,12 +62,15 @@ public synchronized void load(Path path) {
if (loadConfig()) config.save();
}
@SuppressWarnings("unchecked")
private Stream<ValueImpl<?>> values() {
return (Stream<ValueImpl<?>>) (Stream<?>) entries.stream().filter(ValueImpl.class::isInstance);
}
public synchronized void unload() {
closeConfig();
entries.stream().forEach(x -> {
if (x instanceof ValueImpl<?> value) value.unload();
});
values().forEach(ValueImpl::unload);
}
@GuardedBy("this")
@ -87,14 +90,15 @@ private synchronized boolean loadConfig() {
config.load();
entries.stream().forEach(x -> {
config.setComment(x.path, x.comment);
if (x instanceof ValueImpl<?> value) value.load(config);
});
var corrected = spec.correct(config, (action, entryPath, oldValue, newValue) -> {
// Ensure the config file matches the spec
var isNewFile = config.isEmpty();
entries.stream().forEach(x -> config.setComment(x.path, x.comment));
var corrected = isNewFile ? spec.correct(config) : spec.correct(config, (action, entryPath, oldValue, newValue) -> {
LOG.warn("Incorrect key {} was corrected from {} to {}", String.join(".", entryPath), oldValue, newValue);
});
// And then load the underlying entries.
values().forEach(x -> x.load(config));
onChange.onConfigChanged(config.getNioPath());
return corrected > 0;
@ -102,7 +106,7 @@ private synchronized boolean loadConfig() {
@Override
public Stream<ConfigFile.Entry> entries() {
return entries.children().map(x -> (ConfigFile.Entry) x);
return entries.stream().map(x -> (ConfigFile.Entry) x);
}
@Nullable
@ -148,7 +152,7 @@ private String takeComment(String suffix) {
public void push(String name) {
var path = getFullPath(name);
var splitPath = SPLITTER.split(path);
entries.setValue(splitPath, new GroupImpl(path, takeComment(), entries.getChild(splitPath)));
entries.setValue(splitPath, new GroupImpl(path, takeComment()));
super.push(name);
}
@ -230,16 +234,8 @@ public final String comment() {
}
private static final class GroupImpl extends Entry implements Group {
private final Trie<String, Entry> children;
private GroupImpl(String path, String comment, Trie<String, Entry> children) {
private GroupImpl(String path, String comment) {
super(path, comment);
this.children = children;
}
@Override
public Stream<ConfigFile.Entry> children() {
return children.children().map(x -> (ConfigFile.Entry) x);
}
}

View File

@ -49,8 +49,8 @@
}
],
"depends": {
"fabricloader": ">=0.14.17",
"fabric-api": ">=0.80.0",
"fabricloader": ">=0.14.21",
"fabric-api": ">=0.86.0",
"minecraft": ">=1.19.4 <1.20"
},
"accessWidener": "computercraft.accesswidener"

View File

@ -32,7 +32,7 @@ public ForgeConfigSpec spec() {
@Override
public Stream<Entry> entries() {
return entries.children();
return entries.stream();
}
@Nullable
@ -68,7 +68,7 @@ public void push(String name) {
@Override
public void pop() {
var path = new ArrayList<>(groupStack);
entries.setValue(path, new GroupImpl(path, entries.getChild(path)));
entries.setValue(path, new GroupImpl(path));
builder.pop();
super.pop();
@ -129,12 +129,10 @@ public ConfigFile build(ConfigListener onChange) {
private static final class GroupImpl implements ConfigFile.Group {
private final List<String> path;
private final Trie<String, ConfigFile.Entry> entries;
private @Nullable ForgeConfigSpec owner;
private GroupImpl(List<String> path, Trie<String, ConfigFile.Entry> entries) {
private GroupImpl(List<String> path) {
this.path = path;
this.entries = entries;
}
@Override
@ -148,11 +146,6 @@ public String comment() {
if (owner == null) throw new IllegalStateException("Config has not been built yet");
return owner.getLevelComment(path);
}
@Override
public Stream<Entry> children() {
return entries.children();
}
}
private static final class ValueImpl<T> implements ConfigFile.Value<T> {