1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-05-25 10:44:11 +00:00

Make turtle upgrade models data driven

It's ItemModel, but for upgrades!
This commit is contained in:
Jonathan Coates 2025-05-11 09:17:29 +01:00
parent 598fd98a8b
commit acafc06449
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
48 changed files with 885 additions and 521 deletions

View File

@ -7,15 +7,11 @@ package dan200.computercraft.api.client;
import com.google.common.base.Suppliers;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.BlockModelWrapper;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.ItemModelResolver;
@ -24,7 +20,6 @@ import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvedModel;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ARGB;
import net.minecraft.world.entity.LivingEntity;
@ -84,6 +79,27 @@ public final class StandaloneModel {
* @return The baked {@link StandaloneModel}.
*/
public static StandaloneModel of(ResolvedModel model, ModelBaker baker) {
return baker.compute(new CacheKey(model));
}
private record CacheKey(ResolvedModel model) implements ModelBaker.SharedOperationKey<StandaloneModel> {
@Override
public StandaloneModel compute(ModelBaker baker) {
return ofUncached(model(), baker);
}
@Override
public boolean equals(Object other) {
return other instanceof CacheKey(var otherModel) && model() == otherModel;
}
@Override
public int hashCode() {
return System.identityHashCode(model());
}
}
private static StandaloneModel ofUncached(ResolvedModel model, ModelBaker baker) {
var slots = model.getTopTextureSlots();
return new StandaloneModel(
model.bakeTopGeometry(slots, baker, BlockModelRotation.X0_Y0).getAll(),
@ -98,7 +114,6 @@ public final class StandaloneModel {
*
* @param layer The layer to set up.
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
* @see TurtleUpgradeModel#renderForItem(ITurtleUpgrade, TurtleSide, DataComponentPatch, ItemStackRenderState, ItemModelResolver, ItemTransform, int)
*/
public void setupItemLayer(ItemStackRenderState.LayerRenderState layer) {
layer.setExtents(extents);
@ -115,7 +130,6 @@ public final class StandaloneModel {
* @param buffers The buffer source to use for rendering.
* @param light The current light texture coordinate.
* @param overlay The current overlay texture coordinate.
* @see TurtleUpgradeModel#renderForLevel(ITurtleUpgrade, ITurtleAccess, TurtleSide, DataComponentPatch, PoseStack, MultiBufferSource, int, int)
*/
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
render(transform, buffers, light, overlay, null);
@ -129,7 +143,6 @@ public final class StandaloneModel {
* @param light The current light texture coordinate.
* @param overlay The current overlay texture coordinate.
* @param tints The tints for this model.
* @see TurtleUpgradeModel#renderForLevel(ITurtleUpgrade, ITurtleAccess, TurtleSide, DataComponentPatch, PoseStack, MultiBufferSource, int, int)
*/
public void render(PoseStack transform, MultiBufferSource buffers, int light, int overlay, int @Nullable [] tints) {
var pose = transform.last();

View File

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.BlockModelWrapper;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.resources.ResourceLocation;
/**
* A {@link TurtleUpgradeModel} that renders a basic model.
* <p>
* This is the {@link TurtleUpgradeModel} equivalent of {@link BlockModelWrapper}.
*/
public final class BasicUpgradeModel implements TurtleUpgradeModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "sided");
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked>mapCodec(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("left").forGetter(Unbaked::left),
ResourceLocation.CODEC.fieldOf("right").forGetter(Unbaked::right)
).apply(instance, Unbaked::new));
private final StandaloneModel left;
private final StandaloneModel right;
private BasicUpgradeModel(StandaloneModel left, StandaloneModel right) {
this.left = left;
this.right = right;
}
/**
* Create an unbaked {@link BasicUpgradeModel}.
*
* @param left The model when equipped on the left.
* @param right The model when equipped on the right.
* @return The unbaked turtle upgrade model.
*/
public static TurtleUpgradeModel.Unbaked unbaked(ResourceLocation left, ResourceLocation right) {
return new Unbaked(left, right);
}
private StandaloneModel getModel(TurtleSide side) {
return switch (side) {
case LEFT -> left;
case RIGHT -> right;
};
}
@Override
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
var layer = renderer.newLayer();
layer.setTransform(transform);
getModel(side).setupItemLayer(layer);
}
@Override
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
getModel(side).render(transform, buffers, light, overlay);
}
private record Unbaked(ResourceLocation left, ResourceLocation right) implements TurtleUpgradeModel.Unbaked {
@Override
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
return new BasicUpgradeModel(StandaloneModel.of(left(), baker), StandaloneModel.of(right(), baker));
}
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(left());
resolver.markDependency(right());
}
}
}

View File

@ -7,9 +7,12 @@ package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
@ -18,15 +21,26 @@ import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.special.SpecialModelRenderer;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import org.joml.Matrix4f;
import org.jspecify.annotations.Nullable;
final class ItemUpgradeModel<T extends ITurtleUpgrade> implements TurtleUpgradeModel<T> {
static final TurtleUpgradeModel.Unbaked<ITurtleUpgrade> UNBAKED = new Unbaked();
static final TurtleUpgradeModel<ITurtleUpgrade> INSTANCE = new ItemUpgradeModel<>();
/**
* A sic {@link TurtleUpgradeModel} that renders the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)
* upgrade item}.
* <p>
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
*/
public final class ItemUpgradeModel implements TurtleUpgradeModel {
private static final TurtleUpgradeModel.Unbaked UNBAKED = new Unbaked();
private static final TurtleUpgradeModel INSTANCE = new ItemUpgradeModel();
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "item");
public static final MapCodec<TurtleUpgradeModel.Unbaked> CODEC = MapCodec.unit(UNBAKED);
private static final TransformedRenderer LEFT = computeRenderer(TurtleSide.LEFT);
private static final TransformedRenderer RIGHT = computeRenderer(TurtleSide.RIGHT);
@ -34,10 +48,19 @@ final class ItemUpgradeModel<T extends ITurtleUpgrade> implements TurtleUpgradeM
private ItemUpgradeModel() {
}
/**
* Get the unbaked {@link ItemUpgradeModel}.
*
* @return The unbaked item upgrade model.
*/
public static TurtleUpgradeModel.Unbaked unbaked() {
return UNBAKED;
}
@Override
public void renderForItem(T upgrade, TurtleSide side, DataComponentPatch data, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
var childState = new ItemStackRenderState();
resolver.updateForTopItem(childState, upgrade.getUpgradeItem(data), ItemDisplayContext.NONE, null, null, seed);
resolver.updateForTopItem(childState, upgrade.getUpgradeItem(), ItemDisplayContext.NONE, null, null, seed);
if (!childState.isEmpty()) {
var layer = renderer.newLayer();
layer.setTransform(transform);
@ -46,17 +69,22 @@ final class ItemUpgradeModel<T extends ITurtleUpgrade> implements TurtleUpgradeM
}
@Override
public void renderForLevel(T upgrade, ITurtleAccess turtle, TurtleSide side, DataComponentPatch data, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
transform.mulPose(getRenderer(side).transform().getMatrix());
transform.mulPose(Axis.YP.rotation(Mth.PI));
Minecraft.getInstance().getItemRenderer().renderStatic(
upgrade.getUpgradeItem(data), ItemDisplayContext.FIXED, light, overlay, transform, buffers, turtle.getLevel(), 0
upgrade.getUpgradeItem(), ItemDisplayContext.FIXED, light, overlay, transform, buffers, turtle.getLevel(), 0
);
}
private static final class Unbaked implements TurtleUpgradeModel.Unbaked<ITurtleUpgrade> {
private static final class Unbaked implements TurtleUpgradeModel.Unbaked {
@Override
public TurtleUpgradeModel<ITurtleUpgrade> bake(ModelBaker baker) {
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
return INSTANCE;
}

View File

@ -4,11 +4,11 @@
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import com.mojang.serialization.MapCodec;
import net.minecraft.resources.ResourceLocation;
/**
* A functional interface to register a {@link TurtleUpgradeModel} for a class of turtle upgrades.
* A functional interface to register a {@link TurtleUpgradeModel}.
* <p>
* This interface is largely intended to be used from multi-loader code, to allow sharing registration code between
* multiple loaders.
@ -18,9 +18,8 @@ public interface RegisterTurtleUpgradeModel {
/**
* Register a {@link TurtleUpgradeModel}.
*
* @param type The turtle upgrade type.
* @param mode The unbaked upgrade model.
* @param <T> The type of the turtle upgrade.
* @param id The id used for this type of upgrade model.
* @param model The codec used to read/decode an upgrade model.
*/
<T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> mode);
void register(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> model);
}

View File

@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.Util;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.item.SelectItemModel;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
/**
* A {@link TurtleUpgradeModel} which selects between different models based on the value of a component in
* {@linkplain UpgradeData#data() the upgrade's data}.
* <p>
* This is the {@link TurtleUpgradeModel} equivalent of {@link SelectItemModel}.
*
* @param <T> The type of value to switch on.
*/
public final class SelectUpgradeModel<T> implements TurtleUpgradeModel {
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "select");
public static final MapCodec<? extends TurtleUpgradeModel.Unbaked> CODEC = RecordCodecBuilder.<Unbaked<?>>mapCodec(instance -> instance.group(
Cases.CODEC.forGetter(Unbaked::cases),
TurtleUpgradeModel.CODEC.optionalFieldOf("fallback").forGetter(Unbaked::fallback)
).apply(instance, Unbaked::new));
private final DataComponentType<T> component;
private final Map<T, TurtleUpgradeModel> cases;
private final TurtleUpgradeModel fallback;
private SelectUpgradeModel(DataComponentType<T> component, Map<T, TurtleUpgradeModel> cases, TurtleUpgradeModel fallback) {
this.component = component;
this.cases = cases;
this.fallback = fallback;
}
private TurtleUpgradeModel getModel(UpgradeData<ITurtleUpgrade> upgrade) {
var value = upgrade.data().get(component);
if (value == null || value.isEmpty()) return fallback;
var model = cases.get(value.get());
return model != null ? model : fallback;
}
@Override
public void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
getModel(upgrade).renderForItem(upgrade, side, renderer, resolver, transform, seed);
}
@Override
public void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
getModel(upgrade).renderForLevel(upgrade, side, turtle, transform, buffers, light, overlay);
}
private record Unbaked<T>(
Cases<T> cases,
Optional<TurtleUpgradeModel.Unbaked> fallback
) implements TurtleUpgradeModel.Unbaked {
private static final TurtleUpgradeModel.Unbaked MISSING = BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION);
@Override
public MapCodec<? extends TurtleUpgradeModel.Unbaked> type() {
return CODEC;
}
@Override
public TurtleUpgradeModel bake(ModelBaker baker) {
Map<T, TurtleUpgradeModel> cases = new Object2ObjectOpenHashMap<>();
for (var condition : cases().cases()) {
var model = condition.getSecond().bake(baker);
for (var when : condition.getFirst()) cases.put(when, model);
}
return new SelectUpgradeModel<>(cases().component(), cases, fallback().orElse(MISSING).bake(baker));
}
@Override
public void resolveDependencies(Resolver resolver) {
cases().cases().forEach(x -> x.getSecond().resolveDependencies(resolver));
fallback().orElse(MISSING).resolveDependencies(resolver);
}
}
private record Cases<T>(DataComponentType<T> component, List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
private static final MapCodec<Cases<?>> CODEC = DataComponentType.CODEC.dispatchMap("property", Cases::component, Util.memoize(Cases::codec));
private static <T> MapCodec<Cases<T>> codec(DataComponentType<T> component) {
return RecordCodecBuilder.mapCodec(instance -> instance.group(
MapCodec.unit(component).forGetter(Cases::component),
caseCodec(component.codecOrThrow()).listOf().fieldOf("cases").validate(Cases::validate).forGetter(Cases::cases)
).apply(instance, Cases<T>::new));
}
private static <T> Codec<Pair<List<T>, TurtleUpgradeModel.Unbaked>> caseCodec(Codec<T> codec) {
return RecordCodecBuilder.create(instance -> instance.group(
codec.listOf().fieldOf("when").forGetter(Pair::getFirst),
TurtleUpgradeModel.CODEC.fieldOf("model").forGetter(Pair::getSecond)
).apply(instance, Pair::new));
}
private static <T> DataResult<List<Pair<List<T>, TurtleUpgradeModel.Unbaked>>> validate(List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases) {
Multiset<T> multiset = HashMultiset.create();
for (var condition : cases) multiset.addAll(condition.getFirst());
if (multiset.isEmpty()) return DataResult.error(() -> "Empty cases");
if (multiset.size() != multiset.entrySet().size()) {
return DataResult.error(() -> "Duplicate case conditions: " + multiset.entrySet().stream()
.filter(x -> x.getCount() > 1)
.map(x -> Objects.toString(x.getElement()))
.collect(Collectors.joining(", ")));
}
return DataResult.success(cases);
}
}
/**
* Create a {@link SelectUpgradeModel} that selects a model based on a component.
*
* @param component The component to select.
* @param <T> The type the component stores.
* @return A {@link Builder}.
*/
public static <T> Builder<T> onComponent(DataComponentType<T> component) {
return new Builder<>(component);
}
/**
* A builder for constructing {@link SelectUpgradeModel}s.
*
* @param <T> The type of value to switch on.
*/
public static final class Builder<T> {
private final DataComponentType<T> component;
private final List<Pair<List<T>, TurtleUpgradeModel.Unbaked>> cases = new ArrayList<>();
private TurtleUpgradeModel.@Nullable Unbaked fallback;
private Builder(DataComponentType<T> component) {
this.component = component;
}
/**
* Add a case to our model.
*
* @param value The value for this case.
* @param model The model to use.
* @return {@code this}, for chaining.
*/
public Builder<T> when(T value, TurtleUpgradeModel.Unbaked model) {
return when(List.of(value), model);
}
/**
* Add a case to our model.
*
* @param values The value(s) for this case.
* @param model The model to use.
* @return {@code this}, for chaining.
*/
public Builder<T> when(List<T> values, TurtleUpgradeModel.Unbaked model) {
cases.add(Pair.of(values, model));
return this;
}
/**
* Add a fallback value, when no previous value matches or the component is not present.
*
* @param model The fallback model.
* @return {@code this}, for chaining.
*/
public Builder<T> fallback(TurtleUpgradeModel.Unbaked model) {
this.fallback = model;
return this;
}
/**
* Convert this builder into an unbaked model.
*
* @return The unbaked {@link SelectUpgradeModel}.
*/
public TurtleUpgradeModel.Unbaked create() {
return new Unbaked<>(new Cases<>(component, cases), Optional.ofNullable(fallback));
}
}
}

