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

Load turtle overlays during model loading

Back in f10e401aeac0b141b230607f6c3347215f0093cc, we changed turtle
overlays to be loaded as a dynamic registry. This solved some of the
problems we had with upgrades (elf-compatibility), and seemed like a
good idea at the time.

However, because overlays are part of datapacks (not resource packs), we
also needed support for loading overlay models, which we did via an
"extra_models.json" file.

With the ItemModel changes, we can go for a different approach:
 - Turtle overlays are now stored on the item/BE as a simple
   ResourceLocation again.

 - The TurtleOverlay class is moved to the client, and loaded from
   resource packs, during model loading. They're now split into a baked
   form (holding a StandaloneModel) and an unbaked one (holding the
   ResourceLocation).

 - extra_model.json is no longer supported.
This commit is contained in:
Jonathan Coates 2025-05-10 22:14:58 +01:00
parent e13e8ff92e
commit 598fd98a8b
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
31 changed files with 365 additions and 408 deletions

View File

@ -20,10 +20,11 @@ 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.shared.ModRegistry;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemTintSource;
import net.minecraft.client.gui.screens.MenuScreens;
@ -35,17 +36,22 @@ import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
import net.minecraft.client.renderer.item.properties.select.SelectItemModelProperty;
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.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
/**
* Registers client-side objects, such as {@link BlockEntityRendererProvider}s and
@ -122,14 +128,61 @@ public final class ClientRegistry {
MissingBlockModel.LOCATION,
};
public static void registerExtraModels(
BiConsumer<ModelKey<StandaloneModel>, ResourceLocation> registerBasic,
BiConsumer<ModelKey<? extends TurtleUpgradeModel<?>>, TurtleUpgradeModel.Unbaked<?>> registerTurtle,
Collection<ResourceLocation> extraModels
/**
* Additional models to load.
*
* @param turtleOverlays The unbaked turtle models.
* @see #gatherExtraModels(ResourceManager, Executor)
* @see #registerExtraModels(RegisterExtraModels, ExtraModels)
*/
public record ExtraModels(
Map<ResourceLocation, TurtleOverlay.Unbaked> turtleOverlays
) {
for (var model : EXTRA_MODELS) registerBasic.accept(getModel(model), model);
for (var model : extraModels) registerBasic.accept(getModel(model), model);
TurtleUpgradeModels.bake(registerTurtle);
}
/**
* Gather the list of extra models to load.
*
* @param resources The current resource manager.
* @param executor The executor to schedule loading on.
* @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);
}
/**
* A callback used to register a model for a {@link ModelKey}.
*/
public interface RegisterExtraModels {
default <U extends ResolvableModel, T> void register(ModelKey<T> key, U unbaked, BiFunction<U, ModelBaker, T> bake) {
register(key, unbaked, ResolvableModel::resolveDependencies, bake);
}
/**
* Register an extra model.
* <p>
* This accepts functions to resolve dependencies and bake the model. While this would be conceptually nicer as
* an interface, it would require multiple adaptors to convert between "upgrade model", "a"bstract model" and
* "platform-specific model", so working with functions is cleaner.
*
* @param key The model key for this model.
* @param unbaked The unbaked model.
* @param resolve The function to resolve dependencies for this model.
* @param bake The function to bake this model.
* @param <U> The type of unbaked model.
* @param <T> The type of baked model.
*/
<U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake);
}
public static void registerExtraModels(RegisterExtraModels register, ExtraModels models) {
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);
}
public static void registerItemModels(BiConsumer<ResourceLocation, MapCodec<? extends ItemModel.Unbaked>> register) {

View File

@ -7,8 +7,8 @@ package dan200.computercraft.client.item.model;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.ClientRegistry;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
@ -40,7 +40,7 @@ public record TurtleOverlayModel(ItemTransforms transforms) implements ItemModel
if (overlay == null) return;
var layer = state.newLayer();
ClientRegistry.getModel(Minecraft.getInstance().getModelManager(), overlay.model()).setupItemLayer(layer);
TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), overlay).model().setupItemLayer(layer);
layer.setTransform(transforms().getTransform(context));
}

View File

@ -6,8 +6,10 @@ package dan200.computercraft.client.item.properties;
import com.mojang.serialization.MapCodec;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.client.turtle.TurtleOverlayManager;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.item.properties.conditional.ConditionalItemModelProperty;
import net.minecraft.resources.ResourceLocation;
@ -29,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 = TurtleItem.getOverlay(stack);
var overlay = TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), TurtleItem.getOverlay(stack));
return overlay == null || overlay.showElfOverlay();
}

