mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-04 23:40:00 +00:00
Merge branch 'mc-1.20.x' into mc-1.21.x
This commit is contained in:
commit
0d8ac304c7
@ -101,7 +101,7 @@ SPDX-License-Identifier = "CC0-1.0"
|
||||
path = ".github/**"
|
||||
|
||||
[[annotations]]
|
||||
path = ["gradle/wrapper/**", "gradlew", "gradlew.bat"]
|
||||
path = ["gradle/wrapper/**"]
|
||||
SPDX-FileCopyrightText = "Gradle Inc"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
|
@ -159,7 +159,7 @@ fun getNextVersion(version: String): String {
|
||||
val lastIndex = mainVersion.lastIndexOf('.')
|
||||
if (lastIndex < 0) throw IllegalArgumentException("Cannot parse version format \"$version\"")
|
||||
val lastVersion = try {
|
||||
version.substring(lastIndex + 1).toInt()
|
||||
mainVersion.substring(lastIndex + 1).toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException("Cannot parse version format \"$version\"", e)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false
|
||||
|
||||
# Mod properties
|
||||
isUnstable=true
|
||||
modVersion=1.112.0
|
||||
modVersion=1.113.0
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.21.1
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
5
gradlew
vendored
5
gradlew
vendored
@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@ -84,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
2
gradlew.bat
vendored
2
gradlew.bat
vendored
@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
|
@ -13,7 +13,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* An item detail provider for {@link ItemStack}'s whose {@link Item} has a specific type.
|
||||
* An item detail provider for {@link ItemStack}s whose {@link Item} has a specific type.
|
||||
*
|
||||
* @param <T> The type the stack's item must have.
|
||||
*/
|
||||
@ -22,7 +22,7 @@ public abstract class BasicItemDetailProvider<T> implements DetailProvider<ItemS
|
||||
private final @Nullable String namespace;
|
||||
|
||||
/**
|
||||
* Create a new item detail provider. Meta will be inserted into a new sub-map named as per {@code namespace}.
|
||||
* Create a new item detail provider. Details will be inserted into a new sub-map named as per {@code namespace}.
|
||||
*
|
||||
* @param itemType The type the stack's item must have.
|
||||
* @param namespace The namespace to use for this provider.
|
||||
@ -34,7 +34,7 @@ public abstract class BasicItemDetailProvider<T> implements DetailProvider<ItemS
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new item detail provider. Meta will be inserted directly into the results.
|
||||
* Create a new item detail provider. Details will be inserted directly into the results.
|
||||
*
|
||||
* @param itemType The type the stack's item must have.
|
||||
*/
|
||||
@ -53,21 +53,18 @@ public abstract class BasicItemDetailProvider<T> implements DetailProvider<ItemS
|
||||
* @param stack The item stack to provide details for.
|
||||
* @param item The item to provide details for.
|
||||
*/
|
||||
public abstract void provideDetails(
|
||||
Map<? super String, Object> data, ItemStack stack, T item
|
||||
);
|
||||
public abstract void provideDetails(Map<? super String, Object> data, ItemStack stack, T item);
|
||||
|
||||
@Override
|
||||
public void provideDetails(Map<? super String, Object> data, ItemStack stack) {
|
||||
public final void provideDetails(Map<? super String, Object> data, ItemStack stack) {
|
||||
var item = stack.getItem();
|
||||
if (!itemType.isInstance(item)) return;
|
||||
|
||||
// If `namespace` is specified, insert into a new data map instead of the existing one.
|
||||
Map<? super String, Object> child = namespace == null ? data : new HashMap<>();
|
||||
|
||||
provideDetails(child, stack, itemType.cast(item));
|
||||
|
||||
if (namespace != null) {
|
||||
if (namespace == null) {
|
||||
provideDetails(data, stack, itemType.cast(item));
|
||||
} else {
|
||||
Map<? super String, Object> child = new HashMap<>();
|
||||
provideDetails(child, stack, itemType.cast(item));
|
||||
data.put(namespace, child);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public interface DetailRegistry<T> {
|
||||
* @param provider The detail provider to register.
|
||||
* @see DetailProvider
|
||||
*/
|
||||
void addProvider(DetailProvider<T> provider);
|
||||
void addProvider(DetailProvider<? super T> provider);
|
||||
|
||||
/**
|
||||
* Compute basic details about an object. This is cheaper than computing all details operation, and so is suitable
|
||||
|
@ -5,7 +5,7 @@
|
||||
package dan200.computercraft.api.turtle;
|
||||
|
||||
/**
|
||||
* An enum representing the two sides of the turtle that a turtle turtle might reside.
|
||||
* An enum representing the two sides of the turtle that a turtle upgrade might reside.
|
||||
*/
|
||||
public enum TurtleSide {
|
||||
/**
|
||||
|
@ -13,6 +13,7 @@ import dan200.computercraft.api.client.turtle.RegisterTurtleUpgradeModeller;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import dan200.computercraft.client.gui.*;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.CustomLecternRenderer;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
@ -79,6 +80,7 @@ public final class ClientRegistry {
|
||||
BlockEntityRenderers.register(ModRegistry.BlockEntities.MONITOR_ADVANCED.get(), MonitorBlockEntityRenderer::new);
|
||||
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_NORMAL.get(), TurtleBlockEntityRenderer::new);
|
||||
BlockEntityRenderers.register(ModRegistry.BlockEntities.TURTLE_ADVANCED.get(), TurtleBlockEntityRenderer::new);
|
||||
BlockEntityRenderers.register(ModRegistry.BlockEntities.LECTERN.get(), CustomLecternRenderer::new);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,15 +6,19 @@ package dan200.computercraft.client.gui;
|
||||
|
||||
import dan200.computercraft.core.terminal.TextBuffer;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerListener;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static dan200.computercraft.client.render.PrintoutRenderer.*;
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
@ -23,41 +27,65 @@ import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMA
|
||||
*
|
||||
* @see dan200.computercraft.client.render.PrintoutRenderer
|
||||
*/
|
||||
public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
|
||||
private final boolean book;
|
||||
private final int pages;
|
||||
private final TextBuffer[] text;
|
||||
private final TextBuffer[] colours;
|
||||
private int page;
|
||||
public final class PrintoutScreen extends AbstractContainerScreen<PrintoutMenu> implements ContainerListener {
|
||||
private PrintoutInfo printout = PrintoutInfo.DEFAULT;
|
||||
private int page = 0;
|
||||
|
||||
public PrintoutScreen(HeldItemMenu container, Inventory player, Component title) {
|
||||
public PrintoutScreen(PrintoutMenu container, Inventory player, Component title) {
|
||||
super(container, player, title);
|
||||
|
||||
imageHeight = Y_SIZE;
|
||||
}
|
||||
|
||||
var printout = container.getStack().getOrDefault(ModRegistry.DataComponents.PRINTOUT.get(), PrintoutData.EMPTY);
|
||||
this.text = new TextBuffer[printout.lines().size()];
|
||||
this.colours = new TextBuffer[printout.lines().size()];
|
||||
for (var i = 0; i < this.text.length; i++) {
|
||||
var line = printout.lines().get(i);
|
||||
this.text[i] = new TextBuffer(line.text());
|
||||
this.colours[i] = new TextBuffer(line.foreground());
|
||||
}
|
||||
|
||||
private void setPrintout(ItemStack stack) {
|
||||
page = 0;
|
||||
pages = Math.max(this.text.length / PrintoutData.LINES_PER_PAGE, 1);
|
||||
book = ((PrintoutItem) container.getStack().getItem()).getType() == PrintoutItem.Type.BOOK;
|
||||
printout = PrintoutInfo.of(PrintoutData.getOrEmpty(stack), stack.is(ModRegistry.Items.PRINTED_BOOK.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
menu.addSlotListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed() {
|
||||
menu.removeSlotListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void slotChanged(AbstractContainerMenu menu, int slot, ItemStack stack) {
|
||||
if (slot == 0) setPrintout(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dataChanged(AbstractContainerMenu menu, int slot, int data) {
|
||||
if (slot == PrintoutMenu.DATA_CURRENT_PAGE) page = data;
|
||||
}
|
||||
|
||||
private void setPage(int page) {
|
||||
this.page = page;
|
||||
|
||||
var gameMode = Objects.requireNonNull(Objects.requireNonNull(minecraft).gameMode);
|
||||
gameMode.handleInventoryButtonClick(menu.containerId, PrintoutMenu.PAGE_BUTTON_OFFSET + page);
|
||||
}
|
||||
|
||||
private void previousPage() {
|
||||
if (page > 0) setPage(page - 1);
|
||||
}
|
||||
|
||||
private void nextPage() {
|
||||
if (page < printout.pages() - 1) setPage(page + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int key, int scancode, int modifiers) {
|
||||
if (key == GLFW.GLFW_KEY_RIGHT) {
|
||||
if (page < pages - 1) page++;
|
||||
nextPage();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key == GLFW.GLFW_KEY_LEFT) {
|
||||
if (page > 0) page--;
|
||||
previousPage();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -69,13 +97,13 @@ public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
|
||||
if (super.mouseScrolled(x, y, deltaX, deltaY)) return true;
|
||||
if (deltaY < 0) {
|
||||
// Scroll up goes to the next page
|
||||
if (page < pages - 1) page++;
|
||||
nextPage();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deltaY > 0) {
|
||||
// Scroll down goes to the previous page
|
||||
if (page > 0) page--;
|
||||
previousPage();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -88,8 +116,8 @@ public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
|
||||
graphics.pose().pushPose();
|
||||
graphics.pose().translate(0, 0, 1);
|
||||
|
||||
drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, pages, book, FULL_BRIGHT_LIGHTMAP);
|
||||
drawText(graphics.pose(), graphics.bufferSource(), leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, text, colours);
|
||||
drawBorder(graphics.pose(), graphics.bufferSource(), leftPos, topPos, 0, page, printout.pages(), printout.book(), FULL_BRIGHT_LIGHTMAP);
|
||||
drawText(graphics.pose(), graphics.bufferSource(), leftPos + X_TEXT_MARGIN, topPos + Y_TEXT_MARGIN, PrintoutData.LINES_PER_PAGE * page, FULL_BRIGHT_LIGHTMAP, printout.text(), printout.colour());
|
||||
|
||||
graphics.pose().popPose();
|
||||
}
|
||||
@ -98,4 +126,21 @@ public class PrintoutScreen extends AbstractContainerScreen<HeldItemMenu> {
|
||||
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
|
||||
// Skip rendering labels.
|
||||
}
|
||||
|
||||
record PrintoutInfo(int pages, boolean book, TextBuffer[] text, TextBuffer[] colour) {
|
||||
public static final PrintoutInfo DEFAULT = of(PrintoutData.EMPTY, false);
|
||||
|
||||
public static PrintoutInfo of(PrintoutData printout, boolean book) {
|
||||
var text = new TextBuffer[printout.lines().size()];
|
||||
var colours = new TextBuffer[printout.lines().size()];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
var line = printout.lines().get(i);
|
||||
text[i] = new TextBuffer(line.text());
|
||||
colours[i] = new TextBuffer(line.foreground());
|
||||
}
|
||||
|
||||
var pages = Math.max(text.length / PrintoutData.LINES_PER_PAGE, 1);
|
||||
return new PrintoutInfo(pages, book, text, colours);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,117 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.model;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.render.CustomLecternRenderer;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.InventoryMenu;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A model for {@linkplain PrintoutItem printouts} placed on a lectern.
|
||||
* <p>
|
||||
* This provides two models, {@linkplain #renderPages(PoseStack, VertexConsumer, int, int, int) one for a variable
|
||||
* number of pages}, and {@linkplain #renderBook(PoseStack, VertexConsumer, int, int) one for books}.
|
||||
*
|
||||
* @see CustomLecternRenderer
|
||||
*/
|
||||
public class LecternPrintoutModel {
|
||||
public static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/printout");
|
||||
public static final Material MATERIAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE);
|
||||
|
||||
private static final int TEXTURE_WIDTH = 32;
|
||||
private static final int TEXTURE_HEIGHT = 32;
|
||||
|
||||
private static final String PAGE_1 = "page_1";
|
||||
private static final String PAGE_2 = "page_2";
|
||||
private static final String PAGE_3 = "page_3";
|
||||
private static final List<String> PAGES = List.of(PAGE_1, PAGE_2, PAGE_3);
|
||||
|
||||
private final ModelPart pagesRoot;
|
||||
private final ModelPart bookRoot;
|
||||
private final ModelPart[] pages;
|
||||
|
||||
public LecternPrintoutModel() {
|
||||
pagesRoot = buildPages();
|
||||
bookRoot = buildBook();
|
||||
pages = PAGES.stream().map(pagesRoot::getChild).toArray(ModelPart[]::new);
|
||||
}
|
||||
|
||||
private static ModelPart buildPages() {
|
||||
var mesh = new MeshDefinition();
|
||||
var parts = mesh.getRoot();
|
||||
parts.addOrReplaceChild(
|
||||
PAGE_1,
|
||||
CubeListBuilder.create().texOffs(0, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
|
||||
PartPose.ZERO
|
||||
);
|
||||
|
||||
parts.addOrReplaceChild(
|
||||
PAGE_2,
|
||||
CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
|
||||
PartPose.offsetAndRotation(-0.125f, 0, 1.5f, (float) Math.PI * (1f / 16), 0, 0)
|
||||
);
|
||||
parts.addOrReplaceChild(
|
||||
PAGE_3,
|
||||
CubeListBuilder.create().texOffs(12, 0).addBox(-0.005f, -4.0f, -2.5f, 1f, 8.0f, 5.0f),
|
||||
PartPose.offsetAndRotation(-0.25f, 0, -1.5f, (float) -Math.PI * (2f / 16), 0, 0)
|
||||
);
|
||||
|
||||
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
}
|
||||
|
||||
private static ModelPart buildBook() {
|
||||
var mesh = new MeshDefinition();
|
||||
var parts = mesh.getRoot();
|
||||
|
||||
parts.addOrReplaceChild(
|
||||
"spine",
|
||||
CubeListBuilder.create().texOffs(12, 15).addBox(-0.005f, -5.0f, -0.5f, 0, 10, 1.0f),
|
||||
PartPose.ZERO
|
||||
);
|
||||
|
||||
var angle = (float) Math.toRadians(5);
|
||||
parts.addOrReplaceChild(
|
||||
"left",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(0, 10).addBox(0, -5.0f, -6.0f, 0, 10, 6.0f)
|
||||
.texOffs(0, 0).addBox(0.005f, -4.0f, -5.0f, 1.0f, 8.0f, 5.0f),
|
||||
PartPose.offsetAndRotation(-0.005f, 0, -0.5f, 0, -angle, 0)
|
||||
);
|
||||
|
||||
parts.addOrReplaceChild(
|
||||
"right",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(14, 10).addBox(0, -5.0f, 0, 0, 10, 6.0f)
|
||||
.texOffs(0, 0).addBox(0.005f, -4.0f, 0, 1.0f, 8.0f, 5.0f),
|
||||
PartPose.offsetAndRotation(-0.005f, 0, 0.5f, 0, angle, 0)
|
||||
);
|
||||
|
||||
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
}
|
||||
|
||||
public void renderBook(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay) {
|
||||
bookRoot.render(poseStack, buffer, packedLight, packedOverlay);
|
||||
}
|
||||
|
||||
public void renderPages(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, int pageCount) {
|
||||
if (pageCount > pages.length) pageCount = pages.length;
|
||||
var i = 0;
|
||||
for (; i < pageCount; i++) pages[i].visible = true;
|
||||
for (; i < pages.length; i++) pages[i].visible = false;
|
||||
|
||||
pagesRoot.render(poseStack, buffer, packedLight, packedOverlay);
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.LecternRenderer;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
|
||||
/**
|
||||
* A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
|
||||
* <p>
|
||||
* This largely follows {@link LecternRenderer}, but with support for multiple types of item.
|
||||
*/
|
||||
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
|
||||
private final LecternPrintoutModel printoutModel;
|
||||
|
||||
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
|
||||
printoutModel = new LecternPrintoutModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(CustomLecternBlockEntity lectern, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5f, 1.0625f, 0.5f);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-lectern.getBlockState().getValue(LecternBlock.FACING).getClockWise().toYRot()));
|
||||
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5f));
|
||||
poseStack.translate(0, -0.125f, 0);
|
||||
|
||||
var item = lectern.getItem();
|
||||
if (item.getItem() instanceof PrintoutItem printout) {
|
||||
var vertexConsumer = LecternPrintoutModel.MATERIAL.buffer(buffer, RenderType::entitySolid);
|
||||
if (printout.getType() == PrintoutItem.Type.BOOK) {
|
||||
printoutModel.renderBook(poseStack, vertexConsumer, packedLight, packedOverlay);
|
||||
} else {
|
||||
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutData.getOrEmpty(item).pages());
|
||||
}
|
||||
}
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.data.client;
|
||||
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.data.DataProviders;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
|
||||
@ -33,7 +34,8 @@ public final class ClientDataProviders {
|
||||
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
|
||||
out.accept(ResourceLocation.withDefaultNamespace("blocks"), List.of(
|
||||
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
|
||||
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
|
||||
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()),
|
||||
new SingleFile(LecternPrintoutModel.TEXTURE, Optional.empty())
|
||||
));
|
||||
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
|
||||
// Buttons
|
||||
|
8
projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json
generated
Normal file
8
projects/common/src/generated/resources/assets/computercraft/blockstates/lectern.json
generated
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=east": {"model": "minecraft:block/lectern", "y": 90},
|
||||
"facing=north": {"model": "minecraft:block/lectern", "y": 0},
|
||||
"facing=south": {"model": "minecraft:block/lectern", "y": 180},
|
||||
"facing=west": {"model": "minecraft:block/lectern", "y": 270}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"sources": [
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"}
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/printout"}
|
||||
]
|
||||
}
|
||||
|
12
projects/common/src/generated/resources/data/computercraft/loot_table/blocks/lectern.json
generated
Normal file
12
projects/common/src/generated/resources/data/computercraft/loot_table/blocks/lectern.json
generated
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"bonus_rolls": 0.0,
|
||||
"conditions": [{"condition": "minecraft:survives_explosion"}],
|
||||
"entries": [{"type": "minecraft:item", "name": "minecraft:lectern"}],
|
||||
"rolls": 1.0
|
||||
}
|
||||
],
|
||||
"random_sequence": "computercraft:blocks/lectern"
|
||||
}
|
@ -23,6 +23,7 @@ import net.minecraft.data.models.BlockModelGenerators;
|
||||
import net.minecraft.data.models.blockstates.*;
|
||||
import net.minecraft.data.models.model.*;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
@ -100,6 +101,11 @@ class BlockModelProvider {
|
||||
registerTurtleUpgrade(generators, "block/turtle_speaker", "block/turtle_speaker_face");
|
||||
registerTurtleModem(generators, "block/turtle_modem_normal", "block/wireless_modem_normal_face");
|
||||
registerTurtleModem(generators, "block/turtle_modem_advanced", "block/wireless_modem_advanced_face");
|
||||
|
||||
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(
|
||||
ModRegistry.Blocks.LECTERN.get(),
|
||||
Variant.variant().with(VariantProperties.MODEL, ModelLocationUtils.getModelLocation(Blocks.LECTERN))
|
||||
).with(createHorizontalFacingDispatch()));
|
||||
}
|
||||
|
||||
private static void registerDiskDrive(BlockModelGenerators generators) {
|
||||
|
@ -288,7 +288,9 @@ public final class LanguageProvider implements DataProvider {
|
||||
return Stream.of(
|
||||
BuiltInRegistries.BLOCK.holders()
|
||||
.filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
|
||||
.map(x -> x.value().getDescriptionId()),
|
||||
.map(x -> x.value().getDescriptionId())
|
||||
// Exclude blocks that just reuse vanilla translations, such as the lectern.
|
||||
.filter(x -> !x.startsWith("block.minecraft.")),
|
||||
BuiltInRegistries.ITEM.holders()
|
||||
.filter(x -> x.key().location().getNamespace().equals(ComputerCraftAPI.MOD_ID))
|
||||
.map(x -> x.value().getDescriptionId()),
|
||||
|
@ -14,6 +14,7 @@ import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
|
||||
import net.minecraft.advancements.critereon.StatePropertiesPredicate;
|
||||
import net.minecraft.data.loot.LootTableProvider.SubProviderEntry;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.storage.loot.LootPool;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
@ -56,6 +57,8 @@ class LootTableProvider {
|
||||
computerDrop(add, ModRegistry.Blocks.TURTLE_NORMAL);
|
||||
computerDrop(add, ModRegistry.Blocks.TURTLE_ADVANCED);
|
||||
|
||||
blockDrop(add, ModRegistry.Blocks.LECTERN, LootItem.lootTableItem(Items.LECTERN), ExplosionCondition.survivesExplosion());
|
||||
|
||||
add.accept(ModRegistry.Blocks.CABLE.get().getLootTable(), LootTable
|
||||
.lootTable()
|
||||
.withPool(LootPool.lootPool()
|
||||
|
@ -5,9 +5,9 @@
|
||||
package dan200.computercraft.data;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.shared.util.RegistryHelper;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.integration.ExternalModTags;
|
||||
import dan200.computercraft.shared.util.RegistryHelper;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.data.tags.ItemTagsProvider;
|
||||
import net.minecraft.data.tags.TagsProvider;
|
||||
@ -107,6 +107,7 @@ class TagProvider {
|
||||
ModRegistry.Items.MONITOR_ADVANCED.get()
|
||||
);
|
||||
|
||||
// Allow printed books to be placed in bookshelves.
|
||||
tags.tag(ItemTags.BOOKSHELF_BOOKS).add(ModRegistry.Items.PRINTED_BOOK.get());
|
||||
|
||||
tags.tag(ComputerCraftTags.Items.TURTLE_CAN_PLACE)
|
||||
|
@ -15,7 +15,7 @@ import java.util.*;
|
||||
* @param <T> The type of object that this registry provides details for.
|
||||
*/
|
||||
public class DetailRegistryImpl<T> implements DetailRegistry<T> {
|
||||
private final Collection<DetailProvider<T>> providers = new ArrayList<>();
|
||||
private final Collection<DetailProvider<? super T>> providers = new ArrayList<>();
|
||||
private final DetailProvider<T> basic;
|
||||
|
||||
public DetailRegistryImpl(DetailProvider<T> basic) {
|
||||
@ -24,7 +24,7 @@ public class DetailRegistryImpl<T> implements DetailRegistry<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addProvider(DetailProvider<T> provider) {
|
||||
public synchronized void addProvider(DetailProvider<? super T> provider) {
|
||||
Objects.requireNonNull(provider, "provider cannot be null");
|
||||
if (!providers.contains(provider)) providers.add(provider);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.shared.computer.core.ResourceMount;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.metrics.ComputerMBean;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlock;
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorWatcher;
|
||||
import dan200.computercraft.shared.util.DropConsumer;
|
||||
import dan200.computercraft.shared.util.TickScheduler;
|
||||
@ -20,16 +21,23 @@ import net.minecraft.server.dedicated.DedicatedServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.CreativeModeTabs;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
|
||||
import net.minecraft.world.level.storage.loot.LootPool;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.entries.NestedLootTable;
|
||||
import net.minecraft.world.level.storage.loot.providers.number.ConstantValue;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Set;
|
||||
@ -92,6 +100,20 @@ public final class CommonHooks {
|
||||
TickScheduler.onChunkTicketChanged(level, chunkPos, oldLevel, newLevel);
|
||||
}
|
||||
|
||||
public static InteractionResult onUseBlock(Player player, Level level, InteractionHand hand, BlockHitResult hitResult) {
|
||||
if (player.isSpectator()) return InteractionResult.PASS;
|
||||
|
||||
var pos = hitResult.getBlockPos();
|
||||
var heldItem = player.getItemInHand(hand);
|
||||
var blockState = level.getBlockState(pos);
|
||||
|
||||
if (blockState.is(Blocks.LECTERN) && !blockState.getValue(LecternBlock.HAS_BOOK)) {
|
||||
return CustomLecternBlock.tryPlaceItem(player, level, pos, blockState, heldItem);
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
public static final ResourceKey<LootTable> TREASURE_DISK_LOOT = ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "treasure_disk"));
|
||||
|
||||
private static final Set<ResourceKey<LootTable>> TREASURE_DISK_LOOT_TABLES = Set.of(
|
||||
|
@ -26,7 +26,6 @@ import dan200.computercraft.shared.command.arguments.TrackingFieldArgumentType;
|
||||
import dan200.computercraft.shared.common.ClearColourRecipe;
|
||||
import dan200.computercraft.shared.common.ColourableRecipe;
|
||||
import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.computer.apis.CommandAPI;
|
||||
import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||
@ -44,12 +43,14 @@ import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
import dan200.computercraft.shared.details.ItemDetails;
|
||||
import dan200.computercraft.shared.integration.PermissionRegistry;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlock;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import dan200.computercraft.shared.media.items.*;
|
||||
import dan200.computercraft.shared.media.recipes.DiskRecipe;
|
||||
import dan200.computercraft.shared.media.recipes.PrintoutRecipe;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import dan200.computercraft.shared.network.container.ContainerData;
|
||||
import dan200.computercraft.shared.network.container.HeldItemContainerData;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlock;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveMenu;
|
||||
@ -114,9 +115,11 @@ import net.minecraft.world.item.crafting.Recipe;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.SoundType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
@ -185,6 +188,10 @@ public final class ModRegistry {
|
||||
public static final RegistryEntry<WiredModemFullBlock> WIRED_MODEM_FULL = REGISTRY.register("wired_modem_full",
|
||||
() -> new WiredModemFullBlock(modemProperties().mapColor(MapColor.STONE)));
|
||||
public static final RegistryEntry<CableBlock> CABLE = REGISTRY.register("cable", () -> new CableBlock(modemProperties().mapColor(MapColor.STONE)));
|
||||
|
||||
public static final RegistryEntry<CustomLecternBlock> LECTERN = REGISTRY.register("lectern", () -> new CustomLecternBlock(
|
||||
BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).instrument(NoteBlockInstrument.BASS).strength(2.5F).sound(SoundType.WOOD).ignitedByLava()
|
||||
));
|
||||
}
|
||||
|
||||
public static class BlockEntities {
|
||||
@ -226,6 +233,8 @@ public final class ModRegistry {
|
||||
ofBlock(Blocks.WIRELESS_MODEM_NORMAL, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_NORMAL.get(), p, s, false));
|
||||
public static final RegistryEntry<BlockEntityType<WirelessModemBlockEntity>> WIRELESS_MODEM_ADVANCED =
|
||||
ofBlock(Blocks.WIRELESS_MODEM_ADVANCED, (p, s) -> new WirelessModemBlockEntity(BlockEntities.WIRELESS_MODEM_ADVANCED.get(), p, s, true));
|
||||
|
||||
public static final RegistryEntry<BlockEntityType<CustomLecternBlockEntity>> LECTERN = ofBlock(Blocks.LECTERN, CustomLecternBlockEntity::new);
|
||||
}
|
||||
|
||||
public static final class Items {
|
||||
@ -429,11 +438,8 @@ public final class ModRegistry {
|
||||
public static final RegistryEntry<MenuType<PrinterMenu>> PRINTER = REGISTRY.register("printer",
|
||||
() -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET));
|
||||
|
||||
public static final RegistryEntry<MenuType<HeldItemMenu>> PRINTOUT = REGISTRY.register("printout",
|
||||
() -> ContainerData.toType(
|
||||
HeldItemContainerData.STREAM_CODEC,
|
||||
(id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.hand())
|
||||
));
|
||||
public static final RegistryEntry<MenuType<PrintoutMenu>> PRINTOUT = REGISTRY.register("printout",
|
||||
() -> new MenuType<>((i, c) -> PrintoutMenu.createRemote(i), FeatureFlags.VANILLA_SET));
|
||||
}
|
||||
|
||||
static class ArgumentTypes {
|
||||
|
@ -63,7 +63,7 @@ public class TableBuilder {
|
||||
/**
|
||||
* Get the number of columns for this table.
|
||||
* <p>
|
||||
* This will be the same as {@link #getHeaders()}'s length if it is is non-{@code null},
|
||||
* This will be the same as {@link #getHeaders()}'s length if it is non-{@code null},
|
||||
* otherwise the length of the first column.
|
||||
*
|
||||
* @return The number of columns.
|
||||
|
@ -1,68 +0,0 @@
|
||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
//
|
||||
// SPDX-License-Identifier: LicenseRef-CCPL
|
||||
|
||||
package dan200.computercraft.shared.common;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class HeldItemMenu extends AbstractContainerMenu {
|
||||
private final ItemStack stack;
|
||||
private final InteractionHand hand;
|
||||
|
||||
public HeldItemMenu(MenuType<? extends HeldItemMenu> type, int id, Player player, InteractionHand hand) {
|
||||
super(type, id);
|
||||
|
||||
this.hand = hand;
|
||||
stack = player.getItemInHand(hand).copy();
|
||||
}
|
||||
|
||||
public ItemStack getStack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack quickMoveStack(Player player, int slot) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
if (!player.isAlive()) return false;
|
||||
|
||||
var stack = player.getItemInHand(hand);
|
||||
return stack == this.stack || !stack.isEmpty() && !this.stack.isEmpty() && stack.getItem() == this.stack.getItem();
|
||||
}
|
||||
|
||||
public static class Factory implements MenuProvider {
|
||||
private final MenuType<HeldItemMenu> type;
|
||||
private final Component name;
|
||||
private final InteractionHand hand;
|
||||
|
||||
public Factory(MenuType<HeldItemMenu> type, ItemStack stack, InteractionHand hand) {
|
||||
this.type = type;
|
||||
name = stack.getHoverName();
|
||||
this.hand = hand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
|
||||
return new HeldItemMenu(type, id, player, hand);
|
||||
}
|
||||
}
|
||||
}
|
@ -111,7 +111,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
fresh = false;
|
||||
computerID = computer.getID();
|
||||
|
||||
// If the on state has changed, mark as as dirty.
|
||||
// If the on state has changed, mark as dirty.
|
||||
var newOn = computer.isOn();
|
||||
if (on != newOn) {
|
||||
on = newOn;
|
||||
|
@ -53,7 +53,9 @@ public class ItemDetails {
|
||||
data.put("itemGroups", getItemGroups(stack));
|
||||
|
||||
var lore = stack.get(DataComponents.LORE);
|
||||
if (lore != null) data.put("lore", lore.lines().stream().map(Component::getString).toList());
|
||||
if (lore != null && !lore.lines().isEmpty()) {
|
||||
data.put("lore", lore.lines().stream().map(Component::getString).toList());
|
||||
}
|
||||
|
||||
var enchants = getAllEnchants(stack);
|
||||
if (!enchants.isEmpty()) data.put("enchantments", enchants);
|
||||
|
@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.lectern;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/**
|
||||
* Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}.
|
||||
* <p>
|
||||
* Unlike the vanilla lectern, this block is never empty. If the book is removed from the lectern, it converts back to
|
||||
* its vanilla version (see {@link #clearLectern(Level, BlockPos, BlockState)}).
|
||||
*
|
||||
* @see PrintoutItem#useOn(UseOnContext) Placing books into a lectern.
|
||||
*/
|
||||
public class CustomLecternBlock extends LecternBlock {
|
||||
public CustomLecternBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(defaultBlockState().setValue(HAS_BOOK, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to place an item onto an (empty) lectern.
|
||||
*
|
||||
* @param player The player placing the item.
|
||||
* @param level The current level.
|
||||
* @param pos The position of the lectern.
|
||||
* @param blockState The current state of the lectern.
|
||||
* @param item The item to place in the custom lectern.
|
||||
* @return Whether the item was placed or not.
|
||||
*/
|
||||
public static InteractionResult tryPlaceItem(Player player, Level level, BlockPos pos, BlockState blockState, ItemStack item) {
|
||||
if (item.getItem() instanceof PrintoutItem) {
|
||||
if (!level.isClientSide) replaceLectern(player, level, pos, blockState, item);
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a vanilla lectern with a custom one.
|
||||
*
|
||||
* @param player The player placing the item.
|
||||
* @param level The current level.
|
||||
* @param pos The position of the lectern.
|
||||
* @param blockState The current state of the lectern.
|
||||
* @param item The item to place in the custom lectern.
|
||||
*/
|
||||
private static void replaceLectern(Player player, Level level, BlockPos pos, BlockState blockState, ItemStack item) {
|
||||
level.setBlockAndUpdate(pos, ModRegistry.Blocks.LECTERN.get().defaultBlockState()
|
||||
.setValue(HAS_BOOK, true)
|
||||
.setValue(FACING, blockState.getValue(FACING))
|
||||
.setValue(POWERED, blockState.getValue(POWERED)));
|
||||
|
||||
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity be) {
|
||||
be.setItem(item.consumeAndReturn(1, player));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom lectern and replace it with an empty vanilla one.
|
||||
*
|
||||
* @param level The current level.
|
||||
* @param pos The position of the lectern.
|
||||
* @param blockState The current state of the lectern.
|
||||
*/
|
||||
static void clearLectern(Level level, BlockPos pos, BlockState blockState) {
|
||||
level.setBlockAndUpdate(pos, Blocks.LECTERN.defaultBlockState()
|
||||
.setValue(HAS_BOOK, false)
|
||||
.setValue(FACING, blockState.getValue(FACING))
|
||||
.setValue(POWERED, blockState.getValue(POWERED)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state) {
|
||||
return new ItemStack(Items.LECTERN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
|
||||
// If we've no lectern, remove it.
|
||||
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern && lectern.getItem().isEmpty()) {
|
||||
clearLectern(level, pos, state);
|
||||
return;
|
||||
}
|
||||
|
||||
super.tick(state, level, pos, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
|
||||
if (state.is(newState.getBlock())) return;
|
||||
|
||||
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) {
|
||||
dropItem(level, pos, state, lectern.getItem().copy());
|
||||
}
|
||||
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
}
|
||||
|
||||
private static void dropItem(Level level, BlockPos pos, BlockState state, ItemStack stack) {
|
||||
if (stack.isEmpty()) return;
|
||||
|
||||
var direction = state.getValue(FACING);
|
||||
var dx = 0.25 * direction.getStepX();
|
||||
var dz = 0.25 * direction.getStepZ();
|
||||
var entity = new ItemEntity(level, pos.getX() + 0.5 + dx, pos.getY() + 1, pos.getZ() + 0.5 + dz, stack);
|
||||
entity.setDefaultPickUpDelay();
|
||||
level.addFreshEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescriptionId() {
|
||||
return Blocks.LECTERN.getDescriptionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomLecternBlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new CustomLecternBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos) {
|
||||
return level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern ? lectern.getRedstoneSignal() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
|
||||
if (!level.isClientSide && level.getBlockEntity(pos) instanceof CustomLecternBlockEntity lectern) {
|
||||
if (player.isSecondaryUseActive()) {
|
||||
// When shift+clicked with an empty hand, drop the item and replace with the normal lectern.
|
||||
clearLectern(level, pos, state);
|
||||
} else {
|
||||
// Otherwise open the screen.
|
||||
player.openMenu(lectern);
|
||||
}
|
||||
|
||||
player.awardStat(Stats.INTERACT_WITH_LECTERN);
|
||||
}
|
||||
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.lectern;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.container.BasicContainer;
|
||||
import dan200.computercraft.shared.container.SingleContainerData;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerData;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The block entity for our {@link CustomLecternBlock}.
|
||||
*
|
||||
* @see LecternBlockEntity
|
||||
*/
|
||||
public final class CustomLecternBlockEntity extends BlockEntity implements MenuProvider {
|
||||
private static final String NBT_ITEM = "Item";
|
||||
private static final String NBT_PAGE = "Page";
|
||||
|
||||
private ItemStack item = ItemStack.EMPTY;
|
||||
private int page, pageCount;
|
||||
|
||||
public CustomLecternBlockEntity(BlockPos pos, BlockState blockState) {
|
||||
super(ModRegistry.BlockEntities.LECTERN.get(), pos, blockState);
|
||||
}
|
||||
|
||||
public ItemStack getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
void setItem(ItemStack item) {
|
||||
this.item = item;
|
||||
itemChanged();
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
}
|
||||
|
||||
int getRedstoneSignal() {
|
||||
if (item.getItem() instanceof PrintoutItem) {
|
||||
var progress = pageCount > 1 ? (float) page / (pageCount - 1) : 1F;
|
||||
return Mth.floor(progress * 14f) + 1;
|
||||
}
|
||||
|
||||
return 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the item has changed. This sets up the state for the new item.
|
||||
*/
|
||||
private void itemChanged() {
|
||||
if (item.getItem() instanceof PrintoutItem) {
|
||||
pageCount = PrintoutData.getOrEmpty(item).pages();
|
||||
page = Mth.clamp(page, 0, pageCount - 1);
|
||||
} else {
|
||||
pageCount = page = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current page, emitting a redstone pulse if needed.
|
||||
*
|
||||
* @param page The new page.
|
||||
*/
|
||||
private void setPage(int page) {
|
||||
if (this.page == page) return;
|
||||
|
||||
this.page = page;
|
||||
setChanged();
|
||||
if (getLevel() != null) LecternBlock.signalPageChange(getLevel(), getBlockPos(), getBlockState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.loadAdditional(tag, registries);
|
||||
|
||||
item = tag.contains(NBT_ITEM, Tag.TAG_COMPOUND) ? ItemStack.parseOptional(registries, tag.getCompound(NBT_ITEM)) : ItemStack.EMPTY;
|
||||
page = tag.getInt(NBT_PAGE);
|
||||
itemChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
super.saveAdditional(tag, registries);
|
||||
|
||||
if (!item.isEmpty()) tag.put(NBT_ITEM, item.save(registries));
|
||||
if (item.getItem() instanceof PrintoutItem) tag.putInt(NBT_PAGE, page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||
return ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
|
||||
var tag = super.getUpdateTag(registries);
|
||||
tag.put(NBT_ITEM, item.save(registries));
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
|
||||
var item = getItem();
|
||||
if (item.getItem() instanceof PrintoutItem) {
|
||||
return new PrintoutMenu(
|
||||
containerId, new LecternContainer(), 0,
|
||||
p -> Container.stillValidBlockEntity(this, player, Container.DEFAULT_DISTANCE_BUFFER),
|
||||
new PrintoutContainerData()
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return getItem().getDisplayName();
|
||||
}
|
||||
|
||||
/**
|
||||
* A read-only container storing the lectern's contents.
|
||||
*/
|
||||
private final class LecternContainer implements BasicContainer {
|
||||
private final List<ItemStack> itemView = new AbstractList<>() {
|
||||
@Override
|
||||
public ItemStack get(int index) {
|
||||
if (index != 0) throw new IndexOutOfBoundsException("Inventory only has one slot");
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<ItemStack> getItems() {
|
||||
return itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChanged() {
|
||||
// Should never happen, so a no-op.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
return !isRemoved();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ContainerData} for a {@link PrintoutMenu}. This provides a read/write view of the current page.
|
||||
*/
|
||||
private final class PrintoutContainerData implements SingleContainerData {
|
||||
@Override
|
||||
public int get() {
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int index, int value) {
|
||||
if (index == 0) setPage(value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.media;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.container.InvisibleSlot;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* The menus for {@linkplain PrintoutItem printouts}.
|
||||
* <p>
|
||||
* This is a somewhat similar design to {@link LecternMenu}, which is used to read written books.
|
||||
* <p>
|
||||
* This holds a single slot (containing the printout), and a single data slot ({@linkplain #DATA_CURRENT_PAGE holding
|
||||
* the current page}). The page is set by the client by sending a {@linkplain #clickMenuButton(Player, int) button
|
||||
* press} with an index of {@link #PAGE_BUTTON_OFFSET} plus the current page.
|
||||
* <p>
|
||||
* The client-side screen uses {@linkplain ContainerListener container listeners} to subscribe to item and page changes.
|
||||
* However, listeners aren't fired on the client, so we copy {@link LecternMenu}'s hack and call
|
||||
* {@link #broadcastChanges()} whenever an item or data value are changed.
|
||||
*/
|
||||
public class PrintoutMenu extends AbstractContainerMenu {
|
||||
public static final int DATA_CURRENT_PAGE = 0;
|
||||
private static final int DATA_SIZE = 1;
|
||||
|
||||
public static final int PAGE_BUTTON_OFFSET = 100;
|
||||
|
||||
private final Predicate<Player> valid;
|
||||
private final ContainerData currentPage;
|
||||
|
||||
public PrintoutMenu(
|
||||
int containerId, Container container, int slotIdx, Predicate<Player> valid, ContainerData currentPage
|
||||
) {
|
||||
super(ModRegistry.Menus.PRINTOUT.get(), containerId);
|
||||
this.valid = valid;
|
||||
this.currentPage = currentPage;
|
||||
|
||||
addSlot(new InvisibleSlot(container, slotIdx) {
|
||||
@Override
|
||||
public void setChanged() {
|
||||
super.setChanged();
|
||||
slotsChanged(container); // Trigger listeners on the client.
|
||||
}
|
||||
});
|
||||
addDataSlots(currentPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link PrintoutMenu} for use a remote (client).
|
||||
*
|
||||
* @param containerId The current container id.
|
||||
* @return The constructed container.
|
||||
*/
|
||||
public static PrintoutMenu createRemote(int containerId) {
|
||||
return new PrintoutMenu(containerId, new SimpleContainer(1), 0, p -> true, new SimpleContainerData(DATA_SIZE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link PrintoutMenu} for the printout in the current player's hand.
|
||||
*
|
||||
* @param containerId The current container id.
|
||||
* @param player The player to open the container.
|
||||
* @param hand The hand containing the item.
|
||||
* @return The constructed container.
|
||||
*/
|
||||
public static PrintoutMenu createInHand(int containerId, Player player, InteractionHand hand) {
|
||||
var currentStack = player.getItemInHand(hand);
|
||||
var currentItem = currentStack.getItem();
|
||||
|
||||
var slot = switch (hand) {
|
||||
case MAIN_HAND -> player.getInventory().selected;
|
||||
case OFF_HAND -> Inventory.SLOT_OFFHAND;
|
||||
};
|
||||
return new PrintoutMenu(
|
||||
containerId, player.getInventory(), slot,
|
||||
p -> player.getItemInHand(hand).getItem() == currentItem, new SimpleContainerData(DATA_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack quickMoveStack(Player player, int index) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
return valid.test(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clickMenuButton(Player player, int id) {
|
||||
if (id >= PAGE_BUTTON_OFFSET) {
|
||||
var page = Mth.clamp(id - PAGE_BUTTON_OFFSET, 0, PrintoutData.getOrEmpty(getPrintout()).pages() - 1);
|
||||
setData(DATA_CURRENT_PAGE, page);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.clickMenuButton(player, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current printout.
|
||||
*
|
||||
* @return The current printout.
|
||||
*/
|
||||
public ItemStack getPrintout() {
|
||||
return getSlot(0).getItem();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current page.
|
||||
*
|
||||
* @return The current page.
|
||||
*/
|
||||
public int getPage() {
|
||||
return currentPage.get(DATA_CURRENT_PAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setData(int id, int data) {
|
||||
super.setData(id, data);
|
||||
broadcastChanges(); // Trigger listeners on the client.
|
||||
}
|
||||
}
|
@ -4,13 +4,13 @@
|
||||
|
||||
package dan200.computercraft.shared.media.items;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.network.container.HeldItemContainerData;
|
||||
import com.google.common.base.Strings;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.InteractionResultHolder;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@ -41,11 +41,13 @@ public class PrintoutItem extends Item {
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand) {
|
||||
var stack = player.getItemInHand(hand);
|
||||
if (!world.isClientSide) {
|
||||
new HeldItemContainerData(hand)
|
||||
.open(player, new HeldItemMenu.Factory(ModRegistry.Menus.PRINTOUT.get(), player.getItemInHand(hand), hand));
|
||||
var title = PrintoutData.getOrEmpty(stack).title();
|
||||
var displayTitle = Strings.isNullOrEmpty(title) ? stack.getDisplayName() : Component.literal(title);
|
||||
player.openMenu(new SimpleMenuProvider((id, playerInventory, p) -> PrintoutMenu.createInHand(id, p, hand), displayTitle));
|
||||
}
|
||||
return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), player.getItemInHand(hand));
|
||||
return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack);
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
|
@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.network.container;
|
||||
|
||||
import dan200.computercraft.shared.common.HeldItemMenu;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.network.codec.MoreStreamCodecs;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
|
||||
/**
|
||||
* Opens a printout GUI based on the currently held item.
|
||||
*
|
||||
* @param hand The hand holding this item.
|
||||
* @see HeldItemMenu
|
||||
* @see PrintoutItem
|
||||
*/
|
||||
public record HeldItemContainerData(InteractionHand hand) implements ContainerData {
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, HeldItemContainerData> STREAM_CODEC = StreamCodec.composite(
|
||||
MoreStreamCodecs.ofEnum(InteractionHand.class), HeldItemContainerData::hand,
|
||||
HeldItemContainerData::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public void toBytes(RegistryFriendlyByteBuf buf) {
|
||||
STREAM_CODEC.encode(buf, this);
|
||||
}
|
||||
}
|
@ -99,11 +99,13 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity imp
|
||||
|
||||
@Override
|
||||
public void clearRemoved() {
|
||||
super.clearRemoved();
|
||||
updateMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemoved() {
|
||||
super.setRemoved();
|
||||
if (recordPlaying) stopRecord();
|
||||
}
|
||||
|
||||
|
@ -70,10 +70,10 @@ public abstract class AbstractFluidMethods<T> implements GenericPeripheral {
|
||||
) throws LuaException;
|
||||
|
||||
/**
|
||||
* Move a fluid from a connected fluid container into this oneone.
|
||||
* Move a fluid from a connected fluid container into this one.
|
||||
* <p>
|
||||
* This allows you to pull fluid in the current fluid container from another container <em>on the same wired
|
||||
* network</em>. Both containers must attached to wired modems which are connected via a cable.
|
||||
* network</em>. Both containers must be attached to wired modems which are connected via a cable.
|
||||
*
|
||||
* @param to Container to move fluid to.
|
||||
* @param computer The current computer.
|
||||
|
@ -30,6 +30,12 @@ import java.util.Objects;
|
||||
* print("On something else")
|
||||
* end
|
||||
* }</pre>
|
||||
* <p>
|
||||
* ## Recipes
|
||||
* <div class="recipe-container">
|
||||
* <mc-recipe recipe="computercraft:pocket_computer_normal"></mc-recipe>
|
||||
* <mc-recipe recipe="computercraft:pocket_computer_advanced"></mc-recipe>
|
||||
* </div>
|
||||
*
|
||||
* @cc.module pocket
|
||||
*/
|
||||
|
@ -102,12 +102,16 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inventoryTick(ItemStack stack, Level world, Entity entity, int slotNum, boolean selected) {
|
||||
public void inventoryTick(ItemStack stack, Level world, Entity entity, int compartmentSlot, boolean selected) {
|
||||
// This (in vanilla at least) is only called for players. Don't bother to handle other entities.
|
||||
if (world.isClientSide || !(entity instanceof ServerPlayer player)) return;
|
||||
|
||||
// Find the actual slot the item exists in, aborting if it can't be found.
|
||||
var slot = InventoryUtil.getInventorySlotFromCompartment(player, compartmentSlot, stack);
|
||||
if (slot < 0) return;
|
||||
|
||||
// If we're in the inventory, create a computer and keep it alive.
|
||||
var holder = new PocketHolder.PlayerHolder(player, slotNum);
|
||||
var holder = new PocketHolder.PlayerHolder(player, slot);
|
||||
var brain = getOrCreateBrain((ServerLevel) world, holder, stack);
|
||||
brain.computer().keepAlive();
|
||||
|
||||
|
@ -47,18 +47,24 @@ import java.util.Optional;
|
||||
* <p>
|
||||
* ## Turtle upgrades
|
||||
* While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles
|
||||
* can be upgraded with *tools* and [peripherals][`peripheral`]. Turtles have two upgrade slots, one on the left and right
|
||||
* sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the [`turtle.equipLeft`]/[`turtle.equipRight`]
|
||||
* functions.
|
||||
* can be upgraded with upgrades. Turtles have two upgrade slots, one on the left and right sides. Upgrades can be
|
||||
* equipped by crafting a turtle with the upgrade, or calling the [`turtle.equipLeft`]/[`turtle.equipRight`] functions.
|
||||
* <p>
|
||||
* Turtle tools allow you to break blocks ([`turtle.dig`]) and attack entities ([`turtle.attack`]). Some tools are more
|
||||
* suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more
|
||||
* damage. Other tools have more niche use-cases, for instance hoes can til dirt.
|
||||
* By default, any diamond tool may be used as an upgrade (though more may be added with [datapacks]). The diamond
|
||||
* pickaxe may be used to break blocks (with [`turtle.dig`]), while the sword can attack entities ([`turtle.attack`]).
|
||||
* Other tools have more niche use-cases, for instance hoes can til dirt.
|
||||
* <p>
|
||||
* Peripherals (such as the [wireless modem][`modem`] or [`speaker`]) can also be equipped as upgrades. These are then
|
||||
* accessible by accessing the `"left"` or `"right"` peripheral.
|
||||
* Some peripherals (namely [speakers][`speaker`] and Ender and Wireless [modems][`modem`]) can also be equipped as
|
||||
* upgrades. These are then accessible by accessing the `"left"` or `"right"` peripheral.
|
||||
* <p>
|
||||
* ## Recipes
|
||||
* <div class="recipe-container">
|
||||
* <mc-recipe recipe="computercraft:turtle_normal"></mc-recipe>
|
||||
* <mc-recipe recipe="computercraft:turtle_advanced"></mc-recipe>
|
||||
* </div>
|
||||
* <p>
|
||||
* [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics"
|
||||
* [datapacks]: https://datapacks.madefor.cc ""
|
||||
*
|
||||
* @cc.module turtle
|
||||
* @cc.since 1.3
|
||||
@ -345,7 +351,7 @@ public class TurtleAPI implements ILuaAPI {
|
||||
* For instance, if a slot contains 13 blocks of dirt, it has room for another 51.
|
||||
*
|
||||
* @param slot The slot we wish to check. Defaults to the {@link #select selected slot}.
|
||||
* @return The space left in in this slot.
|
||||
* @return The space left in this slot.
|
||||
* @throws LuaException If the slot is out of range.
|
||||
*/
|
||||
@LuaFunction
|
||||
|
@ -129,11 +129,15 @@ public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implem
|
||||
protected final void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
|
||||
if (state.is(newState.getBlock())) return;
|
||||
|
||||
if (!level.isClientSide && level.getBlockEntity(pos) instanceof TurtleBlockEntity turtle && !turtle.hasMoved()) {
|
||||
Containers.dropContents(level, pos, turtle);
|
||||
}
|
||||
// Most blocks drop items and then remove the BE. However, if a turtle is consuming drops right now, that can
|
||||
// lead to loops where it tries to insert an item back into the inventory. To prevent this, take a reference to
|
||||
// the turtle BE now, remove it, and then drop the items.
|
||||
var turtle = !level.isClientSide && level.getBlockEntity(pos) instanceof TurtleBlockEntity t && !t.hasMoved()
|
||||
? t : null;
|
||||
|
||||
super.onRemove(state, level, pos, newState, isMoving);
|
||||
|
||||
if (turtle != null) Containers.dropContents(level, pos, turtle);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,9 +9,12 @@ import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
@ -35,6 +38,28 @@ public final class InventoryUtil {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a slot inside a player's compartment to a slot in the full player's inventory.
|
||||
* <p>
|
||||
* {@link Inventory#tick()} passes in a slot to {@link Item#inventoryTick(ItemStack, Level, Entity, int, boolean)}.
|
||||
* However, this slot corresponds to the index within the current compartment (items, armour, offhand) and not
|
||||
* the actual slot.
|
||||
* <p>
|
||||
* This method searches the relevant compartments (inventory and offhand, skipping armour) for the stack, returning
|
||||
* its slot if found.
|
||||
*
|
||||
* @param player The player holding the item.
|
||||
* @param slot The slot inside the compartment.
|
||||
* @param stack The stack being ticked.
|
||||
* @return The inventory slot, or {@code -1} if the item could not be found in the inventory.
|
||||
*/
|
||||
public static int getInventorySlotFromCompartment(Player player, int slot, ItemStack stack) {
|
||||
if (stack.isEmpty()) throw new IllegalArgumentException("Cannot search for empty stack");
|
||||
if (player.getInventory().getItem(slot) == stack) return slot;
|
||||
if (player.getInventory().getItem(Inventory.SLOT_OFFHAND) == stack) return Inventory.SLOT_OFFHAND;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static @Nullable Container getEntityContainer(ServerLevel level, BlockPos pos, Direction side) {
|
||||
var vecStart = new Vec3(
|
||||
pos.getX() + 0.5 + 0.6 * side.getStepX(),
|
||||
|
@ -109,8 +109,9 @@ public final class TickScheduler {
|
||||
return State.UNLOADED;
|
||||
} else {
|
||||
// This should be impossible: either the block entity is at the above position, or it has been removed.
|
||||
if (level.getBlockEntity(pos) != blockEntity) {
|
||||
throw new IllegalStateException("Expected " + blockEntity + " at " + pos);
|
||||
var currentBlockEntity = level.getBlockEntity(pos);
|
||||
if (currentBlockEntity != blockEntity) {
|
||||
throw new IllegalStateException("Expected " + blockEntity + " at " + pos + ", got " + currentBlockEntity);
|
||||
}
|
||||
|
||||
// Otherwise schedule a tick and remove it from the queue.
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 212 B |
@ -0,0 +1,3 @@
|
||||
SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
|
||||
SPDX-License-Identifier: MPL-2.0
|
@ -6,16 +6,11 @@ package dan200.computercraft.mixin.gametest;
|
||||
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.gametest.framework.GameTestInfo;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
@Mixin(GameTestHelper.class)
|
||||
public interface GameTestHelperAccessor {
|
||||
@Invoker
|
||||
AABB callGetBounds();
|
||||
|
||||
@Accessor
|
||||
GameTestInfo getTestInfo();
|
||||
}
|
||||
|
@ -19,9 +19,11 @@ import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.LeverBlock
|
||||
import net.minecraft.world.level.block.RedstoneLampBlock
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.lwjgl.glfw.GLFW
|
||||
@ -115,6 +117,19 @@ class Computer_Test {
|
||||
thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a computer item is dropped on explosion.
|
||||
*/
|
||||
@GameTest
|
||||
fun Drops_on_explosion(context: GameTestHelper) = context.sequence {
|
||||
thenExecute {
|
||||
val explosionPos = Vec3.atCenterOf(context.absolutePos(BlockPos(2, 2, 2)))
|
||||
context.level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 2.0f, Level.ExplosionInteraction.TNT)
|
||||
|
||||
context.assertItemEntityCountIs(ModRegistry.Items.COMPUTER_NORMAL.get(), 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the client can open the computer UI and interact with it.
|
||||
*/
|
||||
|
@ -734,6 +734,21 @@ class Turtle_Test {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a turtle can break a block that explodes, causing the turtle itself to explode.
|
||||
*
|
||||
* @see [#585](https://github.com/cc-tweaked/CC-Tweaked/issues/585).
|
||||
*/
|
||||
@GameTest
|
||||
fun Breaks_exploding_block(context: GameTestHelper) = context.sequence {
|
||||
thenOnComputer { turtle.dig(Optional.empty()) }
|
||||
thenIdle(2)
|
||||
thenExecute {
|
||||
context.assertItemEntityCountIs(ModRegistry.Items.TURTLE_NORMAL.get(), 1)
|
||||
context.assertItemEntityCountIs(Items.BONE_BLOCK, 65)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render turtles as an item.
|
||||
*/
|
||||
|
@ -22,6 +22,7 @@ import net.minecraft.world.Container
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.entity.Entity
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.item.Item
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.context.UseOnContext
|
||||
import net.minecraft.world.level.GameType
|
||||
@ -257,6 +258,16 @@ fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: Strin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to [GameTestHelper.assertItemEntityCountIs], but searching anywhere in the structure bounds.
|
||||
*/
|
||||
fun GameTestHelper.assertItemEntityCountIs(expected: Item, count: Int) {
|
||||
val actualCount = getEntities(EntityType.ITEM).sumOf { if (it.item.`is`(expected)) it.item.count else 0 }
|
||||
if (actualCount != count) {
|
||||
throw GameTestAssertException("Expected $count ${expected.description.string} items to exist (found $actualCount)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getName(type: BlockEntityType<*>): ResourceLocation =
|
||||
RegistryHelper.getKeyOrThrow(BuiltInRegistries.BLOCK_ENTITY_TYPE, type)
|
||||
|
||||
|
@ -13,8 +13,14 @@ import dan200.computercraft.shared.computer.core.ServerContext
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.gametest.framework.*
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.server.level.ServerLevel
|
||||
import net.minecraft.world.level.GameRules
|
||||
import net.minecraft.world.level.Level
|
||||
import net.minecraft.world.level.LevelAccessor
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.entity.StructureBlockEntity
|
||||
import net.minecraft.world.level.block.state.BlockState
|
||||
import net.minecraft.world.phys.Vec3
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
@ -188,4 +194,23 @@ object TestHooks {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a hook that makes breaking a bone block spawn an explosion.
|
||||
*
|
||||
* It would be more Correct to register a custom block, but that's quite a lot of work, and doesn't seem worth it
|
||||
* for test code.
|
||||
*
|
||||
* See also [Turtle_Test.Breaks_exploding_block].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun onBeforeDestroyBlock(level: LevelAccessor, pos: BlockPos, state: BlockState): Boolean {
|
||||
if (state.block === Blocks.BONE_BLOCK && level is ServerLevel) {
|
||||
val explosionPos = Vec3.atCenterOf(pos)
|
||||
level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 4.0f, Level.ExplosionInteraction.TNT)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
138
projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt
generated
Normal file
138
projects/common/src/testMod/resources/data/cctest/structures/computer_test.drops_on_explosion.snbt
generated
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
DataVersion: 3465,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [0, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 1], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 2], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 3], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [1, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "minecraft:air"},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [2, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:computer_normal{facing:east,state:off}", nbt: {On: 0b, id: "computercraft:computer_normal"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:air"},
|
||||
{pos: [2, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [3, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "minecraft:air"},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 1], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 2], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 3], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 1], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [1, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [2, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [3, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 1], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 2], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 3], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
palette: [
|
||||
"minecraft:obsidian",
|
||||
"minecraft:barrier",
|
||||
"minecraft:air",
|
||||
"computercraft:computer_normal{facing:east,state:off}"
|
||||
]
|
||||
}
|
139
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt
generated
Normal file
139
projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt
generated
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
DataVersion: 3465,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [0, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [1, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [2, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [3, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 0], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 1], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 2], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 3], state: "minecraft:obsidian"},
|
||||
{pos: [4, 0, 4], state: "minecraft:obsidian"},
|
||||
{pos: [0, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 1], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 2], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 3], state: "minecraft:barrier"},
|
||||
{pos: [0, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [1, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "minecraft:air"},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [2, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.breaks_exploding_block", LeftUpgrade: "minecraft:diamond_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0}}, On: 1b, Owner: {LowerId: -5670393268852517359L, Name: "Player172", UpperId: 3578583684139923613L}, Slot: 0, Items: [{Count: 64b, Slot: 0b, id: "minecraft:bone_block"}], id: "computercraft:turtle_normal"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:bone_block{axis:y}"},
|
||||
{pos: [2, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [3, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "minecraft:air"},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 0], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 1], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 2], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 3], state: "minecraft:barrier"},
|
||||
{pos: [4, 1, 4], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 1], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:barrier"},
|
||||
{pos: [0, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [1, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [2, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [3, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 0], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 1], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 2], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 3], state: "minecraft:barrier"},
|
||||
{pos: [4, 2, 4], state: "minecraft:barrier"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
palette: [
|
||||
"minecraft:obsidian",
|
||||
"minecraft:barrier",
|
||||
"minecraft:bone_block{axis:y}",
|
||||
"minecraft:air",
|
||||
"computercraft:turtle_normal{facing:south,waterlogged:false}"
|
||||
]
|
||||
}
|
@ -8,9 +8,7 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The result of invoking a Lua method.
|
||||
@ -55,6 +53,12 @@ public final class MethodResult {
|
||||
* <p>
|
||||
* In order to provide a custom object with methods, one may return a {@link IDynamicLuaObject}, or an arbitrary
|
||||
* class with {@link LuaFunction} annotations. Anything else will be converted to {@code nil}.
|
||||
* <p>
|
||||
* Shared objects in a {@link MethodResult} will preserve their sharing when converted to Lua values. For instance,
|
||||
* {@code Map<?, ?> m = new HashMap(); return MethodResult.of(m, m); } will return two values {@code a}, {@code b}
|
||||
* where {@code a == b}. The one exception to this is Java's singleton collections ({@link List#of()},
|
||||
* {@link Set#of()} and {@link Map#of()}), which are always converted to new table. This is not true for other
|
||||
* singleton collections, such as those provided by {@link Collections} or Guava.
|
||||
*
|
||||
* @param value The value to return to the calling Lua function.
|
||||
* @return A method result which returns immediately with the given value.
|
||||
|
@ -38,7 +38,7 @@ public interface IPeripheral {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called when when a computer is attaching to the peripheral.
|
||||
* Is called when a computer is attaching to the peripheral.
|
||||
* <p>
|
||||
* This will occur when a peripheral is placed next to an active computer, when a computer is turned on next to a
|
||||
* peripheral, when a turtle travels into a square next to a peripheral, or when a wired modem adjacent to this
|
||||
|
@ -101,7 +101,7 @@ public class FSAPI implements ILuaAPI {
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String[] list(String path) throws LuaException {
|
||||
public final List<String> list(String path) throws LuaException {
|
||||
try (var ignored = environment.time(Metrics.FS_OPS)) {
|
||||
return getFileSystem().list(path);
|
||||
} catch (FileSystemException e) {
|
||||
|
@ -122,7 +122,7 @@ public class OSAPI implements ILuaAPI {
|
||||
}
|
||||
|
||||
private static long getEpochForCalendar(Calendar c) {
|
||||
return c.getTime().getTime();
|
||||
return c.getTimeInMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,7 +298,7 @@ public class OSAPI implements ILuaAPI {
|
||||
* textutils.formatTime(os.time())
|
||||
* }</pre>
|
||||
* @cc.since 1.2
|
||||
* @cc.changed 1.80pr1 Add support for getting the local local and UTC time.
|
||||
* @cc.changed 1.80pr1 Add support for getting the local and UTC time.
|
||||
* @cc.changed 1.82.0 Arguments are now case insensitive.
|
||||
* @cc.changed 1.83.0 {@link #time(IArguments)} now accepts table arguments and converts them to UNIX timestamps.
|
||||
* @see #date To get a date table that can be converted with this function.
|
||||
|
@ -177,9 +177,9 @@ public abstract class AbstractHandle {
|
||||
/**
|
||||
* Read the remainder of the file.
|
||||
*
|
||||
* @return The file, or {@code null} if at the end of it.
|
||||
* @return The remaining contents of the file, or {@code null} in the event of an error.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error.
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
@Nullable
|
||||
|
@ -57,7 +57,7 @@ interface AddressPredicate {
|
||||
prefixSize = Integer.parseInt(prefixSizeStr);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new InvalidRuleException(String.format(
|
||||
"Invalid host host '%s': Cannot extract size of CIDR mask from '%s'.",
|
||||
"Invalid host '%s': Cannot extract size of CIDR mask from '%s'.",
|
||||
addressStr + '/' + prefixSizeStr, prefixSizeStr
|
||||
));
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ final class Generator<T> {
|
||||
}
|
||||
|
||||
// Fold over the original method's arguments, excluding the target in reverse. For each argument, we reduce
|
||||
// a method of type type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _
|
||||
// a method of type (target, args..., arg_n, context..., IArguments) -> _ to (target, args..., context..., IArguments) -> _
|
||||
// until eventually we've flattened the whole list.
|
||||
for (var i = parameterTypes.size() - 1; i >= 0; i--) {
|
||||
handle = MethodHandles.foldArguments(handle, i + 1, argSelectors.get(i));
|
||||
|
@ -122,6 +122,10 @@ public class FileSystem {
|
||||
}
|
||||
|
||||
var lastSlash = path.lastIndexOf('/');
|
||||
|
||||
// If the trailing segment is a "..", then just append another one.
|
||||
if (path.substring(lastSlash < 0 ? 0 : lastSlash + 1).equals("..")) return path + "/..";
|
||||
|
||||
if (lastSlash >= 0) {
|
||||
return path.substring(0, lastSlash);
|
||||
} else {
|
||||
@ -145,7 +149,7 @@ public class FileSystem {
|
||||
return getMount(sanitizePath(path)).getAttributes(sanitizePath(path));
|
||||
}
|
||||
|
||||
public synchronized String[] list(String path) throws FileSystemException {
|
||||
public synchronized List<String> list(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
|
||||
@ -161,10 +165,8 @@ public class FileSystem {
|
||||
}
|
||||
|
||||
// Return list
|
||||
var array = new String[list.size()];
|
||||
list.toArray(array);
|
||||
Arrays.sort(array);
|
||||
return array;
|
||||
list.sort(Comparator.naturalOrder());
|
||||
return list;
|
||||
}
|
||||
|
||||
public synchronized boolean exists(String path) throws FileSystemException {
|
||||
|
@ -13,6 +13,7 @@ import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.util.LuaUtil;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.core.util.SanitisedError;
|
||||
import org.slf4j.Logger;
|
||||
@ -183,10 +184,35 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
return ValueFactory.valueOf(bytes);
|
||||
}
|
||||
|
||||
// Don't share singleton values, and instead convert them to a new table.
|
||||
if (LuaUtil.isSingletonCollection(object)) return new LuaTable();
|
||||
|
||||
if (values == null) values = new IdentityHashMap<>(1);
|
||||
var result = values.get(object);
|
||||
if (result != null) return result;
|
||||
|
||||
var wrapped = toValueWorker(object, values);
|
||||
if (wrapped == null) {
|
||||
LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName());
|
||||
return Constants.NIL;
|
||||
}
|
||||
|
||||
values.put(object, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a complex Java object (such as a collection or Lua object) to a Lua value.
|
||||
* <p>
|
||||
* This is a worker function for {@link #toValue(Object, IdentityHashMap)}, which handles the actual construction
|
||||
* of values, without reading/writing from the value map.
|
||||
*
|
||||
* @param object The object to convert.
|
||||
* @param values The map of Java to Lua values.
|
||||
* @return The converted value, or {@code null} if it could not be converted.
|
||||
* @throws LuaError If the value could not be converted.
|
||||
*/
|
||||
private @Nullable LuaValue toValueWorker(Object object, IdentityHashMap<Object, LuaValue> values) throws LuaError {
|
||||
if (object instanceof ILuaFunction) {
|
||||
return new ResultInterpreterFunction(this, FUNCTION_METHOD, object, context, object.toString());
|
||||
}
|
||||
@ -194,15 +220,12 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
if (object instanceof IDynamicLuaObject) {
|
||||
LuaValue wrapped = wrapLuaObject(object);
|
||||
if (wrapped == null) wrapped = new LuaTable();
|
||||
values.put(object, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
if (object instanceof Map<?, ?> map) {
|
||||
var table = new LuaTable();
|
||||
values.put(object, table);
|
||||
|
||||
for (Map.Entry<?, ?> pair : map.entrySet()) {
|
||||
for (var pair : map.entrySet()) {
|
||||
var key = toValue(pair.getKey(), values);
|
||||
var value = toValue(pair.getValue(), values);
|
||||
if (!key.isNil() && !value.isNil()) table.rawset(key, value);
|
||||
@ -212,27 +235,18 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
|
||||
if (object instanceof Collection<?> objects) {
|
||||
var table = new LuaTable(objects.size(), 0);
|
||||
values.put(object, table);
|
||||
var i = 0;
|
||||
for (Object child : objects) table.rawset(++i, toValue(child, values));
|
||||
for (var child : objects) table.rawset(++i, toValue(child, values));
|
||||
return table;
|
||||
}
|
||||
|
||||
if (object instanceof Object[] objects) {
|
||||
var table = new LuaTable(objects.length, 0);
|
||||
values.put(object, table);
|
||||
for (var i = 0; i < objects.length; i++) table.rawset(i + 1, toValue(objects[i], values));
|
||||
return table;
|
||||
}
|
||||
|
||||
var wrapped = wrapLuaObject(object);
|
||||
if (wrapped != null) {
|
||||
values.put(object, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName());
|
||||
return Constants.NIL;
|
||||
return wrapLuaObject(object);
|
||||
}
|
||||
|
||||
Varargs toValues(@Nullable Object[] objects) throws LuaError {
|
||||
|
@ -4,9 +4,18 @@
|
||||
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class LuaUtil {
|
||||
private static final List<?> EMPTY_LIST = List.of();
|
||||
private static final Set<?> EMPTY_SET = Set.of();
|
||||
private static final Map<?, ?> EMPTY_MAP = Map.of();
|
||||
|
||||
public static Object[] consArray(Object value, Collection<?> rest) {
|
||||
if (rest.isEmpty()) return new Object[]{ value };
|
||||
|
||||
@ -14,7 +23,20 @@ public class LuaUtil {
|
||||
var out = new Object[rest.size() + 1];
|
||||
out[0] = value;
|
||||
var i = 1;
|
||||
for (Object additionalType : rest) out[i++] = additionalType;
|
||||
for (var additionalType : rest) out[i++] = additionalType;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a value is a singleton collection, such as one created with {@link List#of()}.
|
||||
* <p>
|
||||
* These collections are treated specially by {@link ILuaMachine} implementations: we skip sharing for them, and
|
||||
* create a new table each time.
|
||||
*
|
||||
* @param value The value to test.
|
||||
* @return Whether this is a singleton collection.
|
||||
*/
|
||||
public static boolean isSingletonCollection(Object value) {
|
||||
return value == EMPTY_LIST || value == EMPTY_SET || value == EMPTY_MAP;
|
||||
}
|
||||
}
|
||||
|
@ -851,13 +851,32 @@ unserialise = unserialize -- GB version
|
||||
|
||||
--[[- Returns a JSON representation of the given data.
|
||||
|
||||
This function attempts to guess whether a table is a JSON array or
|
||||
object. However, empty tables are assumed to be empty objects - use
|
||||
[`textutils.empty_json_array`] to mark an empty array.
|
||||
|
||||
This is largely intended for interacting with various functions from the
|
||||
[`commands`] API, though may also be used in making [`http`] requests.
|
||||
|
||||
Lua has a rather different data model to Javascript/JSON. As a result, some Lua
|
||||
values do not serialise cleanly into JSON.
|
||||
|
||||
- Lua tables can contain arbitrary key-value pairs, but JSON only accepts arrays,
|
||||
and objects (which require a string key). When serialising a table, if it only
|
||||
has numeric keys, then it will be treated as an array. Otherwise, the table will
|
||||
be serialised to an object using the string keys. Non-string keys (such as numbers
|
||||
or tables) will be dropped.
|
||||
|
||||
A consequence of this is that an empty table will always be serialised to an object,
|
||||
not an array. [`textutils.empty_json_array`] may be used to express an empty array.
|
||||
|
||||
- Lua strings are an a sequence of raw bytes, and do not have any specific encoding.
|
||||
However, JSON strings must be valid unicode. By default, non-ASCII characters in a
|
||||
string are serialised to their unicode code point (for instance, `"\xfe"` is
|
||||
converted to `"\u00fe"`). The `unicode_strings` option may be set to treat all input
|
||||
strings as UTF-8.
|
||||
|
||||
- Lua does not distinguish between missing keys (`undefined` in JS) and ones explicitly
|
||||
set to `null`. As a result `{ x = nil }` is serialised to `{}`. [`textutils.json_null`]
|
||||
may be used to get an explicit null value (`{ x = textutils.json_null }` will serialise
|
||||
to `{"x": null}`).
|
||||
|
||||
@param[1] t The value to serialise. Like [`textutils.serialise`], this should not
|
||||
contain recursive tables or functions.
|
||||
@tparam[1,opt] {
|
||||
|
@ -114,7 +114,7 @@ local vector = {
|
||||
--
|
||||
-- @tparam Vector self The first vector to compute the dot product of.
|
||||
-- @tparam Vector o The second vector to compute the dot product of.
|
||||
-- @treturn Vector The dot product of `self` and `o`.
|
||||
-- @treturn number The dot product of `self` and `o`.
|
||||
-- @usage v1:dot(v2)
|
||||
dot = function(self, o)
|
||||
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
|
||||
|
@ -1,3 +1,15 @@
|
||||
# New features in CC: Tweaked 1.113.0
|
||||
|
||||
* Allow placing printed pages and books in lecterns.
|
||||
|
||||
Several bug fixes:
|
||||
* Various documentation fixes (MCJack123)
|
||||
* Fix computers and turtles not being dropped when exploded with TNT.
|
||||
* Fix crash when turtles are broken while mining a block.
|
||||
* Fix pocket computer terminals not updating when in the off-hand.
|
||||
* Fix disk drives not being exposed as a peripheral.
|
||||
* Fix item details being non-serialisable due to duplicated tables.
|
||||
|
||||
# New features in CC: Tweaked 1.112.0
|
||||
|
||||
* Report a custom error when using `!` instead of `not`.
|
||||
@ -811,7 +823,7 @@ And several bug fixes:
|
||||
# New features in CC: Tweaked 1.86.2
|
||||
|
||||
* Fix peripheral.getMethods returning an empty table.
|
||||
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable.
|
||||
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing features and may be unstable.
|
||||
|
||||
# New features in CC: Tweaked 1.86.1
|
||||
|
||||
@ -1427,7 +1439,7 @@ And several bug fixes:
|
||||
* Turtles can now compare items in their inventories
|
||||
* Turtles can place signs with text on them with `turtle.place( [signText] )`
|
||||
* Turtles now optionally require fuel items to move, and can refuel themselves
|
||||
* The size of the the turtle inventory has been increased to 16
|
||||
* The size of the turtle inventory has been increased to 16
|
||||
* The size of the turtle screen has been increased
|
||||
* New turtle functions: `turtle.compareTo( [slotNum] )`, `turtle.craft()`, `turtle.attack()`, `turtle.attackUp()`, `turtle.attackDown()`, `turtle.dropUp()`, `turtle.dropDown()`, `turtle.getFuelLevel()`, `turtle.refuel()`
|
||||
* New disk function: disk.getID()
|
||||
|
@ -1,14 +1,13 @@
|
||||
New features in CC: Tweaked 1.112.0
|
||||
New features in CC: Tweaked 1.113.0
|
||||
|
||||
* Report a custom error when using `!` instead of `not`.
|
||||
* Update several translations (zyxkad, MineKID-LP).
|
||||
* Add `cc.strings.split` function.
|
||||
* Allow placing printed pages and books in lecterns.
|
||||
|
||||
Several bug fixes:
|
||||
* Fix `drive.getAudioTitle` returning `nil` when no disk is inserted.
|
||||
* Preserve item data when upgrading pocket computers.
|
||||
* Add missing bounds check to `cc.strings.wrap` (Lupus950).
|
||||
* Fix dyed turtles rendering transparent.
|
||||
* Fix dupe bug when crafting with turtles.
|
||||
* Various documentation fixes (MCJack123)
|
||||
* Fix computers and turtles not being dropped when exploded with TNT.
|
||||
* Fix crash when turtles are broken while mining a block.
|
||||
* Fix pocket computer terminals not updating when in the off-hand.
|
||||
* Fix disk drives not being exposed as a peripheral.
|
||||
* Fix item details being non-serialisable due to duplicated tables.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
@ -13,7 +13,7 @@ Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web
|
||||
and converted a format suitable for [`speaker.playAudio`].
|
||||
|
||||
## Encoding and decoding files
|
||||
This modules exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder.
|
||||
This module exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder.
|
||||
The returned encoder/decoder is itself a function, which converts between the two kinds of data.
|
||||
|
||||
These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for
|
||||
@ -21,9 +21,9 @@ a specific audio stream. Typically you will want to create a decoder for each st
|
||||
for each one you write.
|
||||
|
||||
## Converting audio to DFPWM
|
||||
DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it.
|
||||
DFPWM is not a popular file format and so standard audio processing tools may not have an option to export to it.
|
||||
Instead, you can convert audio files online using [music.madefor.cc], the [LionRay Wav Converter][LionRay] Java
|
||||
application or development builds of [FFmpeg].
|
||||
application or [FFmpeg] 5.1 or later.
|
||||
|
||||
[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked"
|
||||
[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter "
|
||||
@ -211,7 +211,7 @@ end
|
||||
|
||||
--[[- A convenience function for encoding a complete file of audio at once.
|
||||
|
||||
This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place,
|
||||
This should only be used for complete pieces of audio. If you are writing multiple chunks to the same place,
|
||||
you should use an encoder returned by [`make_encoder`] instead.
|
||||
|
||||
@tparam { number... } input The table of amplitude data.
|
||||
|
@ -5,11 +5,14 @@
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.time.Duration.ofSeconds;
|
||||
@ -30,6 +33,26 @@ public class ComputerTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateObjects() {
|
||||
class CustomApi implements ILuaAPI {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "custom" };
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] getObjects() {
|
||||
return new Object[]{ List.of(), List.of() };
|
||||
}
|
||||
}
|
||||
|
||||
ComputerBootstrap.run("""
|
||||
local x, y = custom.getObjects()
|
||||
assert(x ~= y)
|
||||
""", i -> i.addApi(new CustomApi()), 50);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
var stream = ComputerTest.class.getClassLoader().getResourceAsStream("benchmark.lua");
|
||||
try (var reader = new InputStreamReader(Objects.requireNonNull(stream), StandardCharsets.UTF_8)) {
|
||||
|
@ -158,6 +158,65 @@ describe("The fs library", function()
|
||||
expect(fs.combine("", "a")):eq("a")
|
||||
expect(fs.combine("a", "", "b", "c")):eq("a/b/c")
|
||||
end)
|
||||
|
||||
it("preserves pattern characters", function()
|
||||
expect(fs.combine("foo*?")):eq("foo*?")
|
||||
end)
|
||||
|
||||
it("sanitises paths", function()
|
||||
expect(fs.combine("foo\":<>|")):eq("foo")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("fs.getName", function()
|
||||
it("returns 'root' for the empty path", function()
|
||||
expect(fs.getName("")):eq("root")
|
||||
expect(fs.getName("foo/..")):eq("root")
|
||||
end)
|
||||
|
||||
it("returns the file name", function()
|
||||
expect(fs.getName("foo/bar")):eq("bar")
|
||||
expect(fs.getName("foo/bar/")):eq("bar")
|
||||
expect(fs.getName("../foo")):eq("foo")
|
||||
end)
|
||||
|
||||
it("returns '..' for parent directories", function()
|
||||
expect(fs.getName("..")):eq("..")
|
||||
end)
|
||||
|
||||
it("preserves pattern characters", function()
|
||||
expect(fs.getName("foo*?")):eq("foo*?")
|
||||
end)
|
||||
|
||||
it("sanitises paths", function()
|
||||
expect(fs.getName("foo\":<>|")):eq("foo")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("fs.getDir", function()
|
||||
it("returns '..' for the empty path", function()
|
||||
expect(fs.getDir("")):eq("..")
|
||||
expect(fs.getDir("foo/..")):eq("..")
|
||||
end)
|
||||
|
||||
it("returns the directory name", function()
|
||||
expect(fs.getDir("foo/bar")):eq("foo")
|
||||
expect(fs.getDir("foo/bar/")):eq("foo")
|
||||
expect(fs.getDir("../foo")):eq("..")
|
||||
end)
|
||||
|
||||
it("returns '..' for parent directories", function()
|
||||
expect(fs.getDir("..")):eq("../..")
|
||||
expect(fs.getDir("../..")):eq("../../..")
|
||||
end)
|
||||
|
||||
it("preserves pattern characters", function()
|
||||
expect(fs.getDir("foo*?/x")):eq("foo*?")
|
||||
end)
|
||||
|
||||
it("sanitises paths", function()
|
||||
expect(fs.getDir("foo\":<>|/x")):eq("foo")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("fs.getSize", function()
|
||||
@ -200,6 +259,14 @@ describe("The fs library", function()
|
||||
handle.close()
|
||||
end)
|
||||
|
||||
it("reading an empty file returns nil", function()
|
||||
local file = create_test_file ""
|
||||
|
||||
local handle = fs.open(file, mode)
|
||||
expect(handle.read()):eq(nil)
|
||||
handle.close()
|
||||
end)
|
||||
|
||||
it("can read a line of text", function()
|
||||
local file = create_test_file "some\nfile\r\ncontents\n\n"
|
||||
|
||||
@ -223,6 +290,16 @@ describe("The fs library", function()
|
||||
expect(handle.readLine(true)):eq(nil)
|
||||
handle.close()
|
||||
end)
|
||||
|
||||
it("readAll always returns a string", function()
|
||||
local contents = "some\nfile\ncontents"
|
||||
local file = create_test_file "some\nfile\ncontents"
|
||||
|
||||
local handle = fs.open(file, mode)
|
||||
expect(handle.readAll()):eq(contents)
|
||||
expect(handle.readAll()):eq("")
|
||||
handle.close()
|
||||
end)
|
||||
end
|
||||
|
||||
describe("reading", function()
|
||||
|
@ -188,4 +188,31 @@ describe("The os library", function()
|
||||
expect.error(os.loadAPI, nil):eq("bad argument #1 (string expected, got nil)")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("os.queueEvent", function()
|
||||
local function roundtrip(...)
|
||||
local event_name = ("event_%08x"):format(math.random(1, 0x7FFFFFFF))
|
||||
os.queueEvent(event_name, ...)
|
||||
return select(2, os.pullEvent(event_name))
|
||||
end
|
||||
|
||||
it("preserves references in tables", function()
|
||||
local tbl = {}
|
||||
local xs = roundtrip({ tbl, tbl })
|
||||
expect(xs[1]):eq(xs[2])
|
||||
end)
|
||||
|
||||
it("does not preserve references in separate args", function()
|
||||
-- I'm not sure I like this behaviour, but it is what CC has always done.
|
||||
local tbl = {}
|
||||
local xs, ys = roundtrip(tbl, tbl)
|
||||
expect(xs):ne(ys)
|
||||
end)
|
||||
|
||||
it("clones objects", function()
|
||||
local tbl = {}
|
||||
local xs = roundtrip(tbl)
|
||||
expect(xs):ne(tbl)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
@ -13,13 +13,13 @@ correct tokens and positions, and that it can report sensible error messages.
|
||||
We can lex some basic comments:
|
||||
|
||||
```lua
|
||||
-- A basic singleline comment comment
|
||||
-- A basic singleline comment
|
||||
--[ Not a multiline comment
|
||||
--[= Also not a multiline comment!
|
||||
```
|
||||
|
||||
```txt
|
||||
1:1-1:37 COMMENT -- A basic singleline comment comment
|
||||
1:1-1:29 COMMENT -- A basic singleline comment
|
||||
2:1-2:27 COMMENT --[ Not a multiline comment
|
||||
3:1-3:34 COMMENT --[= Also not a multiline comment!
|
||||
```
|
||||
|
@ -122,6 +122,7 @@ public class ComputerCraft {
|
||||
|
||||
PlayerBlockBreakEvents.BEFORE.register(FabricCommonHooks::onBlockDestroy);
|
||||
UseBlockCallback.EVENT.register(FabricCommonHooks::useOnBlock);
|
||||
UseBlockCallback.EVENT.register(CommonHooks::onUseBlock);
|
||||
|
||||
LootTableEvents.MODIFY.register((id, tableBuilder, source, registries) -> {
|
||||
var pool = CommonHooks.getExtraLootPool(id);
|
||||
|
@ -14,6 +14,7 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.Event;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.minecraft.gametest.framework.GameTestRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
@ -26,6 +27,7 @@ public class TestMod implements ModInitializer, ClientModInitializer {
|
||||
ServerLifecycleEvents.SERVER_STARTED.addPhaseOrdering(Event.DEFAULT_PHASE, phase);
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(phase, TestHooks::onServerStarted);
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> CCTestCommand.register(dispatcher));
|
||||
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> !TestHooks.onBeforeDestroyBlock(level, pos, state));
|
||||
|
||||
TestHooks.loadTests(GameTestRegistry::register);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.neoforged.bus.api.EventPriority;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
@ -18,6 +19,7 @@ import net.neoforged.neoforge.event.LootTableLoadEvent;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent;
|
||||
import net.neoforged.neoforge.event.entity.living.LivingDropsEvent;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||
import net.neoforged.neoforge.event.level.ChunkTicketLevelUpdatedEvent;
|
||||
import net.neoforged.neoforge.event.level.ChunkWatchEvent;
|
||||
@ -78,6 +80,15 @@ public class ForgeCommonHooks {
|
||||
CommonHooks.onChunkTicketLevelChanged(event.getLevel(), event.getChunkPos(), event.getOldTicketLevel(), event.getNewTicketLevel());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onUseBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
var result = CommonHooks.onUseBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getHitVec());
|
||||
if (result == InteractionResult.PASS) return;
|
||||
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(result);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAddReloadListeners(AddReloadListenerEvent event) {
|
||||
CommonHooks.onDatapackReload((id, listener) -> event.addListener(listener));
|
||||
|
@ -15,6 +15,7 @@ import net.neoforged.neoforge.client.event.ScreenEvent;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.neoforge.event.RegisterGameTestsEvent;
|
||||
import net.neoforged.neoforge.event.level.BlockEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStartedEvent;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
|
||||
@ -26,6 +27,10 @@ public class TestMod {
|
||||
var bus = NeoForge.EVENT_BUS;
|
||||
bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer()));
|
||||
bus.addListener((RegisterCommandsEvent e) -> CCTestCommand.register(e.getDispatcher()));
|
||||
bus.addListener((BlockEvent.BreakEvent e) -> {
|
||||
if (TestHooks.onBeforeDestroyBlock(e.getLevel(), e.getPos(), e.getState())) e.setCanceled(true);
|
||||
});
|
||||
|
||||
if (FMLEnvironment.dist == Dist.CLIENT) TestMod.onInitializeClient();
|
||||
|
||||
modBus.addListener((RegisterGameTestsEvent event) -> TestHooks.loadTests(event::register));
|
||||
|
@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
|
||||
|
||||
package cc.tweaked.linter
|
||||
|
||||
import com.google.errorprone.BugPattern
|
||||
import com.google.errorprone.VisitorState
|
||||
import com.google.errorprone.bugpatterns.BugChecker
|
||||
import com.google.errorprone.matchers.Description
|
||||
import com.google.errorprone.util.ASTHelpers
|
||||
import com.sun.source.tree.*
|
||||
import com.sun.source.util.TreeScanner
|
||||
import com.sun.tools.javac.code.Symbol.MethodSymbol
|
||||
import javax.lang.model.element.Modifier
|
||||
|
||||
@BugPattern(
|
||||
summary = "Checks that a methods invoke their super method.",
|
||||
explanation = """
|
||||
This extends ErrorProne's built in "MustCallSuper" with several additional Minecraft-specific methods.
|
||||
""",
|
||||
severity = BugPattern.SeverityLevel.ERROR,
|
||||
tags = [BugPattern.StandardTags.LIKELY_ERROR],
|
||||
)
|
||||
class ExtraMustCallSuper : BugChecker(), BugChecker.MethodTreeMatcher {
|
||||
companion object {
|
||||
private val REQUIRED_METHODS = setOf(
|
||||
MethodReference("net.minecraft.world.level.block.entity.BlockEntity", "setRemoved"),
|
||||
MethodReference("net.minecraft.world.level.block.entity.BlockEntity", "clearRemoved"),
|
||||
)
|
||||
}
|
||||
|
||||
override fun matchMethod(tree: MethodTree, state: VisitorState): Description {
|
||||
val methodSym: MethodSymbol = ASTHelpers.getSymbol(tree)
|
||||
if (methodSym.modifiers.contains(Modifier.ABSTRACT)) return Description.NO_MATCH
|
||||
|
||||
val superMethod: MethodReference = findRequiredSuper(methodSym, state) ?: return Description.NO_MATCH
|
||||
val foundSuper = SuperScanner(superMethod.method).scan(tree, Unit) ?: false
|
||||
if (foundSuper) return Description.NO_MATCH
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage("This method overrides %s#%s but does not call the super method.".format(superMethod.owner, superMethod.method))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun findRequiredSuper(method: MethodSymbol, state: VisitorState): MethodReference? {
|
||||
for (superMethod in ASTHelpers.findSuperMethods(method, state.types)) {
|
||||
val superName = MethodReference(superMethod.owner.qualifiedName.toString(), superMethod.name.toString())
|
||||
if (REQUIRED_METHODS.contains(superName)) return superName
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private data class MethodReference(val owner: String, val method: String)
|
||||
|
||||
private class SuperScanner(private val methodName: String) : TreeScanner<Boolean?, Unit>() {
|
||||
// Skip visiting other elements.
|
||||
override fun visitClass(tree: ClassTree, state: Unit): Boolean = false
|
||||
override fun visitLambdaExpression(tree: LambdaExpressionTree, state: Unit): Boolean = false
|
||||
|
||||
override fun visitMethodInvocation(tree: MethodInvocationTree, state: Unit): Boolean? {
|
||||
val methodSelect: ExpressionTree = tree.methodSelect
|
||||
if (methodSelect.kind == Tree.Kind.MEMBER_SELECT) {
|
||||
val memberSelect = methodSelect as MemberSelectTree
|
||||
if (ASTHelpers.isSuper(memberSelect.expression) && memberSelect.identifier.contentEquals(methodName)) return true
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(tree, state)
|
||||
}
|
||||
|
||||
override fun reduce(r1: Boolean?, r2: Boolean?): Boolean = (r1 ?: false) || (r2 ?: false)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
# SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
#
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
cc.tweaked.linter.ExtraMustCallSuper
|
||||
cc.tweaked.linter.LoaderOverride
|
||||
cc.tweaked.linter.MissingLoaderOverride
|
||||
cc.tweaked.linter.SideChecker
|
||||
|
Loading…
Reference in New Issue
Block a user