1
0
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:
Jonathan Coates 2024-08-19 20:54:03 +01:00
commit 0d8ac304c7
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
76 changed files with 1609 additions and 245 deletions

View File

@ -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"

View File

@ -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)
}

View File

@ -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

Binary file not shown.

View File

@ -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
View File

@ -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
View File

@ -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 ##########################################################################

View File

@ -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);
}
}

View File

@ -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

View File

@ -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 {
/**

View File

@ -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);
}
/**

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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

View 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}
}
}

View File

@ -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"}
]
}

View 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"
}

View File

@ -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) {

View File

@ -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()),

View File

@ -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()

View File

@ -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)

View File

@ -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);
}

View File

@ -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(

View File

@ -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 {

View File

@ -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.

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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.
}
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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.

View File

@ -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
*/

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0

View File

@ -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();
}

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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)

View File

@ -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
}
}

View 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}"
]
}

View 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}"
]
}

View File

@ -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.

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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

View File

@ -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
));
}

View File

@ -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));

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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] {

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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.

View File

@ -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)) {

View File

@ -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()

View File

@ -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)

View File

@ -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!
```

View File

@ -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);

View File

@ -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);
}

View File

@ -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));

View File

@ -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));

View File

@ -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)
}
}

View File

@ -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