1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-09-01 01:57:55 +00:00

Update to 1.21.4

Please don't talk to me about this. The first couple of hours of this
update were quite enjoyable, and then the rest was one of the most
miserable times I've had modding.

This has been a real slog, partly due to some large MC changes (item
models are a great change, but a pain to adapt to), and partly due to
mental health reasons — honestly, I've opened up my IDE so many times,
and then just closed it because I've hated the thought of even working
on this.

I will publish this to my maven, so mod authors can depend on it, but I
have no plans to publish a 1.21.4 version. 1.21.5 is right around the
corner (again, with some cool, but no-doubt painful changes), and I
need some time to focus on some breaking changes.

This commit actually includes the 1.21.3 update — the git history got so
messy here, so I just clobbered the whole thing. Sorry.

== Rendering ==

 - Remove TBO monitor renderer: There was a big overhaul to how shaders
   are defined and loaded in 1.21.2. It might have been possible to
   update the monitor shader code to this version, it doesn't see much
   use nowadays, so let's just delete it.

   This is a real shame — the TBO renderer was one of my favourite
   projects I've worked on. Unfortunately, it just doesn't seem worth
   the ongoing maintenance burden. It lives on in the standalone
   emulator :D.

 - Similarly, the VBO rendering code got a bit of an overhaul. We no
   longer use a custom VBO subclass, and instead just hack vanilla's to
   support changing the number of vertices rendered.

   This does mean we need to construct a MeshData, rather than a raw
   ByteBuffer. This isn't too hard, but not sure how it'll play with
   Iris. Given recent vanilla performance improvements, maybe we can
   remove our Unsafe code and use a normal BufferBuilder now.

 - Remove our custom emissive model code, now that vanilla supports
   it. We should add emissive textures to some other models at some
   point.

 - Remove mod-loader specific model code, and replace it with vanilla's
   ItemModel. This does constrain the design of turtle upgrade modellers
   quite a bit — we now only accept an untransformed BakedModel or a
   transformed ItemStack model. We may relax this in the future,
   unclear.

   This change does mean that updsidedown turtles are broken. RIP :(.

 - Entity rendering now separates reading state from the entity from
   actual rendering. This means we need to pass some extra state around
   for item frames. Easy on Forge, but requires a mixin on Fabric.

== Recipes ==

There were several major changes to ingredients this update. The code
here hasn't been very well tested right now — might be nice to add some
game tests for this.

 - Ingredients can no longer be constructed directly from a tag key (it
   needs to be fetched from the current registries), so the recipe
   generation code needs a bit of a reshuffle.

 - DiskRecipe now accepts a custom list of ingredients, rather than
   being hard-coded (fixes #1755). Recipes can now return custom
   `RecipeDisplay`s used to show a recipe in the crafting book. We use
   this to replace the impostor recipes.

   I'm not entirely sure how well this'll play with other recipe
   mods. Here's hoping.

 - Similarly, our recipe mod integration has been updated to use
   RecipeDisplay. We had to do this as ingredients no longer accept
   arbitrary ItemStacks (only a specific item).

== Misc ==

 - Blocks/items now need to know their ID ahead of time (so they can
   compute their description). This requires some reshuffling to the
   registration code, but it's pretty minor.

 - updateShape and neighborChanged no longer take a direction (the
   Orientation is mostly null) and so invalidates all redstone and
   peripherals.

 - All the positions were lowered by one in game tests. It's a good
   change (they now match the positions in structures), but annoying to
   update for!
This commit is contained in:
Jonathan Coates
2025-03-02 21:32:07 +00:00
parent 598fc4aefd
commit 9277aa33e9
352 changed files with 2875 additions and 4764 deletions

View File

@@ -6,14 +6,13 @@ package dan200.computercraft.client;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.client.model.CustomModelLoader;
import dan200.computercraft.client.model.ExtraModels;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.config.ConfigSpec;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.platform.FabricConfigFile;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
@@ -21,19 +20,18 @@ import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
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;
import net.fabricmc.fabric.api.event.client.player.ClientPickBlockGatherCallback;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemTintSources;
import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.client.renderer.item.ItemModels;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperties;
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperties;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
@@ -48,15 +46,16 @@ public class ComputerCraftClient {
ClientRegistry.register();
ClientRegistry.registerTurtleModellers(FabricComputerCraftAPIClient::registerTurtleUpgradeModeller);
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMenuScreens(MenuScreens::register);
ClientRegistry.registerMainThread(ItemProperties::register);
ClientRegistry.registerItemModels(ItemModels.ID_MAPPER::put);
ClientRegistry.registerItemColours(ItemTintSources.ID_MAPPER::put);
ClientRegistry.registerSelectItemProperties(SelectItemModelProperties.ID_MAPPER::put);
ClientRegistry.registerConditionalItemProperties(ConditionalItemModelProperties.ID_MAPPER::put);
PreparableModelLoadingPlugin.register(CustomModelLoader::prepare, (state, context) -> {
ClientRegistry.registerExtraModels(context::addModels, state.getExtraModels());
context.resolveModel().register(ctx -> state.loadModel(ctx.id()));
context.modifyModelAfterBake().register((model, ctx) -> model == null ? null : state.wrapModel(ctx, model));
});
PreparableModelLoadingPlugin.register(
(resources, executor) -> CompletableFuture.supplyAsync(() -> ExtraModels.loadAll(resources), executor),
(state, context) -> ClientRegistry.registerExtraModels(context::addModels, state)
);
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_NORMAL.get(), RenderType.cutout());
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_COMMAND.get(), RenderType.cutout());
@@ -76,18 +75,6 @@ public class ComputerCraftClient {
}
});
// Easier to hook in as an event than use BlockPickInteractionAware.
ClientPickBlockGatherCallback.EVENT.register((player, hit) -> {
if (hit.getType() != HitResult.Type.BLOCK) return ItemStack.EMPTY;
var pos = ((BlockHitResult) hit).getBlockPos();
var level = Objects.requireNonNull(Minecraft.getInstance().level);
var state = level.getBlockState(pos);
if (!(state.getBlock() instanceof CableBlock cable)) return ItemStack.EMPTY;
return cable.getCloneItemStack(state, hit, level, pos, player);
});
ClientCommandRegistrationCallback.EVENT.register(
(dispatcher, registryAccess) -> ClientRegistry.registerClientCommands(dispatcher, FabricClientCommandSource::sendError)
);