View File

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
record SidedUpgradeModel<T extends ITurtleUpgrade>(
StandaloneModel left, StandaloneModel right
) implements TurtleUpgradeModelViaStandalone<T> {
@Override
public StandaloneModel getModel(T upgrade, TurtleSide side, DataComponentPatch data) {
return switch (side) {
case LEFT -> left();
case RIGHT -> right();
};
}
record Unbaked<T extends ITurtleUpgrade>(
ResourceLocation left, ResourceLocation right
) implements TurtleUpgradeModel.Unbaked<T> {
@Override
public TurtleUpgradeModel<T> bake(ModelBaker baker) {
return new SidedUpgradeModel<>(StandaloneModel.of(left(), baker), StandaloneModel.of(right(), baker));
}
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(left());
resolver.markDependency(right());
}
}
}

View File

@ -5,9 +5,14 @@
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.client.ComputerCraftAPIClientService;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
@ -16,8 +21,6 @@ import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
@ -25,83 +28,80 @@ import net.minecraft.world.item.ItemStack;
/**
* The model for a {@link ITurtleUpgrade}.
* <p>
* Use {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModeller} to register a
* modeller on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent} to register one
* Turtle upgrade models are very similar to vanilla's {@link ItemModel}. Each upgrade's model is defined in JSON, and
* loaded from resource packs with other assets.
* <p>
* In most cases, upgrades can use one of the existing implementations of {@link TurtleUpgradeModel} (e.g.
* {@link BasicUpgradeModel} or {@link ItemUpgradeModel}), and do not need to subclass it. However, in the cases where
* a custom model is required, one should use
* {@code dan200.computercraft.api.client.FabricComputerCraftAPIClient#registerTurtleUpgradeModel} to register a
* model on Fabric and {@code dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent} to register one
* on Forge.
* <p>
* See {@link ITurtleUpgrade} for a full example of registering turtle upgrades and their models.
*
* <h2>Example</h2>
* <h3>Fabric</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_model}
*
* <h3>Forge</h3>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_model}
*
* @param <T> The type of turtle upgrade this modeller applies to.
* @see RegisterTurtleUpgradeModel For multi-loader registration support.
* @see ItemUpgradeModel A {@code TurtleUpgradeModel} which uses the upgrade's item.
* @see BasicUpgradeModel A {@code TurtleUpgradeModel} which renders a simple model.
*/
public interface TurtleUpgradeModel<T extends ITurtleUpgrade> {
public interface TurtleUpgradeModel {
/**
* The directory from which turtle upgrade models are loaded. This may be used by data generators.
*/
String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_upgrade";
/**
* The codec used to read/write {@linkplain TurtleUpgradeModel.Unbaked unbaked upgrade models}.
*/
Codec<TurtleUpgradeModel.Unbaked> CODEC = Codec.lazyInitialized(() -> ComputerCraftAPIClientService.get().getTurtleUpgradeModelCodec());
/**
* Render this upgrade to an {@link ItemStackRenderState}. This is used for rendering the item form of the upgrade.
*
* @param upgrade The upgrade being rendered.
* @param side Which side of the turtle (left or right) the upgrade resides on.
* @param data Upgrade data instance for current turtle side.
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
* @param renderer The render state to draw to.
* @param resolver The model resolver.
* @param transform The root model's transformation.
* @param seed The current model seed.
* @see ItemModel#update(ItemStackRenderState, ItemStack, ItemModelResolver, ItemDisplayContext, ClientLevel, LivingEntity, int)
*/
void renderForItem(T upgrade, TurtleSide side, DataComponentPatch data, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed);
void renderForItem(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed);
/**
* Render this upgrade to a {@link MultiBufferSource}. This is used for rendering the block-entity form of the
* upgrade.
*
* @param upgrade The upgrade being rendered.
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models.
* @param side Which side of the turtle (left or right) the upgrade resides on.
* @param data Upgrade data instance for current turtle side.
* @param side Which side of the turtle (left or right) the upgrade is equipped on.
* @param turtle Access to the turtle that the upgrade resides on.
* @param transform The current pose stack.
* @param buffers The buffers to render to.
* @param light The lightmap coordinate.
* @param overlay The overlay coordinate.
*/
void renderForLevel(T upgrade, ITurtleAccess turtle, TurtleSide side, DataComponentPatch data, PoseStack transform, MultiBufferSource buffers, int light, int overlay);
void renderForLevel(UpgradeData<ITurtleUpgrade> upgrade, TurtleSide side, ITurtleAccess turtle, PoseStack transform, MultiBufferSource buffers, int light, int overlay);
/**
* An unbaked turtle model. Much like other unbaked models (e.g. {@link ItemModel.Unbaked}), this should resolve
* any dependencies and returned the fully-resolved model.
*
* @param <T> The type of turtle upgrade for this model.
*/
interface Unbaked<T extends ITurtleUpgrade> extends ResolvableModel {
TurtleUpgradeModel<T> bake(ModelBaker baker);
}
interface Unbaked extends ResolvableModel {
/**
* The {@link MapCodec} used to read/write this unbaked model.
*
* @return The codec used to read/write this model.
* @see ItemModel.Unbaked#type()
*/
MapCodec<? extends Unbaked> type();
/**
* A basic {@link TurtleUpgradeModel} which renders using the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(DataComponentPatch)
* upgrade item}.
* <p>
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
*
* @param <T> The type of the turtle upgrade.
* @return The constructed modeller.
*/
static <T extends ITurtleUpgrade> TurtleUpgradeModel.Unbaked<? super T> flatItem() {
return ItemUpgradeModel.UNBAKED;
}
/**
* Construct a {@link TurtleUpgradeModel} which has a single model for the left and right side.
*
* @param left The model to use on the left.
* @param right The model to use on the right.
* @param <T> The type of the turtle upgrade.
* @return The constructed modeller.
*/
static <T extends ITurtleUpgrade> TurtleUpgradeModel.Unbaked<T> sided(ResourceLocation left, ResourceLocation right) {
return new SidedUpgradeModel.Unbaked<>(left, right);
/**
* Bake this turtle model.
*
* @param baker The current model baker
* @return The baked upgrade model.
* @see ItemModel.Unbaked#bake(ItemModel.BakingContext)
*/
TurtleUpgradeModel bake(ModelBaker baker);
}
}

View File

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.client.turtle;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.block.model.ItemTransform;
import net.minecraft.client.renderer.item.ItemModelResolver;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.core.component.DataComponentPatch;
public interface TurtleUpgradeModelViaStandalone<T extends ITurtleUpgrade> extends TurtleUpgradeModel<T> {
StandaloneModel getModel(T upgrade, TurtleSide side, DataComponentPatch data);
@Override
default void renderForItem(T upgrade, TurtleSide side, DataComponentPatch data, ItemStackRenderState renderer, ItemModelResolver resolver, ItemTransform transform, int seed) {
var layer = renderer.newLayer();
layer.setTransform(transform);
getModel(upgrade, side, data).setupItemLayer(layer);
}
@Override
default void renderForLevel(T upgrade, ITurtleAccess turtle, TurtleSide side, DataComponentPatch data, PoseStack transform, MultiBufferSource buffers, int light, int overlay) {
getModel(upgrade, side, data).render(transform, buffers, light, overlay);
}
}

View File

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.client;
import com.mojang.serialization.Codec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.impl.Services;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* Bridge between implementation
* <p>
* Do <strong>NOT</strong> directly reference this class. It exists for internal use by the API.
*/
@ApiStatus.Internal
public interface ComputerCraftAPIClientService {
static ComputerCraftAPIClientService get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ComputerCraftAPIClientService.class, Instance.ERROR) : instance;
}
Codec<TurtleUpgradeModel.Unbaked> getTurtleUpgradeModelCodec();
final class Instance {
static final @Nullable ComputerCraftAPIClientService INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(ComputerCraftAPIClientService.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
}

View File

@ -51,16 +51,6 @@ import java.util.function.Function;
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.ForgeExampleMod region=turtle_upgrades}
*
* <h3>Rendering the upgrade</h3>
* Next, we need to register a model for our upgrade. This is done by registering a
* {@link dan200.computercraft.api.client.turtle.TurtleUpgradeModel} for your upgrade type.
*
* <h4>Fabric</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_model}
*
* <h4>Forge</h4>
* {@snippet class=com.example.examplemod.FabricExampleModClient region=turtle_model}
*
* <h3 id="datagen">Registering the upgrade itself</h3>
* Upgrades themselves are loaded from datapacks when a level is loaded. In order to register our new upgrade, we must
* create a new JSON file at {@code data/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
@ -71,8 +61,15 @@ import java.util.function.Function;
* by the type itself. As our upgrade was defined with {@link UpgradeType#simpleWithCustomItem(Function)}, the
* {@code "item"} field will construct our upgrade with {@link Items#COMPASS}.
* <p>
* Rather than manually creating the file, it is recommended to use data-generators to generate this file. First, we
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}.
* Similarly, {@linkplain dan200.computercraft.api.client.turtle.TurtleUpgradeModel the upgrade's model} is loaded from
* a resource pack, and so we must also create a new JSON file at
* {@code assets/<my_mod>/computercraft/turtle_upgrade/<my_upgrade_id>.json}.
*
* {@snippet file=assets/examplemod/computercraft/turtle_upgrade/example_turtle_upgrade.json}
*
* Rather than manually creating these file, it is recommended to use data-generators to generate this file. First, we
* register our new upgrades into a {@linkplain PatchedRegistries patched registry}. Models must similarly be
* registered.
*
* {@snippet class=com.example.examplemod.data.TurtleUpgradeProvider region=body}
*

View File

@ -7,8 +7,7 @@ package dan200.computercraft.client;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.client.turtle.*;
import dan200.computercraft.client.gui.*;
import dan200.computercraft.client.item.colour.PocketComputerLight;
import dan200.computercraft.client.item.model.TurtleOverlayModel;
@ -19,10 +18,9 @@ import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.client.render.CustomLecternRenderer;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleModemModel;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.client.turtle.TurtleUpgradeModels;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.Minecraft;
@ -104,16 +102,9 @@ public final class ClientRegistry {
}
public static void registerTurtleModels(RegisterTurtleUpgradeModel register) {
register.register(ModRegistry.TurtleUpgradeTypes.SPEAKER.get(), TurtleUpgradeModel.sided(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
register.register(ModRegistry.TurtleUpgradeTypes.WORKBENCH.get(), TurtleUpgradeModel.sided(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
register.register(ModRegistry.TurtleUpgradeTypes.WIRELESS_MODEM.get(), TurtleModemModel.UNBAKED);
register.register(ModRegistry.TurtleUpgradeTypes.TOOL.get(), TurtleUpgradeModel.flatItem());
register.register(BasicUpgradeModel.ID, BasicUpgradeModel.CODEC);
register.register(ItemUpgradeModel.ID, ItemUpgradeModel.CODEC);
register.register(SelectUpgradeModel.ID, SelectUpgradeModel.CODEC);
}
public static void registerReloadListeners(BiConsumer<ResourceLocation, PreparableReloadListener> register, Minecraft minecraft) {
@ -132,11 +123,13 @@ public final class ClientRegistry {
* Additional models to load.
*
* @param turtleOverlays The unbaked turtle models.
* @param turtleUpgrades The unbaked turtle upgrades.
* @see #gatherExtraModels(ResourceManager, Executor)
* @see #registerExtraModels(RegisterExtraModels, ExtraModels)
*/
public record ExtraModels(
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays,
Map<ResourceLocation, TurtleUpgradeModel.Unbaked> turtleUpgrades
) {
}
@ -148,8 +141,9 @@ public final class ClientRegistry {
* @return A promise which contains our extra models.
*/
public static CompletableFuture<ExtraModels> gatherExtraModels(ResourceManager resources, Executor executor) {
var turtleOverlays = TurtleOverlayManager.load(resources, executor);
return turtleOverlays.thenApply(ExtraModels::new);
var turtleOverlays = TurtleOverlayManager.loader().load(resources, executor);
var turtleUpgrades = TurtleUpgradeModelManager.loader().load(resources, executor);
return turtleOverlays.thenCombine(turtleUpgrades, ExtraModels::new);
}
/**
@ -181,8 +175,8 @@ public final class ClientRegistry {
for (var model : EXTRA_MODELS) {
register.register(getModel(model), model, (id, r) -> r.markDependency(id), StandaloneModel::of);
}
TurtleOverlayManager.register(register, models.turtleOverlays());
TurtleUpgradeModels.bake(register);
TurtleOverlayManager.loader().register(register, models.turtleOverlays());
TurtleUpgradeModelManager.loader().register(register, models.turtleUpgrades());
}
public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.shared.util.ResourceUtils;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
/**
* A manager for loading custom models. This is responsible for {@linkplain #load(ResourceManager, Executor) loading
* models from resource packs}, {@linkplain #register(ClientRegistry.RegisterExtraModels, Map) registering them as
* extra models}, and then {@linkplain #get(ModelManager, ResourceLocation) looking them up}.
*
* @param <U> The type of unbaked model.
* @param <T> The type of baked model.
*/
public class CustomModelManager<U extends ResolvableModel, T> {
private final String kind;
private final FileToIdConverter lister;
private final Codec<U> codec;
private final BiFunction<U, ModelBaker, T> bake;
private final ModelKey<T> missingModelKey;
private final U missingModel;
private final Map<ResourceLocation, ModelKey<T>> modelKeys = new ConcurrentHashMap<>();
public CustomModelManager(String kind, FileToIdConverter lister, Codec<U> codec, BiFunction<U, ModelBaker, T> bake, U missingModel) {
this.kind = kind;
this.lister = lister;
this.codec = codec;
this.bake = bake;
this.missingModelKey = ClientPlatformHelper.get().createModelKey(MissingBlockModel.LOCATION, () -> "Missing " + kind);
this.missingModel = missingModel;
}
private ModelKey<T> getModelKey(ResourceLocation id) {
return modelKeys.computeIfAbsent(id, o -> ClientPlatformHelper.get().createModelKey(o, () -> kind + " " + o));
}
/**
* Load our models from resources.
*
* @param resources The current resource manager.
* @param executor The executor to schedule work on.
* @return The map of unbaked models.
*/
public CompletableFuture<Map<ResourceLocation, U>> load(ResourceManager resources, Executor executor) {
return ResourceUtils.load(resources, executor, kind, lister, JsonOps.INSTANCE, codec);
}
/**
* Register our unbaked models.
*
* @param register The callback to register models with.
* @param models The models to register.
*/
public void register(ClientRegistry.RegisterExtraModels register, Map<ResourceLocation, U> models) {
models.forEach((id, model) -> register.register(getModelKey(id), model, bake));
register.register(missingModelKey, missingModel, bake);
}
/**
* Find the model with the given id. If the model does not exist, then the missing model is returned instead.
*
* @param modelManager The model manager.
* @param id The model id.
* @return The loaded model.
*/
public T get(ModelManager modelManager, ResourceLocation id) {
var model = getModelKey(id).get(modelManager);
if (model != null) return model;
var missing = missingModelKey.get(modelManager);
if (missing == null) throw new IllegalStateException("Models have not yet been loaded.");
return missing;
}
}

View File

@ -40,7 +40,7 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
if (overlay == null) return;
var layer = state.newLayer();
TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
layer.setTransform(transforms().getTransform(context));
}

View File

@ -8,8 +8,9 @@ import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.turtle.TurtleUpgradeModels;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.block.model.ItemTransforms;
import net.minecraft.client.renderer.item.ItemModel;
@ -39,7 +40,8 @@ public record TurtleUpgradeModel(TurtleSide side, ItemTransforms base) implement
var upgrade = TurtleItem.getUpgradeWithData(stack, side);
if (upgrade == null) return;
TurtleUpgradeModels.getModeller(upgrade.upgrade()).renderForItem(upgrade.upgrade(), side, upgrade.data(), state, resolver, base.getTransform(context), seed);
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
.renderForItem(upgrade, side, state, resolver, base.getTransform(context), seed);
}
public record Unbaked(TurtleSide side, ResourceLocation base) implements ItemModel.Unbaked {

View File

@ -31,7 +31,7 @@ public class TurtleShowElfOverlay implements ConditionalItemModelProperty {
@Override
public boolean get(ItemStack stack, @Nullable ClientLevel level, @Nullable LivingEntity holder, int i, ItemDisplayContext context) {
var overlay = TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
return overlay == null || overlay.showElfOverlay();
}

View File

@ -11,7 +11,7 @@ import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.ClientRegistry;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.client.turtle.TurtleUpgradeModels;
import dan200.computercraft.client.turtle.TurtleUpgradeModelManager;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
@ -79,7 +79,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
// Render the turtle
var colour = turtle.getColour();
var overlay = TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
var overlay = TurtleOverlayManager.get(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
if (colour == -1) {
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
@ -103,7 +103,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
private void renderUpgrade(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
var upgrade = turtle.getUpgrade(side);
var upgrade = turtle.getAccess().getUpgradeWithData(side);
if (upgrade == null) return;
transform.pushPose();
@ -112,8 +112,8 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.mulPose(Axis.XN.rotationDegrees(toolAngle));
transform.translate(0.0f, -0.5f, -0.5f);
TurtleUpgradeModels.getModeller(upgrade).renderForLevel(upgrade, turtle.getAccess(), side, turtle.getAccess().getUpgradeData(side), transform, buffers, lightmapCoord, overlayLight);
TurtleUpgradeModelManager.get(Minecraft.getInstance().getModelManager(), upgrade.holder())
.renderForLevel(upgrade, side, turtle.getAccess(), transform, buffers, lightmapCoord, overlayLight);
transform.popPose();
}

View File

@ -1,89 +0,0 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModelViaStandalone;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import dan200.computercraft.shared.util.DataComponentUtil;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A {@link TurtleUpgradeModel} for modems, providing different models depending on if the modem is on/off.
*
* @param normal The models for a normal wireless modem.
* @param advanced The models for an advanced/ender modem.
*/
public record TurtleModemModel(
ModemModels<StandaloneModel> normal, ModemModels<StandaloneModel> advanced
) implements TurtleUpgradeModelViaStandalone<TurtleModem> {
public static final TurtleUpgradeModel.Unbaked<TurtleModem> UNBAKED = new Unbaked();
@Override
public StandaloneModel getModel(TurtleModem modem, TurtleSide side, DataComponentPatch data) {
var active = DataComponentUtil.isPresent(data, ModRegistry.DataComponents.ON.get(), x -> x);
var models = modem.advanced() ? advanced() : normal();
return side == TurtleSide.LEFT
? (active ? models.leftOnModel() : models.leftOffModel())
: (active ? models.rightOnModel() : models.rightOffModel());
}
private static final class Unbaked implements TurtleUpgradeModel.Unbaked<TurtleModem> {
@Override
public TurtleUpgradeModel<TurtleModem> bake(ModelBaker baker) {
return new TurtleModemModel(
ModemModels.NORMAL.map(x -> StandaloneModel.of(x, baker)),
ModemModels.ADVANCED.map(x -> StandaloneModel.of(x, baker))
);
}
@Override
public void resolveDependencies(Resolver resolver) {
ModemModels.NORMAL.forEach(resolver::markDependency);
ModemModels.ADVANCED.forEach(resolver::markDependency);
}
}
private record ModemModels<T>(
T leftOffModel, T rightOffModel,
T leftOnModel, T rightOnModel
) {
private static final ModemModels<ResourceLocation> NORMAL = create("normal");
private static final ModemModels<ResourceLocation> ADVANCED = create("advanced");
static ModemModels<ResourceLocation> create(String type) {
return new ModemModels<>(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_off_right"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_on_right")
);
}
public void forEach(Consumer<T> out) {
out.accept(leftOffModel());
out.accept(rightOffModel);
out.accept(leftOnModel());
out.accept(rightOnModel());
}
public <U> ModemModels<U> map(Function<T, U> mapper) {
return new ModemModels<>(
mapper.apply(leftOffModel()), mapper.apply(rightOffModel()),
mapper.apply(leftOnModel()), mapper.apply(rightOnModel())
);
}
}
}

View File

@ -4,66 +4,27 @@
package dan200.computercraft.client.turtle;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.client.ClientRegistry;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.shared.util.ResourceUtils;
import dan200.computercraft.client.CustomModelManager;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* A manager for loading {@link TurtleOverlay}s. This is responsible for {@linkplain #load(ResourceManager, Executor)
* loading overlays from resource packs}, {@linkplain #register(ClientRegistry.RegisterExtraModels, Map) baking them}, and
* then {@linkplain #getOverlay(ModelManager, ResourceLocation) looking them up}.
* The model manager for {@link TurtleOverlay}s.
*/
public class TurtleOverlayManager {
private static final FileToIdConverter ID_CONVERTER = FileToIdConverter.json(TurtleOverlay.SOURCE);
/**
* The {@link ModelKey} for the missing turtle overlay. This is used by
* {@link #getOverlay(ModelManager, ResourceLocation)} when an overlay does not exist.
*/
private static final ModelKey<TurtleOverlay> MISSING_KEY = ClientPlatformHelper.get().createModelKey(
MissingBlockModel.LOCATION, () -> "Missing turtle overlay"
private static final CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader = new CustomModelManager<>(
"turtle overlay", FileToIdConverter.json(TurtleOverlay.SOURCE), TurtleOverlay.CODEC,
TurtleOverlay.Unbaked::bake,
new TurtleOverlay.Unbaked(MissingBlockModel.LOCATION, false)
);
private static final Map<ResourceLocation, ModelKey<TurtleOverlay>> modelKeys = new ConcurrentHashMap<>();
private static ModelKey<TurtleOverlay> getModelKey(ResourceLocation overlay) {
return modelKeys.computeIfAbsent(overlay, o -> ClientPlatformHelper.get().createModelKey(o, () -> "Turtle overlay " + o));
}
/**
* Load our overlays from resources.
*
* @param resources The current resource manager.
* @param executor The executor to schedule work on.
* @return The map of unbaked overlay.
*/
public static CompletableFuture<Map<ResourceLocation, TurtleOverlay.Unbaked>> load(ResourceManager resources, Executor executor) {
return ResourceUtils.load(resources, executor, "turtle overlay", ID_CONVERTER, JsonOps.INSTANCE, TurtleOverlay.CODEC);
}
/**
* Register our unbaked overlay models.
*
* @param register The callback to register models with.
* @param overlays The overlays to register.
*/
public static void register(ClientRegistry.RegisterExtraModels register, Map<ResourceLocation, TurtleOverlay.Unbaked> overlays) {
overlays.forEach((id, overlay) -> register.register(getModelKey(id), overlay, TurtleOverlay.Unbaked::bake));
register.register(MISSING_KEY, new TurtleOverlay.Unbaked(MissingBlockModel.LOCATION, false), TurtleOverlay.Unbaked::bake);
public static CustomModelManager<TurtleOverlay.Unbaked, TurtleOverlay> loader() {
return loader;
}
/**
@ -75,14 +36,7 @@ public class TurtleOverlayManager {
* @return The turtle overlay.
*/
@Contract("_, null -> null; _, !null -> !null")
public static @Nullable TurtleOverlay getOverlay(ModelManager modelManager, @Nullable ResourceLocation id) {
if (id == null) return null;
var overlay = getModelKey(id).get(modelManager);
if (overlay != null) return overlay;
var missing = MISSING_KEY.get(modelManager);
if (missing == null) throw new IllegalStateException("Rendering turtles before models are baked");
return missing;
public static @Nullable TurtleOverlay get(ModelManager modelManager, @Nullable ResourceLocation id) {
return id == null ? null : loader.get(modelManager, id);
}
}

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.CustomModelManager;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.core.Holder;
import net.minecraft.resources.FileToIdConverter;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;
/**
* The model manager for {@link TurtleUpgradeModel}s.
*/
public final class TurtleUpgradeModelManager {
private static final CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader = new CustomModelManager<>(
"turtle upgrade", FileToIdConverter.json(TurtleUpgradeModel.SOURCE), TurtleUpgradeModel.CODEC,
TurtleUpgradeModel.Unbaked::bake,
BasicUpgradeModel.unbaked(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION)
);
public static CustomModelManager<TurtleUpgradeModel.Unbaked, TurtleUpgradeModel> loader() {
return loader;
}
/**
* Find the model for the given turtle upgrade.
*
* @param modelManager The model manager.
* @param upgrade The turtle upgrade
* @return The turtle upgrade model.
*/
@Contract("_, null -> null; _, !null -> !null")
public static @Nullable TurtleUpgradeModel get(ModelManager modelManager, Holder.@Nullable Reference<ITurtleUpgrade> upgrade) {
return upgrade == null ? null : loader.get(modelManager, upgrade.key().location());
}
}

View File

@ -1,88 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.client.ClientRegistry;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.shared.util.RegistryHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.MissingBlockModel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A registry of {@link TurtleUpgradeModel}s.
*/
public final class TurtleUpgradeModels {
private static final Object fetchedLock = new Object();
private static volatile boolean fetchedModels;
private static final Map<ModelKey<? extends TurtleUpgradeModel<?>>, TurtleUpgradeModel.Unbaked<?>> unbaked = new ConcurrentHashMap<>();
private static final Map<UpgradeType<? extends ITurtleUpgrade>, ModelKey<? extends TurtleUpgradeModel<?>>> modelKeys = new ConcurrentHashMap<>();
public static final ModelKey<TurtleUpgradeModel<ITurtleUpgrade>> missingModelKey = ClientPlatformHelper.get().createModelKey(
MissingBlockModel.LOCATION,
() -> "Missing turtle model"
);
private TurtleUpgradeModels() {
}
public static <T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> modeller) {
if (fetchedModels) {
throw new IllegalStateException(String.format(
"Turtle upgrade type %s must be registered before models are baked.",
RegistryHelper.getKeyOrThrow(RegistryHelper.getRegistry(ITurtleUpgrade.typeRegistry()), type)
));
}
if (unbaked.putIfAbsent(getModelKey(type), modeller) != null) {
throw new IllegalStateException("Modeller already registered for serialiser");
}
}
public static void fetch(Runnable action) {
if (fetchedModels) return;
synchronized (fetchedLock) {
if (fetchedModels) return;
action.run();
fetchedModels = true;
}
}
@SuppressWarnings("unchecked")
private static <T extends ITurtleUpgrade> ModelKey<TurtleUpgradeModel<? super T>> getModelKey(UpgradeType<T> type) {
return (ModelKey<TurtleUpgradeModel<? super T>>) modelKeys.computeIfAbsent(type, t -> {
var id = RegistryHelper.getKeyOrThrow(RegistryHelper.getRegistry(ITurtleUpgrade.typeRegistry()), t);
return ClientPlatformHelper.get().createModelKey(
RegistryHelper.getKeyOrThrow(RegistryHelper.getRegistry(ITurtleUpgrade.typeRegistry()), t),
() -> "Turtle upgrade " + id
);
});
}
public static <T extends ITurtleUpgrade> TurtleUpgradeModel<? super T> getModeller(T upgrade) {
var modelManager = Minecraft.getInstance().getModelManager();
@SuppressWarnings("unchecked")
var model = getModelKey((UpgradeType<T>) upgrade.getType()).get(modelManager);
if (model != null) return model;
var missing = missingModelKey.get(modelManager);
if (missing == null) throw new IllegalStateException("Rendering turtles before models are baked");
return missing;
}
@SuppressWarnings("unchecked")
public static void bake(ClientRegistry.RegisterExtraModels register) {
unbaked.forEach((id, model) ->
register.register((ModelKey<TurtleUpgradeModel<?>>) id, model, TurtleUpgradeModel.Unbaked::bake));
register.register(missingModelKey, TurtleUpgradeModel.sided(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION), TurtleUpgradeModel.Unbaked::bake);
}
}

View File

@ -5,6 +5,7 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
@ -52,7 +53,7 @@ public final class DataProviders {
var fullRegistryPatch = RegistryPatchGenerator.createLookup(
generator.registries(),
Util.make(new RegistrySetBuilder(), builder -> {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::register);
builder.add(IPocketUpgrade.REGISTRY, PocketUpgradeProvider::addUpgrades);
}));
var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
@ -89,6 +90,7 @@ public final class DataProviders {
});
generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register);
generator.addFromCodec("Turtle upgrade models", PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, TurtleUpgradeProvider::addModels);
generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
}

View File

@ -6,6 +6,10 @@ package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.client.turtle.BasicUpgradeModel;
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
import dan200.computercraft.api.client.turtle.SelectUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.turtle.upgrades.TurtleCraftingTable;
@ -17,21 +21,21 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.function.BiConsumer;
import static dan200.computercraft.api.turtle.TurtleToolBuilder.tool;
class TurtleUpgradeProvider {
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(id("speaker"), new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
upgrades.register(vanilla("crafting_table"), new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
upgrades.register(id("wireless_modem_normal"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
upgrades.register(id("wireless_modem_advanced"), new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
private static final ResourceKey<ITurtleUpgrade> SPEAKER = id("speaker");
private static final ResourceKey<ITurtleUpgrade> CRAFTING_TABLE = vanilla("crafting_table");
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_NORMAL = id("wireless_modem_normal");
private static final ResourceKey<ITurtleUpgrade> WIRELESS_MODEM_ADVANCED = id("wireless_modem_advanced");
tool(vanilla("diamond_axe").location(), Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
tool(vanilla("diamond_pickaxe"), Items.DIAMOND_PICKAXE).register(upgrades);
tool(vanilla("diamond_hoe"), Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
tool(vanilla("diamond_shovel"), Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
tool(vanilla("diamond_sword"), Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
}
private static final ResourceKey<ITurtleUpgrade> DIAMOND_AXE = vanilla("diamond_axe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_PICKAXE = vanilla("diamond_pickaxe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_HOE = vanilla("diamond_hoe");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SHOVEL = vanilla("diamond_shovel");
private static final ResourceKey<ITurtleUpgrade> DIAMOND_SWORD = vanilla("diamond_sword");
private static ResourceKey<ITurtleUpgrade> id(String id) {
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, id));
@ -41,4 +45,52 @@ class TurtleUpgradeProvider {
// Naughty, please don't do this. Mostly here for some semblance of backwards compatibility.
return ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath("minecraft", id));
}
public static void register(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(SPEAKER, new TurtleSpeaker(new ItemStack(ModRegistry.Items.SPEAKER.get())));
upgrades.register(CRAFTING_TABLE, new TurtleCraftingTable(new ItemStack(Items.CRAFTING_TABLE)));
upgrades.register(WIRELESS_MODEM_NORMAL, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_NORMAL.get()), false));
upgrades.register(WIRELESS_MODEM_ADVANCED, new TurtleModem(new ItemStack(ModRegistry.Items.WIRELESS_MODEM_ADVANCED.get()), true));
tool(DIAMOND_AXE, Items.DIAMOND_AXE).damageMultiplier(6.0f).register(upgrades);
tool(DIAMOND_PICKAXE, Items.DIAMOND_PICKAXE).register(upgrades);
tool(DIAMOND_HOE, Items.DIAMOND_HOE).breakable(ComputerCraftTags.Blocks.TURTLE_HOE_BREAKABLE).register(upgrades);
tool(DIAMOND_SHOVEL, Items.DIAMOND_SHOVEL).breakable(ComputerCraftTags.Blocks.TURTLE_SHOVEL_BREAKABLE).register(upgrades);
tool(DIAMOND_SWORD, Items.DIAMOND_SWORD).breakable(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).damageMultiplier(9.0f).register(upgrades);
}
public static void addModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> out) {
out.accept(SPEAKER.location(), BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_speaker_right")
));
out.accept(CRAFTING_TABLE.location(), BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_crafting_table_right")
));
out.accept(WIRELESS_MODEM_NORMAL.location(), createModemModel("normal"));
out.accept(WIRELESS_MODEM_ADVANCED.location(), createModemModel("advanced"));
out.accept(DIAMOND_AXE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_PICKAXE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_HOE.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_SHOVEL.location(), ItemUpgradeModel.unbaked());
out.accept(DIAMOND_SWORD.location(), ItemUpgradeModel.unbaked());
}
private static TurtleUpgradeModel.Unbaked createModemModel(String type) {
return SelectUpgradeModel.onComponent(ModRegistry.DataComponents.ON.get())
.when(false, createBaseModemModel(type, "off"))
.when(true, createBaseModemModel(type, "on"))
.fallback(createBaseModemModel(type, "off"))
.create();
}
private static TurtleUpgradeModel.Unbaked createBaseModemModel(String type, String state) {
return BasicUpgradeModel.unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_left"),
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_modem_" + type + "_" + state + "_right")
);
}
}

View File

@ -24,6 +24,7 @@ import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.color.item.Dye;
import net.minecraft.client.data.models.BlockModelGenerators;
import net.minecraft.client.data.models.MultiVariant;
import net.minecraft.client.data.models.blockstates.ConditionBuilder;
@ -216,7 +217,7 @@ public class BlockModelProvider {
generators.itemModelOutput.accept(block.asItem(), ItemModelUtils.composite(
ItemModelUtils.conditional(
new HasComponent(DataComponents.DYED_COLOR, false),
ItemModelUtils.plainModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL),
ItemModelUtils.tintedModel(TurtleBlockEntityRenderer.COLOUR_TURTLE_MODEL, new Dye(-1)),
ItemModelUtils.plainModel(model)
),
new TurtleUpgradeModel.Unbaked(TurtleSide.LEFT, model),

View File

@ -11,7 +11,7 @@ import net.minecraft.world.item.ItemStack;
// @start region=body
public class ExampleTurtleUpgrade extends AbstractTurtleUpgrade {
public ExampleTurtleUpgrade(ItemStack stack) {
super(TurtleUpgradeType.PERIPHERAL, "example", stack);
super(TurtleUpgradeType.PERIPHERAL, "upgrade.examplemod.example_turtle_upgrade.adjective", stack);
}
@Override

View File

@ -2,6 +2,8 @@ package com.example.examplemod.data;
import com.example.examplemod.ExampleMod;
import com.example.examplemod.ExampleTurtleUpgrade;
import dan200.computercraft.api.client.turtle.ItemUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.Util;
import net.minecraft.core.HolderLookup;
@ -13,18 +15,19 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* Extends the bootstrap registries with our {@linkplain ExampleTurtleUpgrade example turtle upgrade}.
*/
// @start region=body
public class TurtleUpgradeProvider {
// Define our upgrade ids.
private static final ResourceLocation EXAMPLE_TURTLE_UPGRADE = ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade");
// Register our turtle upgrades.
public static void addUpgrades(BootstrapContext<ITurtleUpgrade> upgrades) {
upgrades.register(
ITurtleUpgrade.createKey(ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_turtle_upgrade")),
new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS))
);
upgrades.register(ITurtleUpgrade.createKey(EXAMPLE_TURTLE_UPGRADE), new ExampleTurtleUpgrade(new ItemStack(Items.COMPASS)));
}
// Set up the dynamic registries to contain our turtle upgrades.
@ -33,5 +36,10 @@ public class TurtleUpgradeProvider {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
}));
}
// Register our turtle models.
public static void addUpgradeModels(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> models) {
models.accept(EXAMPLE_TURTLE_UPGRADE, ItemUpgradeModel.unbaked());
}
}
// @end region=body

View File

@ -0,0 +1,5 @@
{
"type": "computercraft:sided",
"left": "computercraft:block/turtle_speaker_left",
"right": "computercraft:block/turtle_speaker_right"
}

View File

@ -0,0 +1,27 @@
{
"type": "computercraft:select",
"cases": [
{
"model": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_advanced_off_left",
"right": "computercraft:block/turtle_modem_advanced_off_right"
},
"when": [false]
},
{
"model": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_advanced_on_left",
"right": "computercraft:block/turtle_modem_advanced_on_right"
},
"when": [true]
}
],
"fallback": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_advanced_off_left",
"right": "computercraft:block/turtle_modem_advanced_off_right"
},
"property": "computercraft:on"
}

View File

@ -0,0 +1,27 @@
{
"type": "computercraft:select",
"cases": [
{
"model": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_normal_off_left",
"right": "computercraft:block/turtle_modem_normal_off_right"
},
"when": [false]
},
{
"model": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_normal_on_left",
"right": "computercraft:block/turtle_modem_normal_on_right"
},
"when": [true]
}
],
"fallback": {
"type": "computercraft:sided",
"left": "computercraft:block/turtle_modem_normal_off_left",
"right": "computercraft:block/turtle_modem_normal_off_right"
},
"property": "computercraft:on"
}

View File

@ -6,7 +6,11 @@
"type": "minecraft:condition",
"component": "minecraft:dyed_color",
"on_false": {"type": "minecraft:model", "model": "computercraft:block/turtle_advanced"},
"on_true": {"type": "minecraft:model", "model": "computercraft:block/turtle_colour"},
"on_true": {
"type": "minecraft:model",
"model": "computercraft:block/turtle_colour",
"tints": [{"type": "minecraft:dye", "default": -1}]
},
"property": "minecraft:has_component"
},
{"type": "computercraft:turtle/upgrade", "side": "left", "transforms": "computercraft:block/turtle_advanced"},

View File

@ -6,7 +6,11 @@
"type": "minecraft:condition",
"component": "minecraft:dyed_color",
"on_false": {"type": "minecraft:model", "model": "computercraft:block/turtle_normal"},
"on_true": {"type": "minecraft:model", "model": "computercraft:block/turtle_colour"},
"on_true": {
"type": "minecraft:model",
"model": "computercraft:block/turtle_colour",
"tints": [{"type": "minecraft:dye", "default": -1}]
},
"property": "minecraft:has_component"
},
{"type": "computercraft:turtle/upgrade", "side": "left", "transforms": "computercraft:block/turtle_normal"},

View File

@ -0,0 +1,5 @@
{
"type": "computercraft:sided",
"left": "computercraft:block/turtle_crafting_table_left",
"right": "computercraft:block/turtle_crafting_table_right"
}

View File

@ -0,0 +1 @@
{"type": "computercraft:item"}

View File

@ -0,0 +1 @@
{"type": "computercraft:item"}

View File

@ -0,0 +1 @@
{"type": "computercraft:item"}

View File

@ -0,0 +1 @@
{"type": "computercraft:item"}

View File

@ -0,0 +1 @@
{"type": "computercraft:item"}

View File

@ -56,7 +56,7 @@ public class ResourceUtils {
var result = codec.parse(ops, JsonParser.parseReader(reader))
.ifError(e -> LOG.error("Couldn't parse {} '{}' from pack '{}': {}", kind, id, resource.sourcePackId(), e.message()))
.result().orElse(null);
return result == null ? null : new Pair<>(id, result);
return result == null ? null : Pair.of(id, result);
} catch (Exception exception) {
LOG.error("Failed to open {} {} from pack '{}'", kind, resource, resource.sourcePackId(), exception);
return null;

View File

@ -4,11 +4,11 @@
package dan200.computercraft.api.client;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.impl.client.FabricComputerCraftAPIClientService;
import net.minecraft.resources.ResourceLocation;
/**
* The Fabric-specific entrypoint for ComputerCraft's client-side API.
@ -27,12 +27,11 @@ public final class FabricComputerCraftAPIClient {
* <p>
* This method may be used as a {@link RegisterTurtleUpgradeModel}, for convenient use in multi-loader code.
*
* @param type The turtle upgrade type.
* @param model The upgrade model.
* @param <T> The type of the turtle upgrade.
* @param id The id used for this type of upgrade model.
* @param codec The codec used to read/decode an upgrade model.
*/
public static <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> model) {
getInstance().registerTurtleUpgradeModeller(type, model);
public static void registerTurtleUpgradeModeller(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> codec) {
getInstance().registerTurtleUpgradeModeller(id, codec);
}
private static FabricComputerCraftAPIClientService getInstance() {

View File

@ -4,12 +4,10 @@
package dan200.computercraft.impl.client;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.impl.Services;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* Backing interface for CC's client-side API.
@ -17,25 +15,10 @@ import org.jspecify.annotations.Nullable;
* Do <strong>NOT</strong> directly reference this class. It exists for internal use by the API.
*/
@ApiStatus.Internal
public interface FabricComputerCraftAPIClientService {
public interface FabricComputerCraftAPIClientService extends ComputerCraftAPIClientService {
static FabricComputerCraftAPIClientService get() {
var instance = Instance.INSTANCE;
return instance == null ? Services.raise(FabricComputerCraftAPIClientService.class, Instance.ERROR) : instance;
return (FabricComputerCraftAPIClientService) ComputerCraftAPIClientService.get();
}
<T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> model);
final class Instance {
static final @Nullable FabricComputerCraftAPIClientService INSTANCE;
static final @Nullable Throwable ERROR;
static {
var helper = Services.tryLoad(FabricComputerCraftAPIClientService.class);
INSTANCE = helper.instance();
ERROR = helper.error();
}
private Instance() {
}
}
void registerTurtleUpgradeModeller(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> codec);
}

View File

@ -5,15 +5,26 @@
package dan200.computercraft.impl.client;
import com.google.auto.service.AutoService;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.upgrades.UpgradeType;
import dan200.computercraft.client.turtle.TurtleUpgradeModels;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ExtraCodecs;
@AutoService(FabricComputerCraftAPIClientService.class)
import java.util.function.Function;
@AutoService(ComputerCraftAPIClientService.class)
public final class FabricComputerCraftAPIClientImpl implements FabricComputerCraftAPIClientService {
private static final ExtraCodecs.LateBoundIdMapper<ResourceLocation, MapCodec<? extends TurtleUpgradeModel.Unbaked>> TURTLE_UPGRADE_MODELS = new ExtraCodecs.LateBoundIdMapper<>();
private static final Codec<TurtleUpgradeModel.Unbaked> TURTLE_UPGRADE_CODEC = TURTLE_UPGRADE_MODELS.codec(ResourceLocation.CODEC).dispatch(TurtleUpgradeModel.Unbaked::type, Function.identity());
@Override
public <T extends ITurtleUpgrade> void registerTurtleUpgradeModeller(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> modeller) {
TurtleUpgradeModels.register(type, modeller);
public Codec<TurtleUpgradeModel.Unbaked> getTurtleUpgradeModelCodec() {
return TURTLE_UPGRADE_CODEC;
}
@Override
public void registerTurtleUpgradeModeller(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> codec) {
TURTLE_UPGRADE_MODELS.put(id, codec);
}
}

View File

@ -1,14 +0,0 @@
package com.example.examplemod;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.fabricmc.api.ClientModInitializer;
public class FabricExampleModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// @start region=turtle_model
FabricComputerCraftAPIClient.registerTurtleUpgradeModeller(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModel.flatItem());
// @end region=turtle_model
}
}

View File

@ -1,15 +1,20 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint;
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider;
import net.fabricmc.fabric.api.event.registry.DynamicRegistries;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistrySetBuilder;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* Data generators for our Fabric example mod.
@ -25,6 +30,17 @@ public class FabricExampleModDataGenerator implements DataGeneratorEntrypoint {
private static void addTurtleUpgrades(FabricDataGenerator.Pack pack, CompletableFuture<HolderLookup.Provider> registries) {
var fullRegistryPatch = TurtleUpgradeProvider.makeUpgradeRegistry(registries);
pack.addProvider((FabricDataOutput output) -> new AutomaticDynamicRegistryProvider(output, fullRegistryPatch));
pack.addProvider((FabricDataOutput output) -> new FabricCodecDataProvider<>(output, registries, PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC) {
@Override
public String getName() {
return "Turtle upgrade models";
}
@Override
protected void configure(BiConsumer<ResourceLocation, TurtleUpgradeModel.Unbaked> out, HolderLookup.Provider provider) {
TurtleUpgradeProvider.addUpgradeModels(out);
}
});
}
/**

View File

@ -4,9 +4,8 @@
package dan200.computercraft.api.client.turtle;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleUpgradeType;
import dan200.computercraft.api.upgrades.UpgradeType;
import com.mojang.serialization.MapCodec;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.bus.api.Event;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
@ -14,8 +13,7 @@ import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import org.jetbrains.annotations.ApiStatus;
/**
* This event is fired to register {@link TurtleUpgradeModel}s for a mod's {@linkplain TurtleUpgradeType turtle
* upgrades}.
* This event is fired to register additional {@link TurtleUpgradeModel}s.
* <p>
* This event is fired during the initial mod construction. Registries will be frozen, but mods may not be fully
* initialised at this point (i.e. {@link FMLCommonSetupEvent} or {@link FMLClientSetupEvent} may not have been
@ -30,10 +28,10 @@ public class RegisterTurtleModelEvent extends Event implements IModBusEvent, Reg
}
/**
* {@inheritDoc}
* {@inheritDoc}The codec used to read/decode an upgrade model.
*/
@Override
public <T extends ITurtleUpgrade> void register(UpgradeType<T> type, TurtleUpgradeModel.Unbaked<? super T> modeller) {
dispatch.register(type, modeller);
public void register(ResourceLocation id, MapCodec<? extends TurtleUpgradeModel.Unbaked> model) {
dispatch.register(id, model);
}
}

View File

@ -10,7 +10,6 @@ import dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent;
import dan200.computercraft.client.platform.ForgeModelKey;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.client.render.ExtendedItemFrameRenderState;
import dan200.computercraft.client.turtle.TurtleUpgradeModels;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.entity.ItemFrameRenderer;
import net.minecraft.client.resources.model.ModelBaker;
@ -19,7 +18,6 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.context.ContextKey;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.event.*;
@ -44,8 +42,6 @@ public final class ForgeClientRegistry {
@SubscribeEvent
public static void registerModels(ModelEvent.RegisterStandalone event) {
TurtleUpgradeModels.fetch(() -> ModLoader.postEvent(new RegisterTurtleModelEvent(TurtleUpgradeModels::register)));
// Load resources
Queue<Runnable> tasks = new ArrayDeque<>();
var state = ClientRegistry.gatherExtraModels(Minecraft.getInstance().getResourceManager(), tasks::add);

View File

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl.client;
import com.google.auto.service.AutoService;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.ExtraCodecs;
import net.neoforged.fml.ModLoader;
import java.util.function.Function;
@AutoService(ComputerCraftAPIClientService.class)
public final class ForgeComputerCraftAPIClientImpl implements ComputerCraftAPIClientService {
@Override
public Codec<TurtleUpgradeModel.Unbaked> getTurtleUpgradeModelCodec() {
var idMapper = new ExtraCodecs.LateBoundIdMapper<ResourceLocation, MapCodec<? extends TurtleUpgradeModel.Unbaked>>();
ModLoader.postEvent(new RegisterTurtleModelEvent(idMapper::put));
return idMapper.codec(ResourceLocation.CODEC).dispatch(TurtleUpgradeModel.Unbaked::type, Function.identity());
}
}

View File

@ -1,20 +0,0 @@
package com.example.examplemod;
import dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
/**
* The client-side entry point for the Forge version of our example mod.
*/
@EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD)
public class ForgeExampleModClient {
// @start region=turtle_model
@SubscribeEvent
public static void onRegisterTurtleModellers(RegisterTurtleModelEvent event) {
event.register(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModel.flatItem());
}
// @end region=turtle_model
}

View File

@ -1,11 +1,14 @@
package com.example.examplemod;
import com.example.examplemod.data.TurtleUpgradeProvider;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.DataGenerator;
import net.minecraft.data.PackOutput;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.common.data.DatapackBuiltinEntriesProvider;
import net.neoforged.neoforge.common.data.JsonCodecProvider;
import net.neoforged.neoforge.data.event.GatherDataEvent;
import java.util.Set;
@ -17,7 +20,7 @@ import java.util.concurrent.CompletableFuture;
@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD)
public class ForgeExampleModDataGenerator {
@SubscribeEvent
public static void gather(GatherDataEvent event) {
public static void gather(GatherDataEvent.Client event) {
var pack = event.getGenerator().getVanillaPack(true);
addTurtleUpgrades(pack, event.getLookupProvider());
}
@ -26,6 +29,12 @@ public class ForgeExampleModDataGenerator {
private static void addTurtleUpgrades(DataGenerator.PackGenerator pack, CompletableFuture<HolderLookup.Provider> registries) {
var fullRegistryPatch = TurtleUpgradeProvider.makeUpgradeRegistry(registries);
pack.addProvider(o -> new DatapackBuiltinEntriesProvider(o, fullRegistryPatch, Set.of(ExampleMod.MOD_ID)));
pack.addProvider(o -> new JsonCodecProvider<>(o, PackOutput.Target.RESOURCE_PACK, TurtleUpgradeModel.SOURCE, TurtleUpgradeModel.CODEC, registries, ExampleMod.MOD_ID) {
@Override
protected void gather() {
TurtleUpgradeProvider.addUpgradeModels(this::unconditional);
}
});
}
// @end region=turtle_upgrades
}