mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-08-01 00:32:52 +00:00
Merge branch 'mc-1.20.x' into mc-1.21.x
This commit is contained in:
commit
3f8c3b026a
@ -12,7 +12,7 @@ neogradle.subsystems.conventions.runs.enabled=false
|
||||
|
||||
# Mod properties
|
||||
isUnstable=true
|
||||
modVersion=1.114.5
|
||||
modVersion=1.115.0
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.21.1
|
||||
|
@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.media;
|
||||
|
||||
import dan200.computercraft.impl.ComputerCraftAPIService;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The contents of a page (or book) created by a ComputerCraft printer.
|
||||
*
|
||||
* @since 1.115
|
||||
*/
|
||||
@Nullable
|
||||
public interface PrintoutContents {
|
||||
/**
|
||||
* Get the (possibly empty) title for this printout.
|
||||
*
|
||||
* @return The title of this printout.
|
||||
*/
|
||||
String getTitle();
|
||||
|
||||
/**
|
||||
* Get the text contents of this printout, as a sequence of lines.
|
||||
* <p>
|
||||
* The lines in the printout may include blank lines at the end of the document, as well as trailing spaces on each
|
||||
* line.
|
||||
*
|
||||
* @return The text contents of this printout.
|
||||
*/
|
||||
Stream<String> getTextLines();
|
||||
|
||||
/**
|
||||
* Get the printout contents for a particular stack.
|
||||
*
|
||||
* @param stack The stack to get the contents for.
|
||||
* @return The printout contents, or {@code null} if this is not a printout item.
|
||||
*/
|
||||
static @Nullable PrintoutContents get(ItemStack stack) {
|
||||
return ComputerCraftAPIService.get().getPrintoutContents(stack);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.media.MediaProvider;
|
||||
import dan200.computercraft.api.media.PrintoutContents;
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
@ -84,6 +85,9 @@ public interface ComputerCraftAPIService {
|
||||
|
||||
DetailRegistry<BlockReference> getBlockInWorldDetailRegistry();
|
||||
|
||||
@Nullable
|
||||
PrintoutContents getPrintoutContents(ItemStack stack);
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable ComputerCraftAPIService INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
@ -70,7 +70,7 @@ public class TerminalWidget extends AbstractWidget {
|
||||
@Override
|
||||
public boolean charTyped(char ch, int modifiers) {
|
||||
var terminalChar = StringUtil.unicodeToTerminal(ch);
|
||||
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped(terminalChar);
|
||||
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,90 @@
|
||||
// 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 dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.pocket.PocketComputerData;
|
||||
import dan200.computercraft.client.render.CustomLecternRenderer;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
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.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.resources.model.Material;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.inventory.InventoryMenu;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
|
||||
/**
|
||||
* A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
|
||||
*
|
||||
* @see CustomLecternRenderer
|
||||
*/
|
||||
public class LecternPocketModel {
|
||||
public static final ResourceLocation TEXTURE_NORMAL = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
|
||||
public static final ResourceLocation TEXTURE_ADVANCED = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
|
||||
public static final ResourceLocation TEXTURE_COLOUR = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
|
||||
public static final ResourceLocation TEXTURE_FRAME = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
|
||||
public static final ResourceLocation TEXTURE_LIGHT = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_light");
|
||||
|
||||
private static final Material MATERIAL_NORMAL = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_NORMAL);
|
||||
private static final Material MATERIAL_ADVANCED = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_ADVANCED);
|
||||
private static final Material MATERIAL_COLOUR = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_COLOUR);
|
||||
private static final Material MATERIAL_FRAME = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_FRAME);
|
||||
private static final Material MATERIAL_LIGHT = new Material(InventoryMenu.BLOCK_ATLAS, TEXTURE_LIGHT);
|
||||
|
||||
// The size of the terminal within the model.
|
||||
public static final float TERM_WIDTH = 12.0f / 32.0f;
|
||||
public static final float TERM_HEIGHT = 14.0f / 32.0f;
|
||||
|
||||
// The size of the texture. The texture is 36x36, but is at 2x resolution.
|
||||
private static final int TEXTURE_WIDTH = 36 / 2;
|
||||
private static final int TEXTURE_HEIGHT = 36 / 2;
|
||||
|
||||
private final ModelPart root;
|
||||
|
||||
public LecternPocketModel() {
|
||||
root = buildPages();
|
||||
}
|
||||
|
||||
private static ModelPart buildPages() {
|
||||
var mesh = new MeshDefinition();
|
||||
var parts = mesh.getRoot();
|
||||
parts.addOrReplaceChild(
|
||||
"root",
|
||||
CubeListBuilder.create().texOffs(0, 0).addBox(0f, -5.0f, -4.0f, 1f, 10.0f, 8.0f),
|
||||
PartPose.ZERO
|
||||
);
|
||||
return mesh.getRoot().bake(TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the pocket computer model.
|
||||
*
|
||||
* @param poseStack The current pose stack.
|
||||
* @param bufferSource The buffer source to draw to.
|
||||
* @param packedLight The current light level.
|
||||
* @param packedOverlay The overlay texture (used for entity hurt animation).
|
||||
* @param family The computer family.
|
||||
* @param frameColour The pocket computer's {@linkplain DyedItemColor colour}.
|
||||
* @param lightColour The pocket computer's {@linkplain PocketComputerData#getLightState() light colour}.
|
||||
*/
|
||||
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight, int packedOverlay, ComputerFamily family, int frameColour, int lightColour) {
|
||||
if (frameColour != -1) {
|
||||
root.render(poseStack, MATERIAL_FRAME.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay);
|
||||
root.render(poseStack, MATERIAL_COLOUR.buffer(bufferSource, RenderType::entityCutout), packedLight, packedOverlay, frameColour);
|
||||
} else {
|
||||
var buffer = (family == ComputerFamily.ADVANCED ? MATERIAL_ADVANCED : MATERIAL_NORMAL).buffer(bufferSource, RenderType::entityCutout);
|
||||
root.render(poseStack, buffer, packedLight, packedOverlay);
|
||||
}
|
||||
|
||||
root.render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
|
||||
}
|
||||
}
|
@ -6,16 +6,30 @@ package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.client.model.LecternPocketModel;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.media.items.PrintoutData;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.LecternRenderer;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_HEIGHT;
|
||||
import static dan200.computercraft.client.render.text.FixedWidthFontRenderer.FONT_WIDTH;
|
||||
|
||||
/**
|
||||
* A block entity renderer for our {@linkplain CustomLecternBlockEntity lectern}.
|
||||
@ -23,10 +37,17 @@ import net.minecraft.world.level.block.LecternBlock;
|
||||
* This largely follows {@link LecternRenderer}, but with support for multiple types of item.
|
||||
*/
|
||||
public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternBlockEntity> {
|
||||
private static final int POCKET_TERMINAL_RENDER_DISTANCE = 32;
|
||||
|
||||
private final BlockEntityRenderDispatcher berDispatcher;
|
||||
private final LecternPrintoutModel printoutModel;
|
||||
private final LecternPocketModel pocketModel;
|
||||
|
||||
public CustomLecternRenderer(BlockEntityRendererProvider.Context context) {
|
||||
berDispatcher = context.getBlockEntityRenderDispatcher();
|
||||
|
||||
printoutModel = new LecternPrintoutModel();
|
||||
pocketModel = new LecternPocketModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,8 +66,46 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
} else {
|
||||
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutData.getOrEmpty(item).pages());
|
||||
}
|
||||
} else if (item.getItem() instanceof PocketComputerItem pocket) {
|
||||
var computer = ClientPocketComputers.get(item);
|
||||
|
||||
pocketModel.render(
|
||||
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), DyedItemColor.getOrDefault(item, -1),
|
||||
FastColor.ARGB32.opaque(computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState())
|
||||
);
|
||||
|
||||
// Jiggle the terminal about a bit, so (0, 0) is in the top left of the model's terminal hole.
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(90f));
|
||||
poseStack.translate(-0.5 * LecternPocketModel.TERM_WIDTH, 0.5 * LecternPocketModel.TERM_HEIGHT + 1f / 32.0f, 1 / 16.0f);
|
||||
poseStack.mulPose(Axis.XP.rotationDegrees(180));
|
||||
|
||||
// Either render the terminal or a black screen, depending on how close we are.
|
||||
var terminal = computer == null ? null : computer.getTerminal();
|
||||
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(poseStack, buffer.getBuffer(RenderTypes.TERMINAL));
|
||||
if (terminal != null && Vec3.atCenterOf(lectern.getBlockPos()).closerThan(berDispatcher.camera.getPosition(), POCKET_TERMINAL_RENDER_DISTANCE)) {
|
||||
renderPocketTerminal(poseStack, quadEmitter, terminal);
|
||||
} else {
|
||||
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, LecternPocketModel.TERM_WIDTH, LecternPocketModel.TERM_HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private static void renderPocketTerminal(PoseStack poseStack, FixedWidthFontRenderer.QuadEmitter quadEmitter, Terminal terminal) {
|
||||
var width = terminal.getWidth() * FONT_WIDTH;
|
||||
var height = terminal.getHeight() * FONT_HEIGHT;
|
||||
|
||||
// Scale the terminal down to fit in the available space.
|
||||
var scaleX = LecternPocketModel.TERM_WIDTH / (width + MARGIN * 2);
|
||||
var scaleY = LecternPocketModel.TERM_HEIGHT / (height + MARGIN * 2);
|
||||
var scale = Math.min(scaleX, scaleY);
|
||||
poseStack.scale(scale, scale, -1.0f);
|
||||
|
||||
// Convert the model dimensions to terminal space, then find out how large the margin should be.
|
||||
var marginX = ((LecternPocketModel.TERM_WIDTH / scale) - width) / 2;
|
||||
var marginY = ((LecternPocketModel.TERM_HEIGHT / scale) - height) / 2;
|
||||
|
||||
FixedWidthFontRenderer.drawTerminal(quadEmitter, marginX, marginY, terminal, marginY, marginY, marginX, marginX);
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public final class FixedWidthFontRenderer {
|
||||
static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;
|
||||
|
||||
private static final int BLACK = FastColor.ARGB32.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
|
||||
private static final float Z_OFFSET = 1e-3f;
|
||||
private static final float Z_OFFSET = 1e-4f;
|
||||
|
||||
private FixedWidthFontRenderer() {
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import com.mojang.serialization.Codec;
|
||||
import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.model.LecternPocketModel;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.data.client.ExtraModelsProvider;
|
||||
import dan200.computercraft.shared.turtle.TurtleOverlay;
|
||||
@ -71,7 +72,9 @@ public final class DataProviders {
|
||||
out.accept(ResourceLocation.withDefaultNamespace("blocks"), makeSprites(Stream.of(
|
||||
UpgradeSlot.LEFT_UPGRADE,
|
||||
UpgradeSlot.RIGHT_UPGRADE,
|
||||
LecternPrintoutModel.TEXTURE
|
||||
LecternPrintoutModel.TEXTURE,
|
||||
LecternPocketModel.TEXTURE_NORMAL, LecternPocketModel.TEXTURE_ADVANCED,
|
||||
LecternPocketModel.TEXTURE_COLOUR, LecternPocketModel.TEXTURE_FRAME, LecternPocketModel.TEXTURE_LIGHT
|
||||
)));
|
||||
out.accept(GuiSprites.SPRITE_SHEET, makeSprites(
|
||||
// Computers
|
||||
|
@ -2,6 +2,11 @@
|
||||
"sources": [
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_left"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/turtle_upgrade_right"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/printout"}
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/printout"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_normal"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_advanced"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_colour"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_frame"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:entity/pocket_computer_light"}
|
||||
]
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.media.MediaProvider;
|
||||
import dan200.computercraft.api.media.PrintoutContents;
|
||||
import dan200.computercraft.api.network.PacketNetwork;
|
||||
import dan200.computercraft.api.network.wired.WiredElement;
|
||||
import dan200.computercraft.api.network.wired.WiredNode;
|
||||
@ -25,6 +26,7 @@ import dan200.computercraft.core.filesystem.WritableFileMount;
|
||||
import dan200.computercraft.impl.detail.DetailRegistryImpl;
|
||||
import dan200.computercraft.impl.network.wired.WiredNodeImpl;
|
||||
import dan200.computercraft.impl.upgrades.TurtleToolSpec;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ResourceMount;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
@ -153,4 +155,9 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic
|
||||
public final DetailRegistry<BlockReference> getBlockInWorldDetailRegistry() {
|
||||
return blockDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PrintoutContents getPrintoutContents(ItemStack stack) {
|
||||
return stack.get(ModRegistry.DataComponents.PRINTOUT.get());
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ package dan200.computercraft.shared.lectern;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.stats.Stats;
|
||||
@ -20,8 +22,12 @@ 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.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Extends {@link LecternBlock} with support for {@linkplain PrintoutItem printouts}.
|
||||
@ -48,7 +54,7 @@ public class CustomLecternBlock extends LecternBlock {
|
||||
* @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 (item.getItem() instanceof PrintoutItem || item.getItem() instanceof PocketComputerItem) {
|
||||
if (!level.isClientSide) replaceLectern(player, level, pos, blockState, item);
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
@ -152,7 +158,7 @@ public class CustomLecternBlock extends LecternBlock {
|
||||
clearLectern(level, pos, state);
|
||||
} else {
|
||||
// Otherwise open the screen.
|
||||
player.openMenu(lectern);
|
||||
lectern.openMenu(player);
|
||||
}
|
||||
|
||||
player.awardStat(Stats.INTERACT_WITH_LECTERN);
|
||||
@ -160,4 +166,11 @@ public class CustomLecternBlock extends LecternBlock {
|
||||
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
|
||||
return level.isClientSide ? null : BlockEntityHelpers.createTickerHelper(type, ModRegistry.BlockEntities.LECTERN.get(), serverTicker);
|
||||
}
|
||||
|
||||
private static final BlockEntityTicker<CustomLecternBlockEntity> serverTicker = (level, pos, state, lectern) -> lectern.tick();
|
||||
}
|
||||
|
@ -10,28 +10,26 @@ 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.pocket.core.PocketHolder;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
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.SimpleMenuProvider;
|
||||
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;
|
||||
@ -41,7 +39,7 @@ import java.util.List;
|
||||
*
|
||||
* @see LecternBlockEntity
|
||||
*/
|
||||
public final class CustomLecternBlockEntity extends BlockEntity implements MenuProvider {
|
||||
public final class CustomLecternBlockEntity extends BlockEntity {
|
||||
private static final String NBT_ITEM = "Item";
|
||||
private static final String NBT_PAGE = "Page";
|
||||
|
||||
@ -83,6 +81,12 @@ public final class CustomLecternBlockEntity extends BlockEntity implements MenuP
|
||||
}
|
||||
}
|
||||
|
||||
void tick() {
|
||||
if (item.getItem() instanceof PocketComputerItem pocket) {
|
||||
pocket.tick(item, new PocketHolder.LecternHolder(this), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current page, emitting a redstone pulse if needed.
|
||||
*
|
||||
@ -125,24 +129,17 @@ public final class CustomLecternBlockEntity extends BlockEntity implements MenuP
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) {
|
||||
void openMenu(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),
|
||||
player.openMenu(new SimpleMenuProvider((id, inventory, entity) -> new PrintoutMenu(
|
||||
id, new LecternContainer(), 0,
|
||||
p -> Container.stillValidBlockEntity(this, p, Container.DEFAULT_DISTANCE_BUFFER),
|
||||
new PrintoutContainerData()
|
||||
);
|
||||
), getItem().getDisplayName()));
|
||||
} else if (item.getItem() instanceof PocketComputerItem pocket) {
|
||||
pocket.open(player, item, new PocketHolder.LecternHolder(this), true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return getItem().getDisplayName();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ package dan200.computercraft.shared.media.items;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import dan200.computercraft.api.media.PrintoutContents;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
@ -16,6 +17,7 @@ import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The contents of a printout.
|
||||
@ -25,7 +27,7 @@ import java.util.List;
|
||||
* @see PrintoutItem
|
||||
* @see dan200.computercraft.shared.ModRegistry.DataComponents#PRINTOUT
|
||||
*/
|
||||
public record PrintoutData(String title, List<Line> lines) {
|
||||
public record PrintoutData(String title, List<Line> lines) implements PrintoutContents {
|
||||
public static final int LINE_LENGTH = 25;
|
||||
public static final int LINES_PER_PAGE = 21;
|
||||
public static final int MAX_PAGES = 16;
|
||||
@ -107,4 +109,14 @@ public record PrintoutData(String title, List<Line> lines) {
|
||||
public int pages() {
|
||||
return Math.ceilDiv(lines.size(), LINES_PER_PAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return title();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> getTextLines() {
|
||||
return lines().stream().map(Line::text);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@
|
||||
package dan200.computercraft.shared.pocket.core;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlockEntity;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@ -51,6 +53,15 @@ public sealed interface PocketHolder {
|
||||
*/
|
||||
void setChanged();
|
||||
|
||||
/**
|
||||
* Whether the terminal is visible to all players in range, and so should be broadcast to everyone.
|
||||
*
|
||||
* @return Whether to send the terminal.
|
||||
*/
|
||||
default boolean isTerminalAlwaysVisible() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link Entity} holding a pocket computer.
|
||||
*/
|
||||
@ -112,4 +123,41 @@ public sealed interface PocketHolder {
|
||||
entity.setItem(entity.getItem().copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A pocket computer in a {@link CustomLecternBlockEntity}.
|
||||
*
|
||||
* @param lectern The lectern holding this item.
|
||||
*/
|
||||
record LecternHolder(CustomLecternBlockEntity lectern) implements PocketHolder {
|
||||
@Override
|
||||
public ServerLevel level() {
|
||||
return (ServerLevel) lectern.getLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 pos() {
|
||||
return Vec3.atCenterOf(lectern.getBlockPos());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos blockPos() {
|
||||
return lectern.getBlockPos();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(ServerComputer computer) {
|
||||
return !lectern().isRemoved() && PocketComputerItem.isServerComputer(computer, lectern.getItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChanged() {
|
||||
BlockEntityHelpers.updateBlock(lectern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminalAlwaysVisible() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public final class PocketServerComputer extends ServerComputer {
|
||||
// Broadcast the state to new players.
|
||||
var added = newTracking.stream().filter(x -> !tracking.contains(x)).toList();
|
||||
if (!added.isEmpty()) {
|
||||
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, false), added);
|
||||
ServerNetworking.sendToPlayers(new PocketComputerDataMessage(this, brain.holder().isTerminalAlwaysVisible()), added);
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,9 +83,15 @@ public final class PocketServerComputer extends ServerComputer {
|
||||
protected void onTerminalChanged() {
|
||||
super.onTerminalChanged();
|
||||
|
||||
if (brain.holder() instanceof PocketHolder.PlayerHolder holder && holder.isValid(this)) {
|
||||
// Broadcast the terminal to the current player.
|
||||
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder.entity());
|
||||
var holder = brain.holder() instanceof PocketHolder.PlayerHolder h && h.isValid(this) ? h.entity() : null;
|
||||
if (brain.holder().isTerminalAlwaysVisible() && !tracking.isEmpty()) {
|
||||
// If the terminal is always visible, send it to all players *and* the holder.
|
||||
var packet = new PocketComputerDataMessage(this, true);
|
||||
ServerNetworking.sendToPlayers(packet, tracking);
|
||||
if (holder != null && !tracking.contains(holder)) ServerNetworking.sendToPlayer(packet, holder);
|
||||
} else if (holder != null) {
|
||||
// Otherwise just send it to the holder.
|
||||
ServerNetworking.sendToPlayer(new PocketComputerDataMessage(this, true), holder);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,12 +58,20 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
/**
|
||||
* Tick a pocket computer.
|
||||
*
|
||||
* @param stack The current pocket computer stack.
|
||||
* @param holder The entity holding the pocket item.
|
||||
* @param brain The pocket computer brain.
|
||||
* @param stack The current pocket computer stack.
|
||||
* @param holder The entity holding the pocket item.
|
||||
* @param passive If set, the pocket computer will not be created if it doesn't exist, and will not be kept alive.
|
||||
*/
|
||||
private void tick(ItemStack stack, PocketHolder holder, PocketBrain brain) {
|
||||
brain.updateHolder(holder);
|
||||
public void tick(ItemStack stack, PocketHolder holder, boolean passive) {
|
||||
PocketBrain brain;
|
||||
if (passive) {
|
||||
var computer = getServerComputer(holder.level().getServer(), stack);
|
||||
if (computer == null) return;
|
||||
brain = computer.getBrain();
|
||||
} else {
|
||||
brain = getOrCreateBrain(holder.level(), holder, stack);
|
||||
brain.computer().keepAlive();
|
||||
}
|
||||
|
||||
// Update pocket upgrade
|
||||
var upgrade = brain.getUpgrade();
|
||||
@ -109,11 +117,7 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
if (slot < 0) return;
|
||||
|
||||
// If we're in the inventory, create a computer and keep it alive.
|
||||
var holder = new PocketHolder.PlayerHolder(player, slot);
|
||||
var brain = getOrCreateBrain((ServerLevel) world, holder, stack);
|
||||
brain.computer().keepAlive();
|
||||
|
||||
tick(stack, holder, brain);
|
||||
tick(stack, new PocketHolder.PlayerHolder(player, slot), false);
|
||||
}
|
||||
|
||||
@ForgeOverride
|
||||
@ -123,8 +127,7 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
|
||||
// If we're an item entity, tick an already existing computer (as to update the position), but do not keep the
|
||||
// computer alive.
|
||||
var computer = getServerComputer(level.getServer(), stack);
|
||||
if (computer != null) tick(stack, new PocketHolder.ItemEntityHolder(entity), computer.getBrain());
|
||||
tick(stack, new PocketHolder.ItemEntityHolder(entity), true);
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -141,25 +144,39 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
var stop = false;
|
||||
var upgrade = getUpgrade(stack);
|
||||
if (upgrade != null) {
|
||||
brain.updateHolder(holder);
|
||||
stop = upgrade.onRightClick(world, brain, computer.getPeripheral(ComputerSide.BACK));
|
||||
// Sync back just in case. We don't need to setChanged, as we'll return the item anyway.
|
||||
updateItem(stack, brain);
|
||||
}
|
||||
|
||||
if (!stop) {
|
||||
PlatformHelper.get().openMenu(
|
||||
player, stack.getHoverName(),
|
||||
(id, inventory, entity) -> new ComputerMenuWithoutInventory(
|
||||
hand == InteractionHand.OFF_HAND ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(),
|
||||
id, inventory, p -> isServerComputer(computer, p.getItemInHand(hand)), computer
|
||||
),
|
||||
new ComputerContainerData(computer, stack));
|
||||
}
|
||||
if (!stop) openImpl(player, stack, holder, hand == InteractionHand.OFF_HAND, computer);
|
||||
}
|
||||
return new InteractionResultHolder<>(InteractionResult.sidedSuccess(world.isClientSide), stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a container for this pocket computer.
|
||||
*
|
||||
* @param player The player to show the menu for.
|
||||
* @param stack The pocket computer stack.
|
||||
* @param holder The holder of the pocket computer.
|
||||
* @param isTypingOnly Open the off-hand pocket screen (only supporting typing, with no visible terminal).
|
||||
*/
|
||||
public void open(Player player, ItemStack stack, PocketHolder holder, boolean isTypingOnly) {
|
||||
var brain = getOrCreateBrain(holder.level(), holder, stack);
|
||||
var computer = brain.computer();
|
||||
computer.turnOn();
|
||||
openImpl(player, stack, holder, isTypingOnly, computer);
|
||||
}
|
||||
|
||||
private static void openImpl(Player player, ItemStack stack, PocketHolder holder, boolean isTypingOnly, ServerComputer computer) {
|
||||
PlatformHelper.get().openMenu(player, stack.getHoverName(), (id, inventory, entity) -> new ComputerMenuWithoutInventory(
|
||||
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory,
|
||||
p -> holder.isValid(computer),
|
||||
computer
|
||||
), new ComputerContainerData(computer, stack));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
var baseString = getDescriptionId(stack);
|
||||
@ -195,7 +212,11 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
var registry = ServerContext.get(level.getServer()).registry();
|
||||
{
|
||||
var computer = getServerComputer(registry, stack);
|
||||
if (computer != null) return computer.getBrain();
|
||||
if (computer != null) {
|
||||
var brain = computer.getBrain();
|
||||
brain.updateHolder(holder);
|
||||
return brain;
|
||||
}
|
||||
}
|
||||
|
||||
var computerID = NonNegativeId.getOrCreate(level.getServer(), stack, ModRegistry.DataComponents.COMPUTER_ID.get(), IDAssigner.COMPUTER);
|
||||
@ -209,8 +230,7 @@ public class PocketComputerItem extends Item implements IMedia {
|
||||
|
||||
stack.set(ModRegistry.DataComponents.COMPUTER.get(), new ServerComputerReference(registry.getSessionID(), computer.register()));
|
||||
|
||||
// Only turn on when initially creating the computer, rather than each tick.
|
||||
if (isMarkedOn(stack) && holder instanceof PocketHolder.PlayerHolder) computer.turnOn();
|
||||
if (isMarkedOn(stack)) computer.turnOn();
|
||||
|
||||
updateItem(stack, brain);
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 152 B |
Binary file not shown.
After Width: | Height: | Size: 179 B |
Binary file not shown.
After Width: | Height: | Size: 119 B |
Binary file not shown.
After Width: | Height: | Size: 92 B |
Binary file not shown.
After Width: | Height: | Size: 152 B |
@ -69,7 +69,7 @@ public class WebsocketHandle {
|
||||
* Send a websocket message to the connected server.
|
||||
*
|
||||
* @param message The message to send.
|
||||
* @param binary Whether this message should be treated as a
|
||||
* @param binary Whether this message should be treated as a binary message.
|
||||
* @throws LuaException If the message is too large.
|
||||
* @throws LuaException If the websocket has been closed.
|
||||
* @cc.changed 1.81.0 Added argument for binary mode.
|
||||
|
@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.ILuaFunction;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.lua.errorinfo.ErrorInfoLib;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.util.LuaUtil;
|
||||
@ -77,6 +78,7 @@ public class CobaltLuaMachine implements ILuaMachine {
|
||||
var globals = state.globals();
|
||||
CoreLibraries.debugGlobals(state);
|
||||
Bit32Lib.add(state, globals);
|
||||
ErrorInfoLib.add(state);
|
||||
globals.rawset("_HOST", ValueFactory.valueOf(environment.hostString()));
|
||||
globals.rawset("_CC_DEFAULT_SETTINGS", ValueFactory.valueOf(CoreConfig.defaultComputerSettings));
|
||||
|
||||
|
@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2009-2011 Luaj.org, 2015-2020 SquidDev
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package dan200.computercraft.core.lua.errorinfo;
|
||||
|
||||
import org.squiddev.cobalt.Prototype;
|
||||
|
||||
import static org.squiddev.cobalt.Lua.*;
|
||||
|
||||
/**
|
||||
* Extracted parts of Cobalt's {@link org.squiddev.cobalt.debug.DebugHelpers}.
|
||||
*/
|
||||
final class DebugHelpers {
|
||||
private DebugHelpers() {
|
||||
}
|
||||
|
||||
private static int filterPc(int pc, int jumpTarget) {
|
||||
return pc < jumpTarget ? -1 : pc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the PC where a register was last set.
|
||||
* <p>
|
||||
* This makes some assumptions about the structure of the bytecode, namely that there are no back edges within the
|
||||
* CFG. As a result, this is only valid for temporary values, and not locals.
|
||||
*
|
||||
* @param pt The function prototype.
|
||||
* @param lastPc The PC to work back from.
|
||||
* @param reg The register.
|
||||
* @return The last instruction where the register was set, or {@code -1} if not defined.
|
||||
*/
|
||||
static int findSetReg(Prototype pt, int lastPc, int reg) {
|
||||
var lastInsn = -1; // Last instruction that changed "reg";
|
||||
var jumpTarget = 0; // Any code before this address is conditional
|
||||
|
||||
for (var pc = 0; pc < lastPc; pc++) {
|
||||
var i = pt.code[pc];
|
||||
var op = GET_OPCODE(i);
|
||||
var a = GETARG_A(i);
|
||||
switch (op) {
|
||||
case OP_LOADNIL -> {
|
||||
var b = GETARG_B(i);
|
||||
if (a <= reg && reg <= a + b) lastInsn = filterPc(pc, jumpTarget);
|
||||
}
|
||||
case OP_TFORCALL -> {
|
||||
if (a >= a + 2) lastInsn = filterPc(pc, jumpTarget);
|
||||
}
|
||||
case OP_CALL, OP_TAILCALL -> {
|
||||
if (reg >= a) lastInsn = filterPc(pc, jumpTarget);
|
||||
}
|
||||
case OP_JMP -> {
|
||||
var dest = pc + 1 + GETARG_sBx(i);
|
||||
// If jump is forward and doesn't skip lastPc, update jump target
|
||||
if (pc < dest && dest <= lastPc && dest > jumpTarget) jumpTarget = dest;
|
||||
}
|
||||
default -> {
|
||||
if (testAMode(op) && reg == a) lastInsn = filterPc(pc, jumpTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastInsn;
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.lua.errorinfo;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.squiddev.cobalt.*;
|
||||
import org.squiddev.cobalt.debug.DebugFrame;
|
||||
import org.squiddev.cobalt.function.LuaFunction;
|
||||
import org.squiddev.cobalt.function.RegisteredFunction;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.squiddev.cobalt.Lua.*;
|
||||
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_ANY_HOOK;
|
||||
|
||||
/**
|
||||
* Provides additional info about an error.
|
||||
* <p>
|
||||
* This is currently an internal and deeply unstable module. It's not clear if doing this via bytecode (rather than an
|
||||
* AST) is the correct approach and/or, what the correct design is.
|
||||
*/
|
||||
public class ErrorInfoLib {
|
||||
private static final int MAX_DEPTH = 8;
|
||||
|
||||
private static final RegisteredFunction[] functions = new RegisteredFunction[]{
|
||||
RegisteredFunction.ofV("info_for_nil", ErrorInfoLib::getInfoForNil),
|
||||
};
|
||||
|
||||
public static void add(LuaState state) throws LuaError {
|
||||
state.registry().getSubTable(Constants.LOADED).rawset("cc.internal.error_info", RegisteredFunction.bind(functions));
|
||||
}
|
||||
|
||||
private static Varargs getInfoForNil(LuaState state, Varargs args) throws LuaError {
|
||||
var thread = args.arg(1).checkThread();
|
||||
var level = args.arg(2).checkInteger();
|
||||
|
||||
var context = getInfoForNil(state, thread, level);
|
||||
return context == null ? Constants.NIL : ValueFactory.varargsOf(
|
||||
ValueFactory.valueOf(context.op()), ValueFactory.valueOf(context.source().isGlobal()),
|
||||
context.source().table(), context.source().key()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get some additional information about an {@code attempt to $OP (a nil value)} error. This often occurs as a
|
||||
* result of a misspelled local, global or table index, and so we attempt to detect those cases.
|
||||
*
|
||||
* @param state The current Lua state.
|
||||
* @param thread The thread which has errored.
|
||||
* @param level The level where the error occurred. We currently expect this to always be 0.
|
||||
* @return Some additional information about the error, where available.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static @Nullable NilInfo getInfoForNil(LuaState state, LuaThread thread, int level) {
|
||||
var frame = thread.getDebugState().getFrame(level);
|
||||
if (frame == null || frame.closure == null || (frame.flags & FLAG_ANY_HOOK) != 0) return null;
|
||||
|
||||
var prototype = frame.closure.getPrototype();
|
||||
var pc = frame.pc;
|
||||
var insn = prototype.code[pc];
|
||||
|
||||
// Find what operation we're doing that errored.
|
||||
return switch (GET_OPCODE(insn)) {
|
||||
case OP_CALL, OP_TAILCALL ->
|
||||
NilInfo.of("call", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
|
||||
case OP_GETTABLE, OP_SETTABLE, OP_SELF ->
|
||||
NilInfo.of("index", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about an {@code attempt to $OP (a nil value)} error.
|
||||
*
|
||||
* @param op The operation we tried to perform.
|
||||
* @param source The expression that resulted in a nil value.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
record NilInfo(String op, ValueSource source) {
|
||||
public static @Nullable NilInfo of(String op, @Nullable ValueSource values) {
|
||||
return values == null ? null : new NilInfo(op, values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A partially-reconstructed Lua expression. This currently only is used for table indexing ({@code table[key]}.
|
||||
*
|
||||
* @param isGlobal Whether this is a global table access. This is a best-effort guess, and does not distinguish between
|
||||
* {@code foo} and {@code _ENV.foo}.
|
||||
* @param table The table being indexed.
|
||||
* @param key The key we tried to index.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
record ValueSource(boolean isGlobal, LuaValue table, LuaString key) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to partially reconstruct a Lua expression from the current debug state.
|
||||
*
|
||||
* @param state The current Lua state.
|
||||
* @param frame The current debug frame.
|
||||
* @param prototype The current function.
|
||||
* @param pc The current program counter.
|
||||
* @param register The register where this value was stored.
|
||||
* @param depth The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
|
||||
* @return The reconstructed expression, or {@code null} if not available.
|
||||
*/
|
||||
@SuppressWarnings("NullTernary")
|
||||
private static @Nullable ValueSource resolveValueSource(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
|
||||
if (depth > MAX_DEPTH) return null;
|
||||
if (prototype.getLocalName(register + 1, pc) != null) return null;
|
||||
|
||||
// Find where this register was set. If unknown, then abort.
|
||||
pc = DebugHelpers.findSetReg(prototype, pc, register);
|
||||
if (pc == -1) return null;
|
||||
|
||||
var insn = prototype.code[pc];
|
||||
return switch (GET_OPCODE(insn)) {
|
||||
case OP_MOVE -> {
|
||||
var a = GETARG_A(insn);
|
||||
var b = GETARG_B(insn); // move from `b' to `a'
|
||||
yield b < a ? resolveValueSource(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b' .
|
||||
}
|
||||
case OP_GETTABUP, OP_GETTABLE, OP_SELF -> {
|
||||
var tableIndex = GETARG_B(insn);
|
||||
var keyIndex = GETARG_C(insn);
|
||||
// We're only interested in expressions of the form "foo.bar". Showing a "did you mean" hint for
|
||||
// "foo[i]" isn't very useful!
|
||||
if (!ISK(keyIndex)) yield null;
|
||||
|
||||
var key = prototype.constants[INDEXK(keyIndex)];
|
||||
if (key.type() != Constants.TSTRING) yield null;
|
||||
|
||||
var table = GET_OPCODE(insn) == OP_GETTABUP
|
||||
? frame.closure.getUpvalue(tableIndex).getValue()
|
||||
: evaluate(state, frame, prototype, pc, tableIndex, depth);
|
||||
if (table == null) yield null;
|
||||
|
||||
var isGlobal = GET_OPCODE(insn) == OP_GETTABUP && Objects.equals(prototype.getUpvalueName(tableIndex), Constants.ENV);
|
||||
yield new ValueSource(isGlobal, table, (LuaString) key);
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reconstruct the value of a register.
|
||||
*
|
||||
* @param state The current Lua state.
|
||||
* @param frame The current debug frame.
|
||||
* @param prototype The current function
|
||||
* @param pc The PC to evaluate at.
|
||||
* @param register The register to evaluate.
|
||||
* @param depth The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
|
||||
* @return The reconstructed value, or {@code null} if unavailable.
|
||||
*/
|
||||
@SuppressWarnings("NullTernary")
|
||||
private static @Nullable LuaValue evaluate(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
|
||||
if (depth >= MAX_DEPTH) return null;
|
||||
|
||||
// If this is a local, then return its contents.
|
||||
if (prototype.getLocalName(register + 1, pc) != null) return frame.stack[register];
|
||||
|
||||
// Otherwise find where this register was set. If unknown, then abort.
|
||||
pc = DebugHelpers.findSetReg(prototype, pc, register);
|
||||
if (pc == -1) return null;
|
||||
|
||||
var insn = prototype.code[pc];
|
||||
var opcode = GET_OPCODE(insn);
|
||||
return switch (opcode) {
|
||||
case OP_MOVE -> {
|
||||
var a = GETARG_A(insn);
|
||||
var b = GETARG_B(insn); // move from `b' to `a'
|
||||
yield b < a ? evaluate(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b'.
|
||||
}
|
||||
// Load constants
|
||||
case OP_LOADK -> prototype.constants[GETARG_Bx(insn)];
|
||||
case OP_LOADKX -> prototype.constants[GETARG_Ax(prototype.code[pc + 1])];
|
||||
case OP_LOADBOOL -> GETARG_B(insn) == 0 ? Constants.FALSE : Constants.TRUE;
|
||||
case OP_LOADNIL -> Constants.NIL;
|
||||
// Upvalues and tables.
|
||||
case OP_GETUPVAL -> frame.closure.getUpvalue(GETARG_B(insn)).getValue();
|
||||
case OP_GETTABLE, OP_GETTABUP -> {
|
||||
var table = opcode == OP_GETTABUP
|
||||
? frame.closure.getUpvalue(GETARG_B(insn)).getValue()
|
||||
: evaluate(state, frame, prototype, pc, GETARG_B(insn), depth + 1);
|
||||
if (table == null) yield null;
|
||||
|
||||
var key = evaluateK(state, frame, prototype, pc, GETARG_C(insn), depth + 1);
|
||||
yield key == null ? null : safeIndex(state, table, key);
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static @Nullable LuaValue evaluateK(LuaState state, DebugFrame frame, Prototype prototype, int pc, int registerOrConstant, int depth) {
|
||||
return ISK(registerOrConstant) ? prototype.constants[INDEXK(registerOrConstant)] : evaluate(state, frame, prototype, pc, registerOrConstant, depth + 1);
|
||||
}
|
||||
|
||||
private static @Nullable LuaValue safeIndex(LuaState state, LuaValue table, LuaValue key) {
|
||||
var loop = 0;
|
||||
do {
|
||||
LuaValue metatable;
|
||||
if (table instanceof LuaTable tbl) {
|
||||
var res = tbl.rawget(key);
|
||||
if (!res.isNil() || (metatable = tbl.metatag(state, CachedMetamethod.INDEX)).isNil()) return res;
|
||||
} else if ((metatable = table.metatag(state, CachedMetamethod.INDEX)).isNil()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metatable instanceof LuaFunction) return null;
|
||||
|
||||
table = metatable;
|
||||
}
|
||||
while (++loop < Constants.MAXTAGLOOP);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -18,24 +18,25 @@ public final class StringUtil {
|
||||
* Convert a Unicode character to a terminal one.
|
||||
*
|
||||
* @param chr The Unicode character.
|
||||
* @return The terminal character.
|
||||
* @return The terminal character. This is either in the range [0, 255] (if a valid character) or {@code -1} if
|
||||
* it cannot be mapped to CC's charset.
|
||||
*/
|
||||
public static byte unicodeToTerminal(int chr) {
|
||||
public static int unicodeToTerminal(int chr) {
|
||||
// ASCII and latin1 map to themselves
|
||||
if (chr == 0 || chr == '\t' || chr == '\n' || chr == '\r' || (chr >= ' ' && chr <= '~') || (chr >= 160 && chr <= 255)) {
|
||||
return (byte) chr;
|
||||
return chr;
|
||||
}
|
||||
|
||||
// Teletext block mosaics are *fairly* contiguous.
|
||||
if (chr >= 0x1FB00 && chr <= 0x1FB13) return (byte) (chr + (129 - 0x1fb00));
|
||||
if (chr >= 0x1FB14 && chr <= 0x1FB1D) return (byte) (chr + (150 - 0x1fb14));
|
||||
if (chr >= 0x1FB00 && chr <= 0x1FB13) return chr + (129 - 0x1fb00);
|
||||
if (chr >= 0x1FB14 && chr <= 0x1FB1D) return chr + (150 - 0x1fb14);
|
||||
|
||||
// Everything else is just a manual lookup. For now, we just use a big switch statement, which we spin into a
|
||||
// separate function to hopefully avoid inlining it here.
|
||||
return unicodeToCraftOsFallback(chr);
|
||||
}
|
||||
|
||||
private static byte unicodeToCraftOsFallback(int c) {
|
||||
private static int unicodeToCraftOsFallback(int c) {
|
||||
return switch (c) {
|
||||
case 0x263A -> 1;
|
||||
case 0x263B -> 2;
|
||||
@ -64,8 +65,8 @@ public final class StringUtil {
|
||||
case 0x25B2 -> 30;
|
||||
case 0x25BC -> 31;
|
||||
case 0x1FB99 -> 127;
|
||||
case 0x258C -> (byte) 149;
|
||||
default -> '?';
|
||||
case 0x258C -> 149;
|
||||
default -> -1;
|
||||
};
|
||||
}
|
||||
|
||||
@ -76,8 +77,8 @@ public final class StringUtil {
|
||||
* @param chr The character to check.
|
||||
* @return Whether this character can be typed.
|
||||
*/
|
||||
public static boolean isTypableChar(byte chr) {
|
||||
return chr != 0 && chr != '\r' && chr != '\n';
|
||||
public static boolean isTypableChar(int chr) {
|
||||
return chr >= 0 && chr <= 255 && chr != 0 && chr != '\r' && chr != '\n';
|
||||
}
|
||||
|
||||
private static boolean isAllowedInLabel(char c) {
|
||||
@ -110,8 +111,9 @@ public final class StringUtil {
|
||||
var iterator = clipboard.codePoints().iterator();
|
||||
while (iterator.hasNext() && idx <= output.length) {
|
||||
var chr = unicodeToTerminal(iterator.next());
|
||||
if (!isTypableChar(chr)) break;
|
||||
output[idx++] = chr;
|
||||
if (chr < 0) continue; // Strip out unconvertible characters
|
||||
if (!isTypableChar(chr)) break; // Stop at untypable ones.
|
||||
output[idx++] = (byte) chr;
|
||||
}
|
||||
|
||||
return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer();
|
||||
|
@ -39,60 +39,55 @@ the other.
|
||||
@since 1.2
|
||||
]]
|
||||
|
||||
local exception = dofile("rom/modules/main/cc/internal/tiny_require.lua")("cc.internal.exception")
|
||||
|
||||
local function create(...)
|
||||
local tFns = table.pack(...)
|
||||
local tCos = {}
|
||||
for i = 1, tFns.n, 1 do
|
||||
local fn = tFns[i]
|
||||
local barrier_ctx = { co = coroutine.running() }
|
||||
|
||||
local functions = table.pack(...)
|
||||
local threads = {}
|
||||
for i = 1, functions.n, 1 do
|
||||
local fn = functions[i]
|
||||
if type(fn) ~= "function" then
|
||||
error("bad argument #" .. i .. " (function expected, got " .. type(fn) .. ")", 3)
|
||||
end
|
||||
|
||||
tCos[i] = coroutine.create(fn)
|
||||
threads[i] = { co = coroutine.create(function() return exception.try_barrier(barrier_ctx, fn) end), filter = nil }
|
||||
end
|
||||
|
||||
return tCos
|
||||
return threads
|
||||
end
|
||||
|
||||
local function runUntilLimit(_routines, _limit)
|
||||
local count = #_routines
|
||||
local function runUntilLimit(threads, limit)
|
||||
local count = #threads
|
||||
if count < 1 then return 0 end
|
||||
local living = count
|
||||
|
||||
local tFilters = {}
|
||||
local eventData = { n = 0 }
|
||||
local event = { n = 0 }
|
||||
while true do
|
||||
for n = 1, count do
|
||||
local r = _routines[n]
|
||||
if r then
|
||||
if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
|
||||
local ok, param = coroutine.resume(r, table.unpack(eventData, 1, eventData.n))
|
||||
if not ok then
|
||||
error(param, 0)
|
||||
else
|
||||
tFilters[r] = param
|
||||
end
|
||||
if coroutine.status(r) == "dead" then
|
||||
_routines[n] = nil
|
||||
living = living - 1
|
||||
if living <= _limit then
|
||||
return n
|
||||
end
|
||||
for i = 1, count do
|
||||
local thread = threads[i]
|
||||
if thread and (thread.filter == nil or thread.filter == event[1] or event[1] == "terminate") then
|
||||
local ok, param = coroutine.resume(thread.co, table.unpack(event, 1, event.n))
|
||||
if ok then
|
||||
thread.filter = param
|
||||
elseif type(param) == "string" and exception.can_wrap_errors() then
|
||||
error(exception.make_exception(param, thread.co))
|
||||
else
|
||||
error(param, 0)
|
||||
end
|
||||
|
||||
if coroutine.status(thread.co) == "dead" then
|
||||
threads[i] = false
|
||||
living = living - 1
|
||||
if living <= limit then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for n = 1, count do
|
||||
local r = _routines[n]
|
||||
if r and coroutine.status(r) == "dead" then
|
||||
_routines[n] = nil
|
||||
living = living - 1
|
||||
if living <= _limit then
|
||||
return n
|
||||
end
|
||||
end
|
||||
end
|
||||
eventData = table.pack(os.pullEventRaw())
|
||||
|
||||
event = table.pack(os.pullEventRaw())
|
||||
end
|
||||
end
|
||||
|
||||
@ -120,8 +115,8 @@ from the [`parallel.waitForAny`] call.
|
||||
print("Everything done!")
|
||||
]]
|
||||
function waitForAny(...)
|
||||
local routines = create(...)
|
||||
return runUntilLimit(routines, #routines - 1)
|
||||
local threads = create(...)
|
||||
return runUntilLimit(threads, #threads - 1)
|
||||
end
|
||||
|
||||
--[[- Switches between execution of the functions, until all of them are
|
||||
@ -144,6 +139,6 @@ from the [`parallel.waitForAll`] call.
|
||||
print("Everything done!")
|
||||
]]
|
||||
function waitForAll(...)
|
||||
local routines = create(...)
|
||||
return runUntilLimit(routines, 0)
|
||||
local threads = create(...)
|
||||
return runUntilLimit(threads, 0)
|
||||
end
|
||||
|
@ -7,9 +7,7 @@
|
||||
-- @module textutils
|
||||
-- @since 1.2
|
||||
|
||||
local pgk_env = setmetatable({}, { __index = _ENV })
|
||||
pgk_env.require = dofile("rom/modules/main/cc/require.lua").make(pgk_env, "rom/modules/main")
|
||||
local require = pgk_env.require
|
||||
local require = dofile("rom/modules/main/cc/internal/tiny_require.lua")
|
||||
|
||||
local expect = require("cc.expect")
|
||||
local expect, field = expect.expect, expect.field
|
||||
|
@ -1,3 +1,14 @@
|
||||
# New features in CC: Tweaked 1.115.0
|
||||
|
||||
* Support placing pocket computers on lecterns.
|
||||
* Suggest alternative table keys on `nil` errors.
|
||||
* Errors from inside `parallel` functions now have source information attached.
|
||||
* Expose printout contents to the Java API.
|
||||
* Add support for MoreRed bundled cables.
|
||||
|
||||
Several bug fixes:
|
||||
* Ignore unrepresentable characters in `char`/`paste` events.
|
||||
|
||||
# New features in CC: Tweaked 1.114.5
|
||||
|
||||
One bug fix:
|
||||
@ -632,7 +643,7 @@ And several bug fixes:
|
||||
* Remove config option for the debug API.
|
||||
* Allow setting the subprotocol header for websockets.
|
||||
* Add basic JMX monitoring on dedicated servers.
|
||||
* Add support for MoreRed bundled.
|
||||
* Add support for MoreRed bundled cables.
|
||||
* Allow uploading files by dropping them onto a computer.
|
||||
|
||||
And several bug fixes:
|
||||
|
@ -1,6 +1,12 @@
|
||||
New features in CC: Tweaked 1.114.5
|
||||
New features in CC: Tweaked 1.115.0
|
||||
|
||||
One bug fix:
|
||||
* Fix `turtle.craft` crafting too many items for shapeless recipes.
|
||||
* Support placing pocket computers on lecterns.
|
||||
* Suggest alternative table keys on `nil` errors.
|
||||
* Errors from inside `parallel` functions now have source information attached.
|
||||
* Expose printout contents to the Java API.
|
||||
* Add support for MoreRed bundled cables.
|
||||
|
||||
Several bug fixes:
|
||||
* Ignore unrepresentable characters in `char`/`paste` events.
|
||||
|
||||
Type "help changelog" to see the full version history.
|
||||
|
@ -0,0 +1,167 @@
|
||||
-- SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
--
|
||||
-- SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
--[[- Internal tools for diagnosing errors and suggesting fixes.
|
||||
|
||||
> [!DANGER]
|
||||
> This is an internal module and SHOULD NOT be used in your own code. It may
|
||||
> be removed or changed at any time.
|
||||
|
||||
@local
|
||||
]]
|
||||
|
||||
local debug, type, rawget = debug, type, rawget
|
||||
local sub, lower, find, min, abs = string.sub, string.lower, string.find, math.min, math.abs
|
||||
|
||||
--[[- Compute the Optimal String Distance between two strings.
|
||||
|
||||
@tparam string str_a The first string.
|
||||
@tparam string str_b The second string.
|
||||
@treturn number|nil The distance between two strings, or nil if they are two far
|
||||
apart.
|
||||
]]
|
||||
local function osa_distance(str_a, str_b, threshold)
|
||||
local len_a, len_b = #str_a, #str_b
|
||||
|
||||
-- If the two strings are too different in length, then bail now.
|
||||
if abs(len_a - len_b) > threshold then return end
|
||||
|
||||
-- Zero-initialise our distance table.
|
||||
local d = {}
|
||||
for i = 1, (len_a + 1) * (len_b + 1) do d[i] = 0 end
|
||||
|
||||
-- Then fill the first row and column
|
||||
local function idx(a, b) return a * (len_a + 1) + b + 1 end
|
||||
for i = 0, len_a do d[idx(i, 0)] = i end
|
||||
for j = 0, len_b do d[idx(0, j)] = j end
|
||||
|
||||
-- Then compute our distance
|
||||
for i = 1, len_a do
|
||||
local char_a = sub(str_a, i, i)
|
||||
for j = 1, len_b do
|
||||
local char_b = sub(str_b, j, j)
|
||||
|
||||
local sub_cost
|
||||
if char_a == char_b then
|
||||
sub_cost = 0
|
||||
elseif lower(char_a) == lower(char_b) then
|
||||
sub_cost = 0.5
|
||||
else
|
||||
sub_cost = 1
|
||||
end
|
||||
|
||||
local new_cost = min(
|
||||
d[idx(i - 1, j)] + 1, -- Deletion
|
||||
d[idx(i, j - 1)] + 1, -- Insertion,
|
||||
d[idx(i - 1, j - 1)] + sub_cost -- Substitution
|
||||
)
|
||||
|
||||
-- Transposition
|
||||
if i > 1 and j > 1 and char_a == sub(str_b, j - 1, j - 1) and char_b == sub(str_a, i - 1, i - 1) then
|
||||
local trans_cost = d[idx(i - 2, j - 2)] + 1
|
||||
if trans_cost < new_cost then new_cost = trans_cost end
|
||||
end
|
||||
|
||||
d[idx(i, j)] = new_cost
|
||||
end
|
||||
end
|
||||
|
||||
local result = d[idx(len_a, len_b)]
|
||||
if result <= threshold then return result else return nil end
|
||||
end
|
||||
|
||||
--- Check whether this suggestion is useful.
|
||||
local function useful_suggestion(str)
|
||||
local len = #str
|
||||
return len > 0 and len < 32 and find(str, "^[%a_]%w*$")
|
||||
end
|
||||
|
||||
local function get_suggestions(is_global, value, key, thread, frame_offset)
|
||||
if not useful_suggestion(key) then return end
|
||||
|
||||
-- Pick a maximum number of edits. We're more lenient on longer strings, but
|
||||
-- still only allow two mistakes.
|
||||
local threshold = #key >= 5 and 2 or 1
|
||||
|
||||
-- Find all items in the table, and see if they seem similar.
|
||||
local suggestions = {}
|
||||
local function process_suggestion(k)
|
||||
if type(k) ~= "string" or not useful_suggestion(k) then return end
|
||||
|
||||
local distance = osa_distance(k, key, threshold)
|
||||
if distance then
|
||||
if distance < threshold then
|
||||
-- If this is better than any existing match, then prefer it.
|
||||
suggestions = { k }
|
||||
threshold = distance
|
||||
else
|
||||
-- Otherwise distance==threshold, and so just add it.
|
||||
suggestions[#suggestions + 1] = k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
while type(value) == "table" do
|
||||
for k in next, value do process_suggestion(k) end
|
||||
|
||||
local mt = debug.getmetatable(value)
|
||||
if mt == nil then break end
|
||||
value = rawget(mt, "__index")
|
||||
end
|
||||
|
||||
-- If we're attempting to lookup a global, then also suggest any locals and
|
||||
-- upvalues. Our upvalues will be incomplete, but maybe a little useful?
|
||||
if is_global then
|
||||
for i = 1, 200 do
|
||||
local name = debug.getlocal(thread, frame_offset, i)
|
||||
if not name then break end
|
||||
process_suggestion(name)
|
||||
end
|
||||
|
||||
local func = debug.getinfo(thread, frame_offset, "f").func
|
||||
for i = 1, 255 do
|
||||
local name = debug.getupvalue(func, i)
|
||||
if not name then break end
|
||||
process_suggestion(name)
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(suggestions)
|
||||
|
||||
return suggestions
|
||||
end
|
||||
|
||||
--[[- Get a tip to display at the end of an error.
|
||||
|
||||
@tparam string err The error message.
|
||||
@tparam coroutine thread The current thread.
|
||||
@tparam number frame_offset The offset into the thread where the current frame exists
|
||||
@return An optional message to append to the error.
|
||||
]]
|
||||
local function get_tip(err, thread, frame_offset)
|
||||
local nil_op = err:match("^attempt to (%l+) .* %(a nil value%)")
|
||||
if not nil_op then return end
|
||||
|
||||
local has_error_info, error_info = pcall(require, "cc.internal.error_info")
|
||||
if not has_error_info then return end
|
||||
local op, is_global, table, key = error_info.info_for_nil(thread, frame_offset)
|
||||
if op == nil or op ~= nil_op then return end
|
||||
|
||||
local suggestions = get_suggestions(is_global, table, key, thread, frame_offset)
|
||||
if not suggestions or next(suggestions) == nil then return end
|
||||
|
||||
local pretty = require "cc.pretty"
|
||||
local msg = "Did you mean: "
|
||||
|
||||
local n_suggestions = min(3, #suggestions)
|
||||
for i = 1, n_suggestions do
|
||||
if i > 1 then
|
||||
if i == n_suggestions then msg = msg .. " or " else msg = msg .. ", " end
|
||||
end
|
||||
msg = msg .. pretty.text(suggestions[i], colours.lightGrey)
|
||||
end
|
||||
return msg .. "?"
|
||||
end
|
||||
|
||||
return { get_tip = get_tip }
|
@ -12,7 +12,7 @@
|
||||
]]
|
||||
|
||||
local expect = require "cc.expect".expect
|
||||
local error_printer = require "cc.internal.error_printer"
|
||||
local type, debug, coroutine = type, debug, coroutine
|
||||
|
||||
local function find_frame(thread, file, line)
|
||||
-- Scan the first 16 frames for something interesting.
|
||||
@ -21,14 +21,14 @@ local function find_frame(thread, file, line)
|
||||
if not frame then break end
|
||||
|
||||
if frame.short_src == file and frame.what ~= "C" and frame.currentline == line then
|
||||
return frame
|
||||
return offset, frame
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[- Check whether this error is an exception.
|
||||
|
||||
Currently we don't provide a stable API for throwing (and propogating) rich
|
||||
Currently we don't provide a stable API for throwing (and propagating) rich
|
||||
errors, like those supported by this module. In lieu of that, we describe the
|
||||
exception protocol, which may be used by user-written coroutine managers to
|
||||
throw exceptions which are pretty-printed by the shell:
|
||||
@ -64,6 +64,86 @@ local function is_exception(exn)
|
||||
return mt and mt.__name == "exception" and type(rawget(exn, "message")) == "string" and type(rawget(exn, "thread")) == "thread"
|
||||
end
|
||||
|
||||
local exn_mt = {
|
||||
__name = "exception",
|
||||
__tostring = function(self) return self.message end,
|
||||
}
|
||||
|
||||
--[[- Create a new exception from a message and thread.
|
||||
|
||||
@tparam string message The exception message.
|
||||
@tparam coroutine thread The coroutine the error occurred on.
|
||||
@return The constructed exception.
|
||||
]]
|
||||
local function make_exception(message, thread)
|
||||
return setmetatable({ message = message, thread = thread }, exn_mt)
|
||||
end
|
||||
|
||||
--[[- A marker function for [`try`] and the wider exception machinery.
|
||||
|
||||
This function is typically the first function on the call stack. It acts as both
|
||||
a signifier that this function is exception aware, and allows us to store
|
||||
additional information for the exception machinery on the call stack.
|
||||
|
||||
@see can_wrap_errors
|
||||
]]
|
||||
local try_barrier = debug.getregistry().cc_try_barrier
|
||||
if not try_barrier then
|
||||
-- We define an extra "bounce" function to prevent f(...) being treated as a
|
||||
-- tail call, and so ensure the barrier remains on the stack.
|
||||
local function bounce(...) return ... end
|
||||
|
||||
--- @tparam { co = coroutine, can_wrap ?= boolean } parent The parent coroutine.
|
||||
-- @tparam function f The function to call.
|
||||
-- @param ... The arguments to this function.
|
||||
try_barrier = function(parent, f, ...) return bounce(f(...)) end
|
||||
|
||||
debug.getregistry().cc_try_barrier = try_barrier
|
||||
end
|
||||
|
||||
-- Functions that act as a barrier for exceptions.
|
||||
local pcall_functions = { [pcall] = true, [xpcall] = true, [load] = true }
|
||||
|
||||
--[[- Check to see whether we can wrap errors into an exception.
|
||||
|
||||
This scans the current thread (up to a limit), and any parent threads, to
|
||||
determine if there is a pcall anywhere on the callstack. If not, then we know
|
||||
the error message is not observed by user code, and so may be wrapped into an
|
||||
exception.
|
||||
|
||||
@tparam[opt] coroutine The thread to check. Defaults to the current thread.
|
||||
@treturn boolean Whether we can wrap errors into exceptions.
|
||||
]]
|
||||
local function can_wrap_errors(thread)
|
||||
if not thread then thread = coroutine.running() end
|
||||
|
||||
for offset = 0, 31 do
|
||||
local frame = debug.getinfo(thread, offset, "f")
|
||||
if not frame then return false end
|
||||
|
||||
local func = frame.func
|
||||
if func == try_barrier then
|
||||
-- If we've a try barrier, then extract the parent coroutine and
|
||||
-- check if it can wrap errors.
|
||||
local _, parent = debug.getlocal(thread, offset, 1)
|
||||
if type(parent) ~= "table" or type(parent.co) ~= "thread" then return false end
|
||||
|
||||
local result = parent.can_wrap
|
||||
if result == nil then
|
||||
result = can_wrap_errors(parent.co)
|
||||
parent.can_wrap = result
|
||||
end
|
||||
|
||||
return result
|
||||
elseif pcall_functions[func] then
|
||||
-- If we're a pcall, then abort.
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--[[- Attempt to call the provided function `func` with the provided arguments.
|
||||
|
||||
@tparam function func The function to call.
|
||||
@ -79,8 +159,8 @@ end
|
||||
local function try(func, ...)
|
||||
expect(1, func, "function")
|
||||
|
||||
local co = coroutine.create(func)
|
||||
local result = table.pack(coroutine.resume(co, ...))
|
||||
local co = coroutine.create(try_barrier)
|
||||
local result = table.pack(coroutine.resume(co, { co = co, can_wrap = true }, func, ...))
|
||||
|
||||
while coroutine.status(co) ~= "dead" do
|
||||
local event = table.pack(os.pullEventRaw(result[2]))
|
||||
@ -111,11 +191,11 @@ local function report(err, thread, source_map)
|
||||
|
||||
if type(err) ~= "string" then return end
|
||||
|
||||
local file, line = err:match("^([^:]+):(%d+):")
|
||||
local file, line, err = err:match("^([^:]+):(%d+): (.*)")
|
||||
if not file then return end
|
||||
line = tonumber(line)
|
||||
|
||||
local frame = find_frame(thread, file, line)
|
||||
local frame_offset, frame = find_frame(thread, file, line)
|
||||
if not frame or not frame.currentcolumn then return end
|
||||
|
||||
local column = frame.currentcolumn
|
||||
@ -152,16 +232,22 @@ local function report(err, thread, source_map)
|
||||
-- Could not determine the line. Bail.
|
||||
if not line_contents or #line_contents == "" then return end
|
||||
|
||||
error_printer({
|
||||
require("cc.internal.error_printer")({
|
||||
get_pos = function() return line, column end,
|
||||
get_line = function() return line_contents end,
|
||||
}, {
|
||||
{ tag = "annotate", start_pos = column, end_pos = column, msg = "" },
|
||||
require "cc.internal.error_hints".get_tip(err, thread, frame_offset),
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
return {
|
||||
make_exception = make_exception,
|
||||
|
||||
try_barrier = try_barrier,
|
||||
can_wrap_errors = can_wrap_errors,
|
||||
|
||||
try = try,
|
||||
report = report,
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
-- SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
--
|
||||
-- SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
--[[- A minimal implementation of require.
|
||||
|
||||
This is intended for use with APIs, and other internal code which is not run in
|
||||
the [`shell`] environment. This allows us to avoid some of the overhead of
|
||||
loading the full [`cc.require`] module.
|
||||
|
||||
> [!DANGER]
|
||||
> This is an internal module and SHOULD NOT be used in your own code. It may
|
||||
> be removed or changed at any time.
|
||||
|
||||
@local
|
||||
|
||||
@tparam string name The module to require.
|
||||
@return The required module.
|
||||
]]
|
||||
|
||||
local loaded = {}
|
||||
local env = setmetatable({}, { __index = _G })
|
||||
local function require(name)
|
||||
local result = loaded[name]
|
||||
if result then return result end
|
||||
|
||||
local path = "rom/modules/main/" .. name:gsub("%.", "/")
|
||||
if fs.exists(path .. ".lua") then
|
||||
result = assert(loadfile(path .. ".lua", nil, env))()
|
||||
else
|
||||
result = assert(loadfile(path .. "/init.lua", nil, env))()
|
||||
end
|
||||
loaded[name] = result
|
||||
return result
|
||||
end
|
||||
env.require = require
|
||||
return require
|
@ -31,7 +31,7 @@ setmetatable(tEnv, { __index = _ENV })
|
||||
do
|
||||
local make_package = require "cc.require".make
|
||||
local dir = shell.dir()
|
||||
_ENV.require, _ENV.package = make_package(_ENV, dir)
|
||||
tEnv.require, tEnv.package = make_package(tEnv, dir)
|
||||
end
|
||||
|
||||
if term.isColour() then
|
||||
|
@ -413,7 +413,7 @@ public class ComputerTestDelegate {
|
||||
var wholeMessage = new StringBuilder();
|
||||
if (message != null) wholeMessage.append(message);
|
||||
if (trace != null) {
|
||||
if (wholeMessage.length() != 0) wholeMessage.append('\n');
|
||||
if (!wholeMessage.isEmpty()) wholeMessage.append('\n');
|
||||
wholeMessage.append(trace);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.lua.errorinfo;
|
||||
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.squiddev.cobalt.*;
|
||||
import org.squiddev.cobalt.compiler.CompileException;
|
||||
import org.squiddev.cobalt.compiler.LoadState;
|
||||
import org.squiddev.cobalt.lib.CoreLibraries;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class ErrorInfoLibTest {
|
||||
@Test
|
||||
public void testNilInfoForUnknownLibFunction() throws LuaError, CompileException {
|
||||
var state = newState();
|
||||
var thread = captureError(state, "string.forma()");
|
||||
|
||||
assertEquals(
|
||||
new ErrorInfoLib.NilInfo(
|
||||
"call",
|
||||
new ErrorInfoLib.ValueSource(false, state.globals().rawget("string"), ValueFactory.valueOf("forma"))
|
||||
),
|
||||
ErrorInfoLib.getInfoForNil(state, thread, 0)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNilInfoForUnknownGlobal() throws LuaError, CompileException {
|
||||
var state = newState();
|
||||
var thread = captureError(state, "pront()");
|
||||
|
||||
assertEquals(
|
||||
new ErrorInfoLib.NilInfo(
|
||||
"call",
|
||||
new ErrorInfoLib.ValueSource(true, state.globals(), ValueFactory.valueOf("pront"))
|
||||
),
|
||||
ErrorInfoLib.getInfoForNil(state, thread, 0)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNilInfoForComplexExpression() throws LuaError, CompileException {
|
||||
var state = newState();
|
||||
var thread = captureError(state, "x = { { y = 1 } }; for i = 1, #x do x[i].z() end");
|
||||
|
||||
var inner = ((LuaTable) state.globals().rawget("x")).rawget(1);
|
||||
assertEquals(
|
||||
new ErrorInfoLib.NilInfo(
|
||||
"call",
|
||||
new ErrorInfoLib.ValueSource(false, inner, ValueFactory.valueOf("z"))
|
||||
),
|
||||
ErrorInfoLib.getInfoForNil(state, thread, 0)
|
||||
);
|
||||
}
|
||||
|
||||
private static LuaState newState() throws LuaError {
|
||||
var state = new LuaState();
|
||||
CoreLibraries.standardGlobals(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private static LuaThread captureError(LuaState state, @Language("lua") String contents) throws CompileException, LuaError {
|
||||
var fn = LoadState.load(state, new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)), "=in.lua", state.globals());
|
||||
var thread = new LuaThread(state, fn);
|
||||
Assertions.assertThrows(LuaError.class, () -> LuaThread.run(thread, Constants.NIL));
|
||||
return thread;
|
||||
}
|
||||
}
|
@ -193,7 +193,7 @@ local function format(value)
|
||||
return "\"" .. escaped .. "\""
|
||||
else
|
||||
local ok, res = pcall(textutils.serialise, value)
|
||||
if ok then return res else return tostring(value) end
|
||||
if ok then return (res:gsub("\\\n", "\\n")) else return tostring(value) end
|
||||
end
|
||||
end
|
||||
|
||||
@ -379,7 +379,7 @@ end
|
||||
function expect_mt:str_match(pattern)
|
||||
local actual_type = type(self.value)
|
||||
if actual_type ~= "string" then
|
||||
self:_fail(("Expected value of type string\nbut got %s"):format(actual_type))
|
||||
self:_fail(("Expected value of type string\nbut got %s (of type %s)"):format(format(self.value), actual_type))
|
||||
end
|
||||
if not self.value:find(pattern) then
|
||||
self:_fail(("Expected %q\n to match pattern %q"):format(self.value, pattern))
|
||||
|
@ -129,4 +129,63 @@ describe("The parallel library", function()
|
||||
expect(exitCount):eq(3)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("exceptions", function()
|
||||
local try = require "cc.internal.exception".try
|
||||
local function check_failure(fn, ...)
|
||||
local ok, message, thread = try(fn, ...)
|
||||
expect(ok):eq(false)
|
||||
expect(message):str_match("/parallel_spec.lua:%d+: Oh no$")
|
||||
return thread
|
||||
end
|
||||
|
||||
it("throws an exception when within a try", function()
|
||||
local expected_thread
|
||||
local thread = check_failure(parallel.waitForAny, function()
|
||||
expected_thread = coroutine.running()
|
||||
error("Oh no")
|
||||
end)
|
||||
|
||||
expect(thread):eq(expected_thread)
|
||||
end)
|
||||
|
||||
it("throws an exception when within a try (nested)", function()
|
||||
local expected_thread
|
||||
local thread = check_failure(parallel.waitForAny, function()
|
||||
parallel.waitForAny(function()
|
||||
expected_thread = coroutine.running()
|
||||
error("Oh no")
|
||||
end)
|
||||
end)
|
||||
expect(thread):eq(expected_thread)
|
||||
end)
|
||||
|
||||
it("throws the raw error when within a pcall", function()
|
||||
local expected_thread
|
||||
local thread = check_failure(function()
|
||||
expected_thread = coroutine.running()
|
||||
|
||||
local ok, err = pcall(parallel.waitForAny, function() error("Oh no") end)
|
||||
expect(ok):eq(false)
|
||||
expect(err):str_match("/parallel_spec.lua:%d+: Oh no$")
|
||||
error(err, 0)
|
||||
end)
|
||||
expect(thread):eq(expected_thread)
|
||||
end)
|
||||
|
||||
it("throws the raw error when within a pcall (nested)", function()
|
||||
local expected_thread
|
||||
local thread = check_failure(function()
|
||||
expected_thread = coroutine.running()
|
||||
|
||||
local ok, err = pcall(parallel.waitForAny, function()
|
||||
parallel.waitForAny(function() error("Oh no") end)
|
||||
end)
|
||||
expect(ok):eq(false)
|
||||
expect(err):str_match("/parallel_spec.lua:%d+: Oh no$")
|
||||
error(err, 0)
|
||||
end)
|
||||
expect(thread):eq(expected_thread)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
@ -0,0 +1,37 @@
|
||||
-- SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
--
|
||||
-- SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
describe("cc.internal.error_hints", function()
|
||||
local error_hints = require "cc.internal.error_hints"
|
||||
|
||||
local function get_tip_for(code)
|
||||
local fn = assert(load(code, "=input.lua"))
|
||||
local co = coroutine.create(fn)
|
||||
local ok, err = coroutine.resume(co)
|
||||
expect(ok):eq(false)
|
||||
|
||||
local _, _, err = err:match("^([^:]+):(%d+): (.*)")
|
||||
|
||||
local tip = error_hints.get_tip(err, co, 0)
|
||||
return tip and tostring(tip) or nil
|
||||
end
|
||||
|
||||
describe("gives hints for 'attempt to OP (a nil value)' errors", function()
|
||||
it("suggests alternative globals", function()
|
||||
expect(get_tip_for("pront()")):eq("Did you mean: print?")
|
||||
end)
|
||||
|
||||
it("suggests alternative locals", function()
|
||||
expect(get_tip_for("local foo; fot()")):eq("Did you mean: foo?")
|
||||
end)
|
||||
|
||||
it("suggests alternative table keys", function()
|
||||
expect(get_tip_for("redstone.getinput()")):eq("Did you mean: getInput?")
|
||||
end)
|
||||
|
||||
it("suggests multiple table keys", function()
|
||||
expect(get_tip_for("redstone.getAnaloguInput()")):eq("Did you mean: getAnalogInput or getAnalogueInput?")
|
||||
end)
|
||||
end)
|
||||
end)
|
@ -265,3 +265,6 @@ tasks.register("checkClient") {
|
||||
modPublishing {
|
||||
output = tasks.jar
|
||||
}
|
||||
|
||||
// TODO: Remove once https://github.com/modrinth/minotaur/pull/72 is merged.
|
||||
modrinth { loaders = listOf("neoforge") }
|
||||
|
@ -51,7 +51,7 @@ public class InputState {
|
||||
|
||||
public void onCharEvent(int codepoint) {
|
||||
var terminalChar = StringUtil.unicodeToTerminal(codepoint);
|
||||
if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, terminalChar);
|
||||
if (StringUtil.isTypableChar(terminalChar)) ComputerEvents.charTyped(computer, (byte) terminalChar);
|
||||
}
|
||||
|
||||
public void onKeyEvent(long window, int key, int action, int modifiers) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user