View File

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
/**
* A list of extra models to load on the client.
* <p>
* This is largely intended for use with {@linkplain TurtleOverlay turtle overlays}. As overlays are stored in a dynamic
* registry, they are not available when resources are loaded, and so we need a way to request the overlays' models be
* loaded.
*
* @param models The models to load.
*/
public record ExtraModels(List<ResourceLocation> models) {
private static final Logger LOG = LoggerFactory.getLogger(ExtraModels.class);
private static final Gson GSON = new Gson();
/**
* The path where the extra models are listed.
*/
public static final ResourceLocation PATH = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "extra_models.json");
/**
* The coded used to store the extra model file.
*/
public static final Codec<ExtraModels> CODEC = ResourceLocation.CODEC.listOf().xmap(ExtraModels::new, ExtraModels::models);
/**
* Get the list of all extra models to load.
*
* @param resources The current resource manager.
* @return A set of all resources to load.
*/
public static Collection<ResourceLocation> loadAll(ResourceManager resources) {
Set<ResourceLocation> out = new HashSet<>();
for (var path : resources.getResourceStack(PATH)) {
ExtraModels models;
try (var stream = path.openAsReader()) {
models = ExtraModels.CODEC.parse(JsonOps.INSTANCE, GSON.fromJson(stream, JsonElement.class)).getOrThrow(JsonParseException::new);
} catch (IOException | RuntimeException e) {
LOG.error("Failed to load extra models from {}", path.sourcePackId());
continue;
}
out.addAll(models.models());
}
return Collections.unmodifiableCollection(out);
}
}

View File

@ -9,9 +9,10 @@ import com.mojang.math.Axis;
import dan200.computercraft.api.ComputerCraftAPI;
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.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@ -78,7 +79,7 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
// Render the turtle
var colour = turtle.getColour();
var overlay = turtle.getOverlay();
var overlay = TurtleOverlayManager.getOverlay(Minecraft.getInstance().getModelManager(), turtle.getOverlay());
if (colour == -1) {
renderModel(transform, buffers, lightmapCoord, overlayLight, turtle.getFamily() == ComputerFamily.NORMAL ? NORMAL_TURTLE_MODEL : ADVANCED_TURTLE_MODEL, null);
@ -88,10 +89,10 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
}
// Render the overlay
if (overlay != null) renderModel(transform, buffers, lightmapCoord, overlayLight, overlay.model(), null);
if (overlay != null) overlay.model().render(transform, buffers, lightmapCoord, overlayLight);
// And the Christmas overlay.
var showChristmas = TurtleOverlay.showElfOverlay(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
var showChristmas = Holiday.getCurrent() == Holiday.CHRISTMAS && (overlay == null || overlay.showElfOverlay());
if (showChristmas) renderModel(transform, buffers, lightmapCoord, overlayLight, TurtleOverlay.ELF_MODEL, null);
// Render the upgrades

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.turtle;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.resources.ResourceLocation;
/**
* A cosmetic overlay on a turtle.
*
* @param model The path to the overlay's model.
* @param showElfOverlay Whether this overlay is compatible with the {@linkplain #ELF_MODEL Christmas elf model}.
* @see ModRegistry.DataComponents#OVERLAY
*/
public record TurtleOverlay(StandaloneModel model, boolean showElfOverlay) {
/**
* The folder where upgrades are loaded from.
*/
public static final String SOURCE = ComputerCraftAPI.MOD_ID + "/turtle_overlay";
/**
* The codec used to read/write turtle overlay definitions from resource packs.
*/
public static final Codec<TurtleOverlay.Unbaked> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("model").forGetter(TurtleOverlay.Unbaked::model),
Codec.BOOL.optionalFieldOf("show_elf_overlay", false).forGetter(TurtleOverlay.Unbaked::showElfOverlay)
).apply(instance, TurtleOverlay.Unbaked::new));
/**
* An additional overlay that is rendered on all turtles at {@linkplain Holiday#CHRISTMAS Christmas}.
*
* @see #showElfOverlay()
*/
public static final ResourceLocation ELF_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
public record Unbaked(ResourceLocation model, boolean showElfOverlay) implements ResolvableModel {
@Override
public void resolveDependencies(Resolver resolver) {
resolver.markDependency(model());
}
public TurtleOverlay bake(ModelBaker baker) {
return new TurtleOverlay(StandaloneModel.of(model(), baker), showElfOverlay());
}
}
}