View File

@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
/**
* An interface implemented on {@link ItemFrameRenderState} to provide a {@link ExtendedItemFrameRenderState}.
*
* @see ClientHooks#onRenderItemFrame(PoseStack, MultiBufferSource, ItemFrameRenderState, ExtendedItemFrameRenderState, int)
*/
public interface ExtendedItemFrameRenderStateHolder {
/**
* Get or create the CC-specific render state.
*
* @return The CC-specific render state.
*/
ExtendedItemFrameRenderState computercraft$state();
}

View File

@@ -4,53 +4,27 @@
package dan200.computercraft.client.integration.rei;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.integration.RecipeModHelpers;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dev.architectury.event.EventResult;
import me.shedaniel.rei.api.client.plugins.REIClientPlugin;
import me.shedaniel.rei.api.client.registry.display.DisplayRegistry;
import me.shedaniel.rei.api.client.registry.entry.CollapsibleEntryRegistry;
import me.shedaniel.rei.api.client.registry.entry.EntryRegistry;
import me.shedaniel.rei.api.common.display.basic.BasicDisplay;
import me.shedaniel.rei.api.common.entry.comparison.ItemComparatorRegistry;
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes;
import me.shedaniel.rei.api.common.util.EntryStacks;
import me.shedaniel.rei.plugin.common.BuiltinPlugin;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor;
/**
* REI integration for ComputerCraft.
* Client-side REI integration for ComputerCraft.
* <p>
* This is Fabric-only for now - getting the common jar working outside of architectury is awkward.
*/
public class REIComputerCraft implements REIClientPlugin {
@Override
public void registerItemComparators(ItemComparatorRegistry registry) {
registry.register((context, stack) -> {
long hash = 1;
var left = TurtleItem.getUpgradeWithData(stack, TurtleSide.LEFT);
var right = TurtleItem.getUpgradeWithData(stack, TurtleSide.RIGHT);
if (left != null) hash = hash * 31 + left.holder().key().location().hashCode();
if (right != null) hash = hash * 31 + right.holder().key().location().hashCode();
return hash;
}, ModRegistry.Items.TURTLE_NORMAL.get(), ModRegistry.Items.TURTLE_ADVANCED.get());
registry.register((context, stack) -> {
var upgrade = PocketComputerItem.getUpgradeWithData(stack);
return upgrade == null ? 1 : upgrade.holder().key().location().hashCode();
}, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
registry.register((context, stack) -> DyedItemColor.getOrDefault(stack, -1), ModRegistry.Items.DISK.get());
}
public class REIComputerCraftClient implements REIClientPlugin {
@Override
public void registerEntries(EntryRegistry registry) {
for (var stack : RecipeModHelpers.getExtraStacks(BasicDisplay.registryAccess())) {

View File

@@ -10,55 +10,36 @@ import me.shedaniel.rei.api.common.display.basic.BasicDisplay;
import me.shedaniel.rei.api.common.entry.EntryStack;
import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes;
import me.shedaniel.rei.api.common.util.EntryIngredients;
import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCraftingDisplay;
import me.shedaniel.rei.plugin.common.displays.crafting.DefaultShapedDisplay;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.ShapedRecipe;
import me.shedaniel.rei.plugin.common.displays.crafting.CraftingDisplay;
import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCustomShapedDisplay;
import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* Provides custom recipe and usage hints for pocket/turtle upgrades.
*/
class UpgradeDisplayGenerator implements DynamicDisplayGenerator<DefaultCraftingDisplay<?>> {
private final UpgradeRecipeGenerator<DefaultCraftingDisplay<?>> resolver = new UpgradeRecipeGenerator<>(GeneratedShapedDisplay::new, BasicDisplay.registryAccess());
class UpgradeDisplayGenerator implements DynamicDisplayGenerator<CraftingDisplay> {
private final UpgradeRecipeGenerator<CraftingDisplay> resolver = new UpgradeRecipeGenerator<>(UpgradeDisplayGenerator::makeDisplay, BasicDisplay.registryAccess());
@Override
public Optional<List<DefaultCraftingDisplay<?>>> getRecipeFor(EntryStack<?> entry) {
public Optional<List<CraftingDisplay>> getRecipeFor(EntryStack<?> entry) {
return entry.getType() == VanillaEntryTypes.ITEM ? Optional.of(resolver.findRecipesWithOutput(entry.castValue())) : Optional.empty();
}
@Override
public Optional<List<DefaultCraftingDisplay<?>>> getUsageFor(EntryStack<?> entry) {
public Optional<List<CraftingDisplay>> getUsageFor(EntryStack<?> entry) {
return entry.getType() == VanillaEntryTypes.ITEM ? Optional.of(resolver.findRecipesWithInput(entry.castValue())) : Optional.empty();
}
/**
* Similar to {@link DefaultShapedDisplay}, but does not require a {@link RecipeHolder}.
*/
private static class GeneratedShapedDisplay extends DefaultCraftingDisplay<ShapedRecipe> {
private final int width, height;
GeneratedShapedDisplay(ShapedRecipe recipe) {
super(
EntryIngredients.ofIngredients(recipe.getIngredients()),
Collections.singletonList(EntryIngredients.of(recipe.getResultItem(BasicDisplay.registryAccess()))),
Optional.empty()
);
width = recipe.getWidth();
height = recipe.getHeight();
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
private static CraftingDisplay makeDisplay(ShapedCraftingRecipeDisplay recipe) {
return new DefaultCustomShapedDisplay(
EntryIngredients.ofSlotDisplays(recipe.ingredients()),
List.of(EntryIngredients.ofSlotDisplay(recipe.result())),
Optional.empty(),
recipe.width(),
recipe.height()
);
}
}

View File

@@ -1,101 +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.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 org.jspecify.annotations.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 ForwardingBakedModel {
private final boolean isVanillaAdapter;
private final List<BakedModel> models;
public CompositeBakedModel(List<BakedModel> models) {
wrapped = models.get(0);
isVanillaAdapter = models.stream().allMatch(FabricBakedModel::isVanillaAdapter);
this.models = models;
}
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" })
List<BakedQuad>[] quads = new List[models.size()];
var i = 0;
for (var model : models) quads[i++] = model.getQuads(blockState, face, rand);
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;
private ConcatListView(List<BakedQuad>[] quads) {
this.quads = quads;
}
@Override
public Iterator<BakedQuad> iterator() {
return stream().iterator();
}
@Override
public Stream<BakedQuad> stream() {
return Arrays.stream(quads).flatMap(Collection::stream);
}
@Override
public BakedQuad get(int index) {
var i = index;
for (var modelQuads : quads) {
if (i < modelQuads.size()) return modelQuads.get(i);
i -= modelQuads.size();
}
throw new IndexOutOfBoundsException(i);
}
@Override
public int size() {
var size = 0;
for (var modelQuads : quads) size += modelQuads.size();
return size;
}
}
}

View File

@@ -1,135 +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.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.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.Reader;
import java.util.Collection;
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 final Collection<ResourceLocation> extraModels;
private CustomModelLoader(Collection<ResourceLocation> extraModels) {
this.extraModels = extraModels;
}
public static CompletableFuture<CustomModelLoader> prepare(ResourceManager resources, Executor executor) {
return CompletableFuture.supplyAsync(() -> {
var loader = new CustomModelLoader(ExtraModels.loadAll(resources));
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);
}
}
public Collection<ResourceLocation> getExtraModels() {
return extraModels;
}
/**
* 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) {
var id = ctx.resourceId();
if (id == null || !id.getNamespace().equals(ComputerCraftAPI.MOD_ID)) return baked;
if (!(ctx.sourceModel() instanceof BlockModel model)) return baked;
var emissive = getEmissive(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

@@ -1,84 +0,0 @@
// 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 org.jspecify.annotations.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,49 +0,0 @@
// 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

@@ -1,79 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import dan200.computercraft.core.util.Nullability;
import net.fabricmc.fabric.api.renderer.v1.Renderer;
import net.fabricmc.fabric.api.renderer.v1.RendererAccess;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.fabricmc.fabric.api.util.TriState;
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 org.jspecify.annotations.Nullable;
import java.util.function.Supplier;
/**
* A model wrapper which applies a {@link RenderMaterial#glint() glint}/foil to the original model.
*/
public final class FoiledModel extends ForwardingBakedModel implements RenderContext.QuadTransform {
private final @Nullable Renderer renderer = RendererAccess.INSTANCE.getRenderer();
private @Nullable RenderMaterial lastMaterial, lastFoiledMaterial;
public FoiledModel(BakedModel model) {
wrapped = model;
}
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockAndTintGetter blockView, BlockState state, BlockPos pos, Supplier<RandomSource> randomSupplier, RenderContext context) {
context.pushTransform(this);
super.emitBlockQuads(blockView, state, pos, randomSupplier, context);
context.popTransform();
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<RandomSource> randomSupplier, RenderContext context) {
context.pushTransform(this);
super.emitItemQuads(stack, randomSupplier, context);
context.popTransform();
}
@Override
public boolean equals(Object obj) {
return this == obj || (obj instanceof FoiledModel other && wrapped.equals(other.wrapped));
}
@Override
public int hashCode() {
return wrapped.hashCode() ^ 1;
}
@Override
public boolean transform(MutableQuadView quad) {
if (renderer == null) return true;
var material = quad.material();
if (material == lastMaterial) {
quad.material(Nullability.assertNonNull(lastFoiledMaterial));
} else {
lastMaterial = material;
quad.material(lastFoiledMaterial = renderer.materialFinder().copyFrom(material).glint(TriState.TRUE).find());
}
return true;
}
}

View File

@@ -1,54 +0,0 @@
// SPDX-FileCopyrightText: 2022 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.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 org.jspecify.annotations.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 ForwardingBakedModel {
private final FabricModelTransformer transformation;
public TransformedBakedModel(BakedModel model, Transformation 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,41 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model.turtle;
import dan200.computercraft.client.model.CompositeBakedModel;
import dan200.computercraft.client.model.TransformedBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.block.model.ItemOverrides;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
/**
* The custom model for turtle items, which renders tools and overlays as part of the model.
*
* @see TurtleModelParts
*/
public class TurtleModel extends ForwardingBakedModel {
private final TurtleModelParts<BakedModel> parts;
private final ItemOverrides overrides = new ItemOverrides() {
@Override
public BakedModel resolve(BakedModel model, ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity entity, int seed) {
return parts.getModel(stack);
}
};
public TurtleModel(BakedModel familyModel, BakedModel colourModel) {
wrapped = familyModel;
parts = new TurtleModelParts<>(familyModel, colourModel, TransformedBakedModel::new, CompositeBakedModel::of);
}
@Override
public ItemOverrides getOverrides() {
return overrides;
}
}

View File

@@ -1,59 +0,0 @@
// 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 = ResourceLocation.fromNamespaceAndPath(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 = ResourceLocation.parse(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) {
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

@@ -6,15 +6,12 @@ package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.client.model.FoiledModel;
import dan200.computercraft.client.render.ModelRenderer;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import org.jspecify.annotations.Nullable;
@@ -29,22 +26,11 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
return model == null ? manager.getMissingModel() : model;
}
@Override
public BakedModel getModel(ModelManager manager, ModelResourceLocation modelLocation, @Nullable ResourceLocation resourceLocation) {
return resourceLocation == null ? manager.getModel(modelLocation) : getModel(manager, resourceLocation);
}
@Override
public BakedModel createdFoiledModel(BakedModel model) {
return new FoiledModel(model);
}
@Override
public void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, int @Nullable [] tints) {
// Unfortunately we can't call Fabric's emitItemQuads here, as there's no way to obtain a RenderContext via the
// API. Instead, we special case our FoiledModel, and just render everything else normally.
var buffer = ItemRenderer.getFoilBuffer(buffers, Sheets.translucentCullBlockSheet(), true, model instanceof FoiledModel);
var buffer = buffers.getBuffer(Sheets.translucentItemSheet());
for (var faceIdx = 0; faceIdx <= ModelHelper.NULL_FACE_ID; faceIdx++) {
var face = ModelHelper.faceFromIndex(faceIdx);
random.setSeed(42);

View File

@@ -1,22 +0,0 @@
// 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.jspecify.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BlockModel.class)
public interface BlockModelAccessor {
@Accessor("parentLocation")
@Nullable
ResourceLocation computercraft$getParentLocation();
@Accessor("parent")
@Nullable
BlockModel computercraft$getParent();
}

View File

@@ -20,10 +20,4 @@ class DebugScreenOverlayMixin {
private void appendBlockDebugInfo(CallbackInfoReturnable<List<String>> cir) {
ClientHooks.addBlockDebugInfo(cir.getReturnValue()::add);
}
@Inject(method = "getGameInformation", at = @At("RETURN"))
@SuppressWarnings("UnusedMethod")
private void appendGameDebugInfo(CallbackInfoReturnable<List<String>> cir) {
ClientHooks.addGameDebugInfo(cir.getReturnValue()::add);
}
}

View File

@@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import dan200.computercraft.client.ClientRegistry;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.server.packs.resources.ResourceProvider;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
@Mixin(GameRenderer.class)
class GameRendererMixin {
@Final
@Shadow
@SuppressWarnings("NullAway")
private Map<String, ShaderInstance> shaders;
@Inject(method = "reloadShaders", at = @At(value = "TAIL"))
@SuppressWarnings("unused")
private void onReloadShaders(ResourceProvider resourceManager, CallbackInfo ci) {
try {
ClientRegistry.registerShaders(resourceManager, (shader, callback) -> {
shaders.put(shader.getName(), shader);
callback.accept(shader);
});
} catch (IOException e) {
throw new UncheckedIOException("Could not reload shaders", e);
}
}
}

View File

@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import dan200.computercraft.client.ExtendedItemFrameRenderStateHolder;
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import org.spongepowered.asm.mixin.Mixin;
import javax.annotation.Nullable;
@Mixin(ItemFrameRenderState.class)
class ItemFrameRenderStateMixin implements ExtendedItemFrameRenderStateHolder {
private @Nullable ExtendedItemFrameRenderState computercraft$state;
@Override
public ExtendedItemFrameRenderState computercraft$state() {
if (computercraft$state == null) computercraft$state = new ExtendedItemFrameRenderState();
return computercraft$state;
}
}

View File

@@ -6,9 +6,12 @@ package dan200.computercraft.mixin.client;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.client.ClientHooks;
import dan200.computercraft.client.ExtendedItemFrameRenderStateHolder;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.ItemFrameRenderer;
import net.minecraft.client.renderer.entity.state.ItemFrameRenderState;
import net.minecraft.world.entity.decoration.ItemFrame;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -18,15 +21,25 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@SuppressWarnings("UnusedMethod")
class ItemFrameRendererMixin {
@Inject(
method = "render(Lnet/minecraft/world/entity/decoration/ItemFrame;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;mulPose(Lorg/joml/Quaternionf;)V", ordinal = 2, shift = At.Shift.AFTER),
method = "render(Lnet/minecraft/client/renderer/entity/state/ItemFrameRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V",
at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/entity/state/ItemFrameRenderState;mapId:Lnet/minecraft/world/level/saveddata/maps/MapId;", opcode = Opcodes.GETFIELD),
cancellable = true
)
@SuppressWarnings("unused")
private void render(ItemFrame entity, float yaw, float partialTicks, PoseStack pose, MultiBufferSource buffers, int light, CallbackInfo ci) {
if (ClientHooks.onRenderItemFrame(pose, buffers, entity, entity.getItem(), light)) {
private void render(ItemFrameRenderState frame, PoseStack pose, MultiBufferSource buffers, int light, CallbackInfo ci) {
var state = ((ExtendedItemFrameRenderStateHolder) frame).computercraft$state();
if (ClientHooks.onRenderItemFrame(pose, buffers, frame, state, light)) {
ci.cancel();
pose.popPose();
}
}
@Inject(
method = "extractRenderState(Lnet/minecraft/world/entity/decoration/ItemFrame;Lnet/minecraft/client/renderer/entity/state/ItemFrameRenderState;F)V",
at = @At("HEAD")
)
@SuppressWarnings("unused")
private void extractRenderState(ItemFrame entity, ItemFrameRenderState state, float f, CallbackInfo ci) {
((ExtendedItemFrameRenderStateHolder) state).computercraft$state().setup(entity.getItem());
}
}

View File

@@ -46,6 +46,6 @@ class MinecraftMixin {
)
@SuppressWarnings("unused")
private void beforeInitialResourceReload(GameConfig gameConfig, CallbackInfo ci) {
ClientRegistry.registerReloadListeners(resourceManager::registerReloadListener, (Minecraft) (Object) this);
ClientRegistry.registerReloadListeners((id, l) -> resourceManager.registerReloadListener(l), (Minecraft) (Object) this);
}
}

View File

@@ -7,11 +7,10 @@
"defaultRequire": 1
},
"client": [
"BlockModelAccessor",
"BlockRenderDispatcherMixin",
"DebugScreenOverlayMixin",
"GameRendererMixin",
"ItemFrameRendererMixin",
"ItemFrameRenderStateMixin",
"ItemInHandRendererMixin",
"MinecraftMixin",
"MultiPlayerGameModeMixin",

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
@@ -12,6 +13,8 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider;
import net.fabricmc.fabric.api.event.registry.DynamicRegistries;
import net.minecraft.client.data.models.BlockModelGenerators;
import net.minecraft.client.data.models.ItemModelGenerators;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
@@ -20,7 +23,6 @@ import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.tags.TagsProvider;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
@@ -32,11 +34,11 @@ import java.util.function.Consumer;
public class FabricDataProviders implements DataGeneratorEntrypoint {
@Override
public void onInitializeDataGenerator(FabricDataGenerator generator) {
var pack = new PlatformGeneratorsImpl(generator.createPack(), generator.getRegistries());
var pack = new GeneratorSinkImpl(generator.createPack(), generator.getRegistries());
DataProviders.add(pack);
}
private record PlatformGeneratorsImpl(
private record GeneratorSinkImpl(
FabricDataGenerator.Pack generator, CompletableFuture<HolderLookup.Provider> registries
) implements DataProviders.GeneratorSink {
public <T extends DataProvider> T addWithFabricOutput(FabricDataGenerator.Pack.Factory<T> factory) {
@@ -53,23 +55,17 @@ public class FabricDataProviders implements DataGeneratorEntrypoint {
}
@Override
public <T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output) {
addWithRegistries((out, registries) -> {
var ourType = switch (type) {
case SERVER_DATA -> PackOutput.Target.DATA_PACK;
case CLIENT_RESOURCES -> PackOutput.Target.RESOURCE_PACK;
};
return new FabricCodecDataProvider<T>(out, registries, ourType, directory, codec) {
@Override
public String getName() {
return name;
}
public <T> void addFromCodec(String name, PackOutput.Target target, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output) {
addWithRegistries((out, registries) -> new FabricCodecDataProvider<T>(out, registries, target, directory, codec) {
@Override
public String getName() {
return name;
}
@Override
protected void configure(BiConsumer<ResourceLocation, T> provider, HolderLookup.Provider registries) {
output.accept(provider);
}
};
@Override
protected void configure(BiConsumer<ResourceLocation, T> provider, HolderLookup.Provider registries) {
output.accept(provider);
}
});
}
@@ -124,5 +120,20 @@ public class FabricDataProviders implements DataGeneratorEntrypoint {
}
});
}
@Override
public void addModels(Consumer<BlockModelGenerators> blocks, Consumer<ItemModelGenerators> items) {
addWithFabricOutput(output -> new FabricModelProvider(output) {
@Override
public void generateBlockStateModels(BlockModelGenerators generators) {
blocks.accept(generators);
}
@Override
public void generateItemModels(ItemModelGenerators generators) {
items.accept(generators);
}
});
}
}
}

View File

@@ -18,7 +18,7 @@ public class FabricExampleMod implements ModInitializer {
public void onInitialize() {
// @start region=turtle_upgrades
@SuppressWarnings("unchecked")
var turtleUpgradeSerialisers = (Registry<UpgradeType<? extends ITurtleUpgrade>>) BuiltInRegistries.REGISTRY.get(ITurtleUpgrade.typeRegistry().location());
var turtleUpgradeSerialisers = (Registry<UpgradeType<? extends ITurtleUpgrade>>) BuiltInRegistries.REGISTRY.getValue(ITurtleUpgrade.typeRegistry().location());
Registry.register(turtleUpgradeSerialisers, ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade"), ExampleMod.EXAMPLE_TURTLE_UPGRADE);
// @end region=turtle_upgrades

View File

@@ -7,8 +7,6 @@ package dan200.computercraft.impl;
import com.google.auto.service.AutoService;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.detail.DetailRegistry;
import dan200.computercraft.api.media.MediaLookup;
import dan200.computercraft.api.media.MediaProvider;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.shared.details.FluidDetails;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
@@ -43,9 +41,4 @@ public final class ComputerCraftAPIImpl extends AbstractComputerCraftAPI impleme
public DetailRegistry<StorageView<FluidVariant>> getFluidDetailRegistry() {
return fluidDetails;
}
@Override
public void registerMediaProvider(MediaProvider provider) {
MediaLookup.get().registerFallback((stack, ctx) -> provider.getMedia(stack));
}
}

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.mixin;
import dan200.computercraft.shared.CommonHooks;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
@@ -16,12 +17,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Entity.class)
class EntityMixin {
@Inject(
method = "spawnAtLocation(Lnet/minecraft/world/item/ItemStack;F)Lnet/minecraft/world/entity/item/ItemEntity;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z"),
method = "spawnAtLocation(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/item/ItemStack;F)Lnet/minecraft/world/entity/item/ItemEntity;",
at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerLevel;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z"),
cancellable = true
)
@SuppressWarnings("unused")
private void spawnAtLocation(ItemStack stack, float yOffset, CallbackInfoReturnable<ItemEntity> cb) {
private void spawnAtLocation(ServerLevel level, ItemStack stack, float yOffset, CallbackInfoReturnable<ItemEntity> cb) {
if (CommonHooks.onLivingDrop((Entity) (Object) this, stack)) cb.setReturnValue(null);
}
}

View File

@@ -1,27 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.mojang.datafixers.schemas.Schema;
import com.mojang.datafixers.types.templates.TypeTemplate;
import dan200.computercraft.shared.datafix.ComponentizationFixers;
import net.minecraft.util.datafix.schemas.V3818_3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Add our custom data components to the datafixer system.
* <p>
* This mixin is identical between Fabric and NeoForge aside from using a different method name.
*/
@Mixin(V3818_3.class)
class V3818_3Mixin {
@ModifyReturnValue(method = "method_57277", at = @At("TAIL"))
@SuppressWarnings("UnusedMethod")
private static TypeTemplate addExtraTypes(TypeTemplate type, Schema schema) {
return ComponentizationFixers.addExtraTypes(type, schema);
}
}

View File

@@ -20,6 +20,7 @@ import dan200.computercraft.shared.details.FluidDetails;
import dan200.computercraft.shared.integration.CreateIntegration;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock;
import dan200.computercraft.shared.platform.FabricConfigFile;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import dan200.computercraft.shared.turtle.TurtleOverlay;
@@ -28,6 +29,7 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.PlayerPickItemEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.fabric.api.event.registry.DynamicRegistries;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
@@ -48,7 +50,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.entity.BlockEntity;
@@ -109,6 +110,10 @@ public class ComputerCraft {
UseBlockCallback.EVENT.register(FabricCommonHooks::useOnBlock);
UseBlockCallback.EVENT.register(CommonHooks::onUseBlock);
PlayerPickItemEvents.BLOCK.register((player, pos, state, includeData) ->
state.getBlock() instanceof CableBlock cable ? cable.getCloneItemStack(player.level(), pos, state, includeData, player) : null
);
LootTableEvents.MODIFY.register((id, tableBuilder, source, registries) -> {
var pool = CommonHooks.getExtraLootPool(id);
if (pool != null) tableBuilder.withPool(pool);
@@ -134,17 +139,17 @@ public class ComputerCraft {
if (FabricLoader.getInstance().isModLoaded(CreateIntegration.ID)) CreateIntegration.setup();
}
private record ReloadListener(String name, PreparableReloadListener listener)
private record ReloadListener(ResourceLocation name, PreparableReloadListener listener)
implements IdentifiableResourceReloadListener {
@Override
public ResourceLocation getFabricId() {
return ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
return name;
}
@Override
public CompletableFuture<Void> reload(PreparationBarrier preparationBarrier, ResourceManager resourceManager, ProfilerFiller preparationsProfiler, ProfilerFiller reloadProfiler, Executor backgroundExecutor, Executor gameExecutor) {
return listener.reload(preparationBarrier, resourceManager, preparationsProfiler, reloadProfiler, backgroundExecutor, gameExecutor);
public CompletableFuture<Void> reload(PreparationBarrier preparationBarrier, ResourceManager resourceManager, Executor backgroundExecutor, Executor gameExecutor) {
return listener.reload(preparationBarrier, resourceManager, backgroundExecutor, gameExecutor);
}
}

View File

@@ -43,7 +43,7 @@ public class FabricCommonHooks {
if (player.isSecondaryUseActive() && doesSneakBypassUse(player.getMainHandItem()) && doesSneakBypassUse(player.getOffhandItem())) {
var result = block.useItemOn(player.getMainHandItem(), level, player, hand, hitResult);
if (result.consumesAction()) return result.result();
if (result.consumesAction()) return result;
}
return InteractionResult.PASS;

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration.rei;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import me.shedaniel.rei.api.common.entry.comparison.ItemComparatorRegistry;
import me.shedaniel.rei.api.common.plugins.REICommonPlugin;
import net.minecraft.world.item.component.DyedItemColor;
/**
* Common integration for ComputerCraft.
*/
public class REIComputerCraft implements REICommonPlugin {
@Override
public void registerItemComparators(ItemComparatorRegistry registry) {
registry.register((context, stack) -> {
long hash = 1;
var left = TurtleItem.getUpgradeWithData(stack, TurtleSide.LEFT);
var right = TurtleItem.getUpgradeWithData(stack, TurtleSide.RIGHT);
if (left != null) hash = hash * 31 + left.holder().key().location().hashCode();
if (right != null) hash = hash * 31 + right.holder().key().location().hashCode();
return hash;
}, ModRegistry.Items.TURTLE_NORMAL.get(), ModRegistry.Items.TURTLE_ADVANCED.get());
registry.register((context, stack) -> {
var upgrade = PocketComputerItem.getUpgradeWithData(stack);
return upgrade == null ? 1 : upgrade.holder().key().location().hashCode();
}, ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get());
registry.register((context, stack) -> DyedItemColor.getOrDefault(stack, -1), ModRegistry.Items.DISK.get());
}
}

View File

@@ -25,7 +25,6 @@ import net.fabricmc.fabric.api.event.player.UseEntityCallback;
import net.fabricmc.fabric.api.itemgroup.v1.FabricItemGroup;
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache;
import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup;
import net.fabricmc.fabric.api.registry.FuelRegistry;
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory;
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType;
import net.fabricmc.fabric.api.tag.convention.v2.ConventionalItemTags;
@@ -42,6 +41,7 @@ import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.ItemTags;
@@ -56,7 +56,6 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuConstructor;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.*;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
@@ -85,7 +84,7 @@ public class PlatformHelperImpl implements PlatformHelper {
@SuppressWarnings("unchecked")
private static <T> Registry<T> getRegistry(ResourceKey<Registry<T>> id) {
var registry = (Registry<T>) BuiltInRegistries.REGISTRY.get(id.location());
var registry = (Registry<T>) BuiltInRegistries.REGISTRY.getValue(id.location());
if (registry == null) throw new IllegalArgumentException("Unknown registry " + id);
return registry;
}
@@ -144,16 +143,16 @@ public class PlatformHelperImpl implements PlatformHelper {
@Override
public RecipeIngredients getRecipeIngredients() {
return new RecipeIngredients(
Ingredient.of(ConventionalItemTags.REDSTONE_DUSTS),
Ingredient.of(ConventionalItemTags.STRINGS),
Ingredient.of(ConventionalItemTags.LEATHERS),
Ingredient.of(ConventionalItemTags.GLASS_PANES),
Ingredient.of(ConventionalItemTags.GOLD_INGOTS),
Ingredient.of(ConventionalItemTags.STORAGE_BLOCKS_GOLD),
Ingredient.of(ConventionalItemTags.IRON_INGOTS),
Ingredient.of(ConventionalItemTags.DYES),
Ingredient.of(ConventionalItemTags.ENDER_PEARLS),
Ingredient.of(ConventionalItemTags.WOODEN_CHESTS)
ConventionalItemTags.REDSTONE_DUSTS,
ConventionalItemTags.STRINGS,
ConventionalItemTags.LEATHERS,
ConventionalItemTags.GLASS_PANES,
ConventionalItemTags.GOLD_INGOTS,
ConventionalItemTags.STORAGE_BLOCKS_GOLD,
ConventionalItemTags.IRON_INGOTS,
ConventionalItemTags.DYES,
ConventionalItemTags.ENDER_PEARLS,
ConventionalItemTags.WOODEN_CHESTS
);
}
@@ -180,9 +179,8 @@ public class PlatformHelperImpl implements PlatformHelper {
}
@Override
public int getBurnTime(ItemStack stack) {
var fuel = FuelRegistry.INSTANCE.get(stack.getItem());
return fuel == null ? 0 : fuel;
public int getBurnTime(MinecraftServer server, ItemStack stack) {
return server.fuelValues().burnDuration(stack);
}
@Override
@@ -219,9 +217,9 @@ public class PlatformHelperImpl implements PlatformHelper {
@Override
public boolean interactWithEntity(ServerPlayer player, Entity entity, Vec3 hitPos) {
return UseEntityCallback.EVENT.invoker().interact(player, entity.level(), InteractionHand.MAIN_HAND, entity, new EntityHitResult(entity, hitPos)).consumesAction() ||
entity.interactAt(player, hitPos.subtract(entity.position()), InteractionHand.MAIN_HAND).consumesAction() ||
player.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction();
return UseEntityCallback.EVENT.invoker().interact(player, entity.level(), InteractionHand.MAIN_HAND, entity, new EntityHitResult(entity, hitPos)).consumesAction()
|| entity.interactAt(player, hitPos.subtract(entity.position()), InteractionHand.MAIN_HAND).consumesAction()
|| player.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction();
}
@Override

View File

@@ -26,8 +26,11 @@
"jei_mod_plugin": [
"dan200.computercraft.client.integration.jei.JEIComputerCraft"
],
"rei_common": [
"dan200.computercraft.shared.integration.rei.REIComputerCraft"
],
"rei_client": [
"dan200.computercraft.client.integration.rei.REIComputerCraft"
"dan200.computercraft.client.integration.rei.REIComputerCraftClient"
],
"emi": [
"dan200.computercraft.client.integration.emi.EMIComputerCraft"
@@ -42,9 +45,9 @@
}
],
"depends": {
"fabricloader": ">=0.15.10",
"fabric-api": ">=0.102.1",
"minecraft": "=1.21.1"
"fabricloader": ">=0.16.10",
"fabric-api": ">=0.118.0",
"minecraft": "=1.21.4"
},
"accessWidener": "computercraft.accesswidener"
}