1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-02 22:53:15 +00:00

Merge branch 'mc-1.19.x' into mc-1.20.x

This commit is contained in:
Jonathan Coates
2023-08-05 10:36:37 +01:00
102 changed files with 2272 additions and 1195 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,17 +5,28 @@
package dan200.computercraft.client.platform;
import com.google.auto.service.AutoService;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.client.model.FoiledModel;
import dan200.computercraft.client.render.ModelRenderer;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext;
import dan200.computercraft.shared.platform.NetworkHandler;
import net.fabricmc.fabric.api.client.model.BakedModelManagerHelper;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.Sheets;
import net.minecraft.client.renderer.entity.ItemRenderer;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import javax.annotation.Nullable;
@AutoService(dan200.computercraft.impl.client.ClientPlatformHelper.class)
public class ClientPlatformHelperImpl implements ClientPlatformHelper {
private static final RandomSource random = RandomSource.create(0);
@Override
public void sendToServer(NetworkMessage<ServerNetworkContext> message) {
Minecraft.getInstance().player.connection.send(NetworkHandler.encodeServer(message));
@@ -23,7 +34,25 @@ public class ClientPlatformHelperImpl implements ClientPlatformHelper {
@Override
public BakedModel getModel(ModelManager manager, ResourceLocation location) {
var model = BakedModelManagerHelper.getModel(manager, location);
var model = manager.getModel(location);
return model == null ? manager.getMissingModel() : model;
}
@Override
public BakedModel createdFoiledModel(BakedModel model) {
return new FoiledModel(model);
}
@Override
public void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints) {
// Unfortunately we can't call Fabric's emitItemQuads here, as there's no way to obtain a RenderContext via the
// API. Instead, we special case our FoiledModel, and just render everything else normally.
var buffer = ItemRenderer.getFoilBuffer(buffers, Sheets.translucentCullBlockSheet(), true, model instanceof FoiledModel);
for (var faceIdx = 0; faceIdx <= ModelHelper.NULL_FACE_ID; faceIdx++) {
var face = ModelHelper.faceFromIndex(faceIdx);
random.setSeed(42);
ModelRenderer.renderQuads(transform, buffer, model.getQuads(null, face, random), lightmapCoord, overlayLight, tints);
}
}
}

View File

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

View File

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

View File

@@ -41,9 +41,6 @@
"commands.computercraft.help.synopsis": "Provide help for a specific command",
"commands.computercraft.queue.desc": "Send a computer_command event to a command computer, passing through the additional arguments. This is mostly designed for map makers, acting as a more computer-friendly version of /trigger. Any player can run the command, which would most likely be done through a text component's click event.",
"commands.computercraft.queue.synopsis": "Send a computer_command event to a command computer",
"commands.computercraft.reload.desc": "Reload the ComputerCraft config file",
"commands.computercraft.reload.done": "Reloaded config",
"commands.computercraft.reload.synopsis": "Reload the ComputerCraft config file",
"commands.computercraft.shutdown.desc": "Shutdown the listed computers or all if none are specified. You can specify the computer's instance id (e.g. 123), computer id (e.g #123) or label (e.g. \"@My Computer\").",
"commands.computercraft.shutdown.done": "Shutdown %s/%s computers",
"commands.computercraft.shutdown.synopsis": "Shutdown computers remotely.",

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_advanced",
"key": {"#": {"item": "minecraft:diamond_axe"}, "T": {"item": "computercraft:turtle_advanced"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_advanced", "nbt": "{RightUpgrade:\"minecraft:diamond_axe\"}"},
"result": {
"item": "computercraft:turtle_advanced",
"nbt": "{RightUpgrade:\"minecraft:diamond_axe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_advanced",
"key": {"#": {"item": "minecraft:diamond_hoe"}, "T": {"item": "computercraft:turtle_advanced"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_advanced", "nbt": "{RightUpgrade:\"minecraft:diamond_hoe\"}"},
"result": {
"item": "computercraft:turtle_advanced",
"nbt": "{RightUpgrade:\"minecraft:diamond_hoe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_advanced",
"key": {"#": {"item": "minecraft:diamond_pickaxe"}, "T": {"item": "computercraft:turtle_advanced"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_advanced", "nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\"}"},
"result": {
"item": "computercraft:turtle_advanced",
"nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_advanced",
"key": {"#": {"item": "minecraft:diamond_shovel"}, "T": {"item": "computercraft:turtle_advanced"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_advanced", "nbt": "{RightUpgrade:\"minecraft:diamond_shovel\"}"},
"result": {
"item": "computercraft:turtle_advanced",
"nbt": "{RightUpgrade:\"minecraft:diamond_shovel\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_advanced",
"key": {"#": {"item": "minecraft:diamond_sword"}, "T": {"item": "computercraft:turtle_advanced"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_advanced", "nbt": "{RightUpgrade:\"minecraft:diamond_sword\"}"},
"result": {
"item": "computercraft:turtle_advanced",
"nbt": "{RightUpgrade:\"minecraft:diamond_sword\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_normal",
"key": {"#": {"item": "minecraft:diamond_axe"}, "T": {"item": "computercraft:turtle_normal"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_normal", "nbt": "{RightUpgrade:\"minecraft:diamond_axe\"}"},
"result": {
"item": "computercraft:turtle_normal",
"nbt": "{RightUpgrade:\"minecraft:diamond_axe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_normal",
"key": {"#": {"item": "minecraft:diamond_hoe"}, "T": {"item": "computercraft:turtle_normal"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_normal", "nbt": "{RightUpgrade:\"minecraft:diamond_hoe\"}"},
"result": {
"item": "computercraft:turtle_normal",
"nbt": "{RightUpgrade:\"minecraft:diamond_hoe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_normal",
"key": {"#": {"item": "minecraft:diamond_pickaxe"}, "T": {"item": "computercraft:turtle_normal"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_normal", "nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\"}"},
"result": {
"item": "computercraft:turtle_normal",
"nbt": "{RightUpgrade:\"minecraft:diamond_pickaxe\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_normal",
"key": {"#": {"item": "minecraft:diamond_shovel"}, "T": {"item": "computercraft:turtle_normal"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_normal", "nbt": "{RightUpgrade:\"minecraft:diamond_shovel\"}"},
"result": {
"item": "computercraft:turtle_normal",
"nbt": "{RightUpgrade:\"minecraft:diamond_shovel\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

@@ -4,6 +4,9 @@
"group": "computercraft:turtle_normal",
"key": {"#": {"item": "minecraft:diamond_sword"}, "T": {"item": "computercraft:turtle_normal"}},
"pattern": ["#T"],
"result": {"item": "computercraft:turtle_normal", "nbt": "{RightUpgrade:\"minecraft:diamond_sword\"}"},
"result": {
"item": "computercraft:turtle_normal",
"nbt": "{RightUpgrade:\"minecraft:diamond_sword\",RightUpgradeNbt:{Tag:{Damage:0}}}"
},
"show_notification": true
}

View File

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

View File

@@ -309,7 +309,7 @@ public class PlatformHelperImpl implements PlatformHelper {
public boolean hasToolUsage(ItemStack stack) {
var item = stack.getItem();
return item instanceof ShovelItem || stack.is(ItemTags.SHOVELS) ||
item instanceof HoeItem || stack.is(ItemTags.HOES);
item instanceof HoeItem || stack.is(ItemTags.HOES);
}
@Override
@@ -343,9 +343,7 @@ public class PlatformHelperImpl implements PlatformHelper {
) implements RegistryWrappers.RegistryWrapper<T> {
@Override
public int getId(T object) {
var id = registry.getId(object);
if (id == -1) throw new IllegalArgumentException(object + " was not registered in " + name);
return id;
return registry.getId(object);
}
@Override
@@ -369,10 +367,13 @@ public class PlatformHelperImpl implements PlatformHelper {
}
@Override
public T get(int id) {
var object = registry.byId(id);
if (object == null) throw new IllegalArgumentException(id + " was not registered in " + name);
return object;
public @Nullable T byId(int id) {
return registry.byId(id);
}
@Override
public int size() {
return registry.size();
}
@Override

View File

@@ -50,7 +50,7 @@
],
"depends": {
"fabricloader": ">=0.14.21",
"fabric-api": ">=0.83.0",
"fabric-api": ">=0.86.1",
"minecraft": ">=1.20.1 <1.21"
},
"accessWidener": "computercraft.accesswidener"