View File

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
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 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}.
*/
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 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);
}
/**
* Find the turtle overlay with the given id. If the overlay does not exist, then the "missing model" overlay is
* returned instead.
*
* @param modelManager The model manager.
* @param id The overlay id.
* @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;
}
}

View File

@ -7,6 +7,7 @@ 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;
@ -15,7 +16,6 @@ import net.minecraft.client.resources.model.MissingBlockModel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
/**
* A registry of {@link TurtleUpgradeModel}s.
@ -79,8 +79,10 @@ public final class TurtleUpgradeModels {
return missing;
}
public static void bake(BiConsumer<ModelKey<? extends TurtleUpgradeModel<?>>, TurtleUpgradeModel.Unbaked<?>> baker) {
unbaked.forEach(baker);
baker.accept(missingModelKey, TurtleUpgradeModel.sided(MissingBlockModel.LOCATION, MissingBlockModel.LOCATION));
@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

@ -10,10 +10,9 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.model.LecternPocketModel;
import dan200.computercraft.client.model.LecternPrintoutModel;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.data.client.BlockModelProvider;
import dan200.computercraft.data.client.ExtraModelsProvider;
import dan200.computercraft.data.client.ItemModelProvider;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.Util;
import net.minecraft.client.data.models.BlockModelGenerators;
@ -55,7 +54,6 @@ public final class DataProviders {
Util.make(new RegistrySetBuilder(), builder -> {
builder.add(ITurtleUpgrade.REGISTRY, TurtleUpgradeProvider::addUpgrades);
builder.add(IPocketUpgrade.REGISTRY, PocketUpgradeProvider::addUpgrades);
builder.add(TurtleOverlay.REGISTRY, TurtleOverlays::register);
}));
var fullRegistries = fullRegistryPatch.thenApply(RegistrySetBuilder.PatchedRegistries::full);
@ -90,12 +88,7 @@ public final class DataProviders {
));
});
generator.add(pack -> new ExtraModelsProvider(pack, fullRegistries) {
@Override
public Stream<ResourceLocation> getModels(HolderLookup.Provider registries) {
return registries.lookupOrThrow(TurtleOverlay.REGISTRY).listElements().map(x -> x.value().model());
}
});
generator.addFromCodec("Turtle overlays", PackOutput.Target.RESOURCE_PACK, TurtleOverlay.SOURCE, TurtleOverlay.CODEC, TurtleOverlays::register);
generator.addModels(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
}

View File

@ -24,7 +24,6 @@ import dan200.computercraft.shared.recipe.ImpostorShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapedRecipe;
import dan200.computercraft.shared.recipe.TransformShapelessRecipe;
import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.items.TurtleItem;
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
import dan200.computercraft.shared.util.ColourUtils;
@ -178,13 +177,11 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
);
}
private void turtleOverlay(ResourceKey<TurtleOverlay> overlay, Consumer<ShapelessSpecBuilder> build) {
var holder = registries.lookupOrThrow(overlay.registryKey()).getOrThrow(overlay);
private void turtleOverlay(ResourceLocation overlay, Consumer<ShapelessSpecBuilder> build) {
for (var turtleItem : turtleItems()) {
var name = RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, turtleItem);
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), holder))
var builder = customShapeless(RecipeCategory.REDSTONE, DataComponentUtil.createStack(turtleItem, ModRegistry.DataComponents.OVERLAY.get(), overlay))
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", has(turtleItem));
build.accept(builder);
@ -193,7 +190,7 @@ final class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.build(s -> new TransformShapelessRecipe(s, List.of(
CopyComponents.builder(turtleItem).exclude(ModRegistry.DataComponents.OVERLAY.get()).build()
)))
.save(output, name.withSuffix("_overlays/" + overlay.location().getPath()));
.save(output, name.withSuffix("_overlays/" + overlay.getPath()));
}
}

View File

@ -5,32 +5,32 @@
package dan200.computercraft.data;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.data.worldgen.BootstrapContext;
import net.minecraft.resources.ResourceKey;
import dan200.computercraft.client.turtle.TurtleOverlay;
import net.minecraft.resources.ResourceLocation;
import java.util.function.BiConsumer;
/**
* Built-in turtle overlays.
*/
final class TurtleOverlays {
public static final ResourceKey<TurtleOverlay> RAINBOW_FLAG = create("rainbow_flag");
public static final ResourceKey<TurtleOverlay> TRANS_FLAG = create("trans_flag");
public static final ResourceLocation RAINBOW_FLAG = create("rainbow_flag");
public static final ResourceLocation TRANS_FLAG = create("trans_flag");
private static ResourceKey<TurtleOverlay> create(String name) {
return ResourceKey.create(TurtleOverlay.REGISTRY, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name));
private static ResourceLocation create(String name) {
return ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, name);
}
private TurtleOverlays() {
}
public static void register(BootstrapContext<TurtleOverlay> registry) {
registry.register(RAINBOW_FLAG, new TurtleOverlay(
public static void register(BiConsumer<ResourceLocation, TurtleOverlay.Unbaked> registry) {
registry.accept(RAINBOW_FLAG, new TurtleOverlay.Unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_rainbow_overlay"),
true
));
registry.register(TRANS_FLAG, new TurtleOverlay(
registry.accept(TRANS_FLAG, new TurtleOverlay.Unbaked(
ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_trans_overlay"),
true
));

View File

@ -11,6 +11,7 @@ import dan200.computercraft.client.item.model.TurtleOverlayModel;
import dan200.computercraft.client.item.model.TurtleUpgradeModel;
import dan200.computercraft.client.item.properties.TurtleShowElfOverlay;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleOverlay;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
@ -21,7 +22,6 @@ import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemBlock;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock;
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState;
import dan200.computercraft.shared.peripheral.printer.PrinterBlock;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.data.models.BlockModelGenerators;

View File

@ -1,52 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
import com.mojang.serialization.JsonOps;
import dan200.computercraft.client.model.ExtraModels;
import net.minecraft.core.HolderLookup;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
/**
* A data provider to generate {@link ExtraModels}.
*/
public abstract class ExtraModelsProvider implements DataProvider {
private final Path path;
private final CompletableFuture<HolderLookup.Provider> registries;
public ExtraModelsProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
path = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(ExtraModels.PATH.getNamespace()).resolve(ExtraModels.PATH.getPath());
this.registries = registries;
}
/**
* Return a stream of models to load.
*
* @param registries The current registries.
* @return The collection of extra models to load.
*/
public abstract Stream<ResourceLocation> getModels(HolderLookup.Provider registries);
@Override
public final CompletableFuture<?> run(CachedOutput output) {
return registries.thenCompose(registries -> {
var models = new ExtraModels(getModels(registries).sorted().toList());
var json = ExtraModels.CODEC.encodeStart(JsonOps.INSTANCE, models).getOrThrow(IllegalStateException::new);
return DataProvider.saveStable(output, json, path);
});
}
@Override
public final String getName() {
return "Extra Models";
}
}

View File

@ -1 +0,0 @@
["computercraft:block/turtle_rainbow_overlay", "computercraft:block/turtle_trans_overlay"]

View File

@ -81,7 +81,6 @@ import dan200.computercraft.shared.recipe.*;
import dan200.computercraft.shared.recipe.function.CopyComponents;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.apis.TurtleAPI;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
@ -389,8 +388,8 @@ public final class ModRegistry {
/**
* The overlay on a turtle.
*/
public static final RegistryEntry<DataComponentType<Holder<TurtleOverlay>>> OVERLAY = register("overlay", b -> b
.persistent(TurtleOverlay.CODEC).networkSynchronized(TurtleOverlay.STREAM_CODEC)
public static final RegistryEntry<DataComponentType<ResourceLocation>> OVERLAY = register("overlay", b -> b
.persistent(ResourceLocation.CODEC).networkSynchronized(ResourceLocation.STREAM_CODEC).cacheEncoding()
);
/**

View File

@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.turtle;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.RegistryFileCodec;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import org.jspecify.annotations.Nullable;
/**
* A cosmetic overlay on a turtle.
*
* @param model The path to the overlay's model.
* @param showElfOverlay Whether this overlay is compatible with the {@linkplain #ELF_MODEL Christmas elf model}.
* @see ModRegistry.DataComponents#OVERLAY
*/
public record TurtleOverlay(ResourceLocation model, boolean showElfOverlay) {
/**
* The registry turtle overlays are stored in.
*/
public static final ResourceKey<Registry<TurtleOverlay>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "turtle_overlay"));
/**
* The codec used to read/write turtle overlay definitions from datapacks.
*/
public static final Codec<TurtleOverlay> DIRECT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("model").forGetter(TurtleOverlay::model),
Codec.BOOL.optionalFieldOf("show_elf_overlay", false).forGetter(TurtleOverlay::showElfOverlay)
).apply(instance, TurtleOverlay::new));
/**
* The codec used for {@link TurtleOverlay} instances.
*
* @see ModRegistry.DataComponents#OVERLAY
*/
public static final Codec<Holder<TurtleOverlay>> CODEC = RegistryFileCodec.create(REGISTRY, DIRECT_CODEC);
/**
* The stream codec used for {@link TurtleOverlay} instances.
*/
public static final StreamCodec<RegistryFriendlyByteBuf, Holder<TurtleOverlay>> STREAM_CODEC = ByteBufCodecs.holder(REGISTRY, StreamCodec.composite(
ResourceLocation.STREAM_CODEC, TurtleOverlay::model,
ByteBufCodecs.BOOL, TurtleOverlay::showElfOverlay,
TurtleOverlay::new
));
/**
* An additional overlay that is rendered on all turtles at {@linkplain Holiday#CHRISTMAS Christmas}.
*
* @see #showElfOverlay()
* @see #showElfOverlay(TurtleOverlay, boolean)
*/
public static final ResourceLocation ELF_MODEL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
/**
* Determine whether we should show the {@linkplain #ELF_MODEL elf overlay}.
*
* @param overlay The current {@link TurtleOverlay}.
* @param christmas Whether it is Christmas.
* @return Whether we should show the elf overlay.
*/
public static boolean showElfOverlay(@Nullable TurtleOverlay overlay, boolean christmas) {
return christmas && (overlay == null || overlay.showElfOverlay());
}
}

View File

@ -21,7 +21,6 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.container.BasicContainer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import net.minecraft.core.BlockPos;
@ -33,6 +32,7 @@ import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
@ -233,9 +233,8 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
return brain.getColour();
}
public @Nullable TurtleOverlay getOverlay() {
var overlay = brain.getOverlay();
return overlay == null ? null : overlay.value();
public @Nullable ResourceLocation getOverlay() {
return brain.getOverlay();
}
public ITurtleAccess getAccess() {

View File

@ -20,7 +20,6 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.container.InventoryDelegate;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.BlockEntityHelpers;
import dan200.computercraft.shared.util.Holiday;
@ -32,6 +31,7 @@ import net.minecraft.core.HolderLookup;
import net.minecraft.core.component.DataComponentPatch;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.Container;
@ -74,7 +74,7 @@ public class TurtleBrain implements TurtleAccessInternal {
private int selectedSlot = 0;
private int fuelLevel = 0;
private int colourHex = -1;
private @Nullable Holder<TurtleOverlay> overlay = null;
private @Nullable ResourceLocation overlay = null;
private TurtleAnimation animation = TurtleAnimation.NONE;
private int animationProgress = 0;
@ -134,7 +134,7 @@ public class TurtleBrain implements TurtleAccessInternal {
// Read fields
colourHex = nbt.getIntOr(NBT_COLOUR, -1);
fuelLevel = nbt.getIntOr(NBT_FUEL, 0);
overlay = nbt.contains(NBT_OVERLAY) ? NBTUtil.decodeFrom(TurtleOverlay.CODEC, registries, nbt, NBT_OVERLAY) : null;
overlay = nbt.contains(NBT_OVERLAY) ? NBTUtil.decodeFrom(ResourceLocation.CODEC, registries, nbt, NBT_OVERLAY) : null;
// Read upgrades
setUpgradeDirect(TurtleSide.LEFT, NBTUtil.decodeFrom(TurtleUpgrades.instance().upgradeDataCodec(), registries, nbt, NBT_LEFT_UPGRADE));
@ -144,7 +144,7 @@ public class TurtleBrain implements TurtleAccessInternal {
private void writeCommon(CompoundTag nbt, HolderLookup.Provider registries) {
nbt.putInt(NBT_FUEL, fuelLevel);
if (colourHex != -1) nbt.putInt(NBT_COLOUR, colourHex);
NBTUtil.encodeTo(TurtleOverlay.CODEC, registries, nbt, NBT_OVERLAY, overlay);
NBTUtil.encodeTo(ResourceLocation.CODEC, registries, nbt, NBT_OVERLAY, overlay);
// Write upgrades
NBTUtil.encodeTo(TurtleUpgrades.instance().upgradeDataCodec(), registries, nbt, NBT_LEFT_UPGRADE, getUpgradeWithData(TurtleSide.LEFT));
@ -418,11 +418,11 @@ public class TurtleBrain implements TurtleAccessInternal {
BlockEntityHelpers.updateBlock(owner);
}
public @Nullable Holder<TurtleOverlay> getOverlay() {
public @Nullable ResourceLocation getOverlay() {
return overlay;
}
public void setOverlay(@Nullable Holder<TurtleOverlay> overlay) {
public void setOverlay(@Nullable ResourceLocation overlay) {
if (!Objects.equals(this.overlay, overlay)) {
this.overlay = overlay;
BlockEntityHelpers.updateBlock(owner);

View File

@ -11,12 +11,12 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.cauldron.CauldronInteraction;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
@ -51,9 +51,8 @@ public class TurtleItem extends BlockItem {
});
}
public static @Nullable TurtleOverlay getOverlay(ItemStack stack) {
var overlay = stack.get(ModRegistry.DataComponents.OVERLAY.get());
return overlay == null ? null : overlay.value();
public static @Nullable ResourceLocation getOverlay(ItemStack stack) {
return stack.get(ModRegistry.DataComponents.OVERLAY.get());
}
public static final CauldronInteraction CAULDRON_INTERACTION = (blockState, level, pos, player, hand, stack) -> {

View File

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import org.jspecify.annotations.Nullable;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
/**
* A list which prepends a single value to another list.
*
* @param <T> The type of item in the list.
*/
public final class ConsList<T> extends AbstractList<T> {
private final T head;
private final List<T> tail;
public ConsList(T head, List<T> tail) {
this.head = head;
this.tail = tail;
}
@Override
public T get(int index) {
return index == 0 ? head : tail.get(index - 1);
}
@Override
public int size() {
return 1 + tail.size();
}
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
private @Nullable Iterator<T> tailIterator;
@Override
public boolean hasNext() {
return tailIterator == null || tailIterator.hasNext();
}
@Override
public T next() {
if (tailIterator != null) return tailIterator.next();
tailIterator = tail.iterator();
return head;
}
};
}
}

View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DynamicOps;
import net.minecraft.Util;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
public class ResourceUtils {
private static final Logger LOG = LoggerFactory.getLogger(ResourceUtils.class);
/**
* A version of {@link SimpleJsonResourceReloadListener#scanDirectory(ResourceManager, FileToIdConverter, DynamicOps, Codec, Map)},
* that runs in parallel over multiple executors.
* <p>
* This shares some similarity with how {@link net.minecraft.client.resources.model.ModelManager#loadBlockModels(ResourceManager, Executor)}
* loads resources.
*
* @param resourceManager The current resource manager.
* @param executor The executor to schedule work on.
* @param kind The name of the thing we're loading, for use in error messages.
* @param lister The file-to-id-converter used to locate files.
* @param ops The dynamic ops used when decoding.
* @param codec The codec used to decode each file.
* @param <T> The type of the entry to load
* @return The loaded resources.
*/
public static <T> CompletableFuture<Map<ResourceLocation, T>> load(
ResourceManager resourceManager, Executor executor, String kind, FileToIdConverter lister, DynamicOps<JsonElement> ops, Codec<T> codec
) {
return CompletableFuture.supplyAsync(() -> lister.listMatchingResources(resourceManager), executor).thenCompose(resources -> {
List<CompletableFuture<@Nullable Pair<ResourceLocation, T>>> futures = new ArrayList<>(resources.size());
resources.forEach((path, resource) -> futures.add(CompletableFuture.supplyAsync(() -> {
var id = lister.fileToId(path);
try (var reader = resource.openAsReader()) {
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);
} catch (Exception exception) {
LOG.error("Failed to open {} {} from pack '{}'", kind, resource, resource.sourcePackId(), exception);
return null;
}
}, executor)));
return Util.sequence(futures).thenApply(result -> result
.stream().filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Pair::getFirst, Pair::getSecond)));
});
}
}

View File

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* A couple of trivial tests for {@link ConsList}, mostly as a quick safety check.
*/
public class ConsListTest {
@Test
public void testGet() {
var list = new ConsList<>(1, List.of(2, 3, 4));
assertEquals(1, list.get(0));
assertEquals(2, list.get(1));
assertEquals(4, list.get(3));
}
@Test
public void testSize() {
var list = new ConsList<>(1, List.of(2, 3, 4));
assertEquals(4, list.size());
}
@Test
public void testIterator() {
var list = new ConsList<>(1, List.of(2, 3, 4));
assertArrayEquals(new Integer[]{ 1, 2, 3, 4 }, list.toArray(Integer[]::new));
}
}

View File

@ -14,7 +14,6 @@ import dan200.computercraft.api.turtle.TurtleSide
import dan200.computercraft.api.upgrades.UpgradeData
import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.gametest.api.*
import dan200.computercraft.gametest.api.GameTest
import dan200.computercraft.gametest.core.TestHooks
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
@ -24,7 +23,6 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState
import dan200.computercraft.shared.turtle.TurtleOverlay
import dan200.computercraft.shared.turtle.apis.TurtleAPI
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity
import dan200.computercraft.shared.turtle.core.TurtleCraftCommand
@ -772,8 +770,7 @@ class Turtle_Test {
@GameTest
fun Data_fixers(helper: GameTestHelper) = helper.sequence {
thenExecute {
val overlay = helper.level.registryAccess().lookupOrThrow(TurtleOverlay.REGISTRY)
.getValue(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "trans_flag"))!!
val overlay = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "trans_flag")
val upgrade = helper.level.registryAccess().lookupOrThrow(ITurtleUpgrade.REGISTRY)
.getValue(ResourceLocation.withDefaultNamespace("diamond_pickaxe"))!!

View File

@ -9,11 +9,8 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.FabricComputerCraftAPIClient;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.model.ExtraModels;
import dan200.computercraft.client.platform.FabricModelKey;
import dan200.computercraft.client.platform.ModelKey;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.impl.Services;
import dan200.computercraft.shared.ComputerCraft;
@ -27,7 +24,6 @@ import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallba
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.model.loading.v1.SimpleUnbakedExtraModel;
import net.fabricmc.fabric.api.client.model.loading.v1.UnbakedExtraModel;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
@ -41,11 +37,13 @@ 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.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ResolvableModel;
import net.minecraft.world.phys.BlockHitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
@ -67,12 +65,13 @@ public class ComputerCraftClient {
ClientRegistry.registerConditionalItemProperties(ConditionalItemModelProperties.ID_MAPPER::put);
PreparableModelLoadingPlugin.register(
(resources, executor) -> CompletableFuture.supplyAsync(() -> ExtraModels.loadAll(resources), executor),
(state, context) -> ClientRegistry.registerExtraModels(
(key, model) -> context.addModel(FabricModelKey.key(key), new SimpleUnbakedExtraModel<>(model, StandaloneModel::of)),
(key, model) -> context.addModel(FabricModelKey.erased(key), new TurtleModelWrapper<>(model)),
state
)
ClientRegistry::gatherExtraModels,
(state, context) -> ClientRegistry.registerExtraModels(new ClientRegistry.RegisterExtraModels() {
@Override
public <U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake) {
context.addModel(FabricModelKey.key(key), new ModelWrapper<>(unbaked, resolve, bake));
}
}, state)
);
BlockRenderLayerMap.INSTANCE.putBlock(ModRegistry.Blocks.COMPUTER_NORMAL.get(), RenderType.cutout());
@ -108,17 +107,17 @@ public class ComputerCraftClient {
((FabricConfigFile) ConfigSpec.clientSpec).load(FabricLoader.getInstance().getConfigDir().resolve(ComputerCraftAPI.MOD_ID + "-client.toml"));
}
private record TurtleModelWrapper<T extends ITurtleUpgrade>(
TurtleUpgradeModel.Unbaked<T> model
) implements UnbakedExtraModel<TurtleUpgradeModel<T>> {
private record ModelWrapper<U, T>(
U model, BiConsumer<U, Resolver> resolve, BiFunction<U, ModelBaker, T> bake
) implements UnbakedExtraModel<T> {
@Override
public TurtleUpgradeModel<T> bake(ModelBaker baker) {
return model().bake(baker);
public T bake(ModelBaker baker) {
return bake().apply(model(), baker);
}
@Override
public void resolveDependencies(Resolver resolver) {
model().resolveDependencies(resolver);
resolve().accept(model(), resolver);
}
}
}

View File

@ -24,11 +24,6 @@ public final class FabricModelKey<T> implements ModelKey<T> {
return ((FabricModelKey<T>) key).key;
}
@SuppressWarnings("unchecked")
public static <T> ExtraModelKey<T> erased(ModelKey<?> key) {
return ((FabricModelKey<T>) key).key;
}
@Override
public @Nullable T get(ModelManager manager) {
return manager.getModel(key);

View File

@ -23,7 +23,6 @@ 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;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@ -85,7 +84,6 @@ public class ComputerCraft {
DynamicRegistries.registerSynced(ITurtleUpgrade.REGISTRY, TurtleUpgrades.instance().upgradeCodec());
DynamicRegistries.registerSynced(IPocketUpgrade.REGISTRY, PocketUpgrades.instance().upgradeCodec());
DynamicRegistries.registerSynced(TurtleOverlay.REGISTRY, TurtleOverlay.DIRECT_CODEC);
ModRegistry.register();
ModRegistry.registerMainThread();

View File

@ -6,12 +6,9 @@ package dan200.computercraft.client;
import com.google.common.reflect.TypeToken;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.client.StandaloneModel;
import dan200.computercraft.api.client.turtle.RegisterTurtleModelEvent;
import dan200.computercraft.api.client.turtle.TurtleUpgradeModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.client.model.ExtraModels;
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;
@ -29,6 +26,11 @@ import net.neoforged.neoforge.client.event.*;
import net.neoforged.neoforge.client.model.standalone.UnbakedStandaloneModel;
import net.neoforged.neoforge.client.renderstate.RegisterRenderStateModifiersEvent;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
/**
* Registers textures and models for items.
@ -44,12 +46,18 @@ public final class ForgeClientRegistry {
public static void registerModels(ModelEvent.RegisterStandalone event) {
TurtleUpgradeModels.fetch(() -> ModLoader.postEvent(new RegisterTurtleModelEvent(TurtleUpgradeModels::register)));
var extraModels = ExtraModels.loadAll(Minecraft.getInstance().getResourceManager());
ClientRegistry.registerExtraModels(
(key, model) -> event.register(ForgeModelKey.key(key), StandaloneModel::of),
(key, model) -> event.register(ForgeModelKey.erased(key), new TurtleModelWrapper<>(model)),
extraModels
);
// Load resources
Queue<Runnable> tasks = new ArrayDeque<>();
var state = ClientRegistry.gatherExtraModels(Minecraft.getInstance().getResourceManager(), tasks::add);
Runnable task;
while ((task = tasks.poll()) != null) task.run();
ClientRegistry.registerExtraModels(new ClientRegistry.RegisterExtraModels() {
@Override
public <U, T> void register(ModelKey<T> key, U unbaked, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake) {
event.register(ForgeModelKey.key(key), new ModelWrapper<>(unbaked, resolve, bake));
}
}, state.resultNow());
}
@SubscribeEvent
@ -102,17 +110,17 @@ public final class ForgeClientRegistry {
ClientRegistry.register();
}
private record TurtleModelWrapper<T extends ITurtleUpgrade>(
TurtleUpgradeModel.Unbaked<T> model
) implements UnbakedStandaloneModel<TurtleUpgradeModel<T>> {
private record ModelWrapper<U, T>(
U model, BiConsumer<U, ResolvableModel.Resolver> resolve, BiFunction<U, ModelBaker, T> bake
) implements UnbakedStandaloneModel<T> {
@Override
public TurtleUpgradeModel<T> bake(ModelBaker baker) {
return model().bake(baker);
public T bake(ModelBaker baker) {
return bake().apply(model(), baker);
}
@Override
public void resolveDependencies(ResolvableModel.Resolver resolver) {
model().resolveDependencies(resolver);
resolve().accept(model(), resolver);
}
}
}

View File

@ -24,11 +24,6 @@ public final class ForgeModelKey<T> implements ModelKey<T> {
return ((ForgeModelKey<T>) key).key;
}
@SuppressWarnings("unchecked")
public static <T> StandaloneModelKey<T> erased(ModelKey<?> key) {
return ((ForgeModelKey<T>) key).key;
}
@Override
public @Nullable T get(ModelManager manager) {
return manager.getStandaloneModel(key);

View File

@ -30,7 +30,6 @@ import dan200.computercraft.shared.peripheral.generic.methods.FluidMethods;
import dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods;
import dan200.computercraft.shared.platform.ForgeConfigFile;
import dan200.computercraft.shared.recipe.function.RecipeFunction;
import dan200.computercraft.shared.turtle.TurtleOverlay;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
@ -104,7 +103,6 @@ public final class ComputerCraft {
public static void registerDynamicRegistries(DataPackRegistryEvent.NewRegistry event) {
event.dataPackRegistry(ITurtleUpgrade.REGISTRY, TurtleUpgrades.instance().upgradeCodec(), TurtleUpgrades.instance().upgradeCodec());
event.dataPackRegistry(IPocketUpgrade.REGISTRY, PocketUpgrades.instance().upgradeCodec(), PocketUpgrades.instance().upgradeCodec());
event.dataPackRegistry(TurtleOverlay.REGISTRY, TurtleOverlay.DIRECT_CODEC, TurtleOverlay.DIRECT_CODEC);
}
@SubscribeEvent