Compare commits
20 Commits
v1.20.1-1.
...
v1.20.1-1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0a8d505323 | ||
![]() |
237a0ac3bb | ||
![]() |
b185d088b3 | ||
![]() |
051c70a731 | ||
![]() |
2e2f308ff3 | ||
![]() |
0f123b5efd | ||
![]() |
88cb03be6b | ||
![]() |
4360485880 | ||
![]() |
b69a44a927 | ||
![]() |
7d8f609c49 | ||
![]() |
e7f56c4d25 | ||
![]() |
fa2140d00b | ||
![]() |
03388149b1 | ||
![]() |
f212861370 | ||
![]() |
55edced9de | ||
![]() |
dc969c5a78 | ||
![]() |
94ad6dab0e | ||
![]() |
938eb38ad5 | ||
![]() |
6739c4c6c0 | ||
![]() |
d6749f8461 |
3
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -1,6 +1,7 @@
|
||||
name: Bug report
|
||||
description: Report some misbehaviour in the mod
|
||||
labels: [ bug ]
|
||||
type: bug
|
||||
body:
|
||||
- type: dropdown
|
||||
id: mc-version
|
||||
@@ -29,3 +30,5 @@ body:
|
||||
Description of the bug. Please include the following:
|
||||
- Logs: These will be located in the `logs/` directory of your Minecraft instance. This is always useful, even if it doesn't include errors, so please upload this!
|
||||
- Detailed reproduction steps: sometimes I can spot a bug pretty easily, but often it's much more obscure. The more information I have to help reproduce it, the quicker it'll get fixed.
|
||||
|
||||

|
||||
|
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,6 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea or improvement
|
||||
labels: enhancement
|
||||
type: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
|
@@ -10,9 +10,7 @@ import org.gradle.api.GradleException
|
||||
import org.gradle.api.NamedDomainObjectProvider
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.provider.ListProperty
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.tasks.SourceSet
|
||||
@@ -168,7 +166,7 @@ abstract class CCTweakedExtension(private val project: Project) {
|
||||
jacoco.applyTo(this)
|
||||
|
||||
extensions.configure(JacocoTaskExtension::class.java) {
|
||||
includes = listOf("dan200.computercraft.*")
|
||||
includes = listOf("dan200.computercraft.*")
|
||||
excludes = listOf(
|
||||
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
|
||||
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.
|
||||
|
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=false
|
||||
modVersion=1.114.3
|
||||
modVersion=1.115.0
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.20.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);
|
||||
}
|
||||
}
|
@@ -19,8 +19,8 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
*/
|
||||
public interface WiredElement extends WiredSender {
|
||||
/**
|
||||
* Called when objects on the network change. This may occur when network nodes are added or removed, or when
|
||||
* peripherals change.
|
||||
* Called when peripherals on the network change. This may occur when network nodes are added or removed, or when
|
||||
* peripherals are attached or detached from a modem.
|
||||
*
|
||||
* @param change The change which occurred.
|
||||
* @see WiredNetworkChange
|
||||
|
@@ -12,6 +12,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;
|
||||
@@ -75,6 +76,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;
|
||||
|
@@ -10,10 +10,10 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.network.server.ComputerActionServerMessage;
|
||||
import dan200.computercraft.shared.network.server.KeyEventServerMessage;
|
||||
import dan200.computercraft.shared.network.server.MouseEventServerMessage;
|
||||
import dan200.computercraft.shared.network.server.QueueEventServerMessage;
|
||||
import dan200.computercraft.shared.network.server.PasteEventComputerMessage;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* An {@link InputHandler} for use on the client.
|
||||
@@ -27,6 +27,11 @@ public final class ClientInputHandler implements InputHandler {
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TERMINATE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void turnOn() {
|
||||
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.TURN_ON));
|
||||
@@ -42,11 +47,6 @@ public final class ClientInputHandler implements InputHandler {
|
||||
ClientNetworking.sendToServer(new ComputerActionServerMessage(menu, ComputerActionServerMessage.Action.REBOOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
ClientNetworking.sendToServer(new QueueEventServerMessage(menu, event, arguments));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyDown(int key, boolean repeat) {
|
||||
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, repeat ? KeyEventServerMessage.Action.REPEAT : KeyEventServerMessage.Action.DOWN, key));
|
||||
@@ -57,6 +57,16 @@ public final class ClientInputHandler implements InputHandler {
|
||||
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.UP, key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void charTyped(byte chr) {
|
||||
ClientNetworking.sendToServer(new KeyEventServerMessage(menu, KeyEventServerMessage.Action.CHAR, chr));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paste(ByteBuffer contents) {
|
||||
ClientNetworking.sendToServer(new PasteEventComputerMessage(menu, contents));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClick(int button, int x, int y) {
|
||||
ClientNetworking.sendToServer(new MouseEventServerMessage(menu, MouseEventServerMessage.Action.CLICK, button, x, y));
|
||||
|
@@ -32,10 +32,13 @@ public final class GuiSprites extends TextureAtlasHolder {
|
||||
public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true);
|
||||
public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false);
|
||||
|
||||
public static final ResourceLocation TURTLE_NORMAL_SELECTED_SLOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/turtle_normal_selected_slot");
|
||||
public static final ResourceLocation TURTLE_ADVANCED_SELECTED_SLOT = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/turtle_advanced_selected_slot");
|
||||
|
||||
private static ButtonTextures button(String name) {
|
||||
return new ButtonTextures(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/buttons/" + name),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sprites/buttons/" + name + "_hover")
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -54,9 +54,9 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
if (slot >= 0) {
|
||||
var slotX = slot % 4;
|
||||
var slotY = slot / 4;
|
||||
graphics.blit(texture,
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
|
||||
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
|
||||
graphics.blit(
|
||||
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0, 22, 22,
|
||||
GuiSprites.get(advanced ? GuiSprites.TURTLE_ADVANCED_SELECTED_SLOT : GuiSprites.TURTLE_NORMAL_SELECTED_SLOT)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -55,7 +55,7 @@ public final class ComputerSidebar {
|
||||
add.accept(new DynamicImageButton(
|
||||
x, y, ICON_WIDTH, ICON_HEIGHT,
|
||||
GuiSprites.TERMINATE::get,
|
||||
b -> input.queueEvent("terminate"),
|
||||
b -> input.terminate(),
|
||||
new HintedMessage(
|
||||
Component.translatable("gui.computercraft.tooltip.terminate"),
|
||||
Component.translatable("gui.computercraft.tooltip.terminate.key")
|
||||
|
@@ -71,11 +71,8 @@ public class TerminalWidget extends AbstractWidget {
|
||||
|
||||
@Override
|
||||
public boolean charTyped(char ch, int modifiers) {
|
||||
if (ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255) {
|
||||
// Queue the char event for any printable chars in byte range
|
||||
computer.queueEvent("char", new Object[]{ Character.toString(ch) });
|
||||
}
|
||||
|
||||
var terminalChar = StringUtil.unicodeToTerminal(ch);
|
||||
if (StringUtil.isTypableChar(terminalChar)) computer.charTyped((byte) terminalChar);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,8 +109,8 @@ public class TerminalWidget extends AbstractWidget {
|
||||
}
|
||||
|
||||
private void paste() {
|
||||
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
|
||||
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
|
||||
var clipboard = StringUtil.getClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
|
||||
if (clipboard.remaining() > 0) computer.paste(clipboard);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,7 +219,7 @@ public class TerminalWidget extends AbstractWidget {
|
||||
|
||||
public void update() {
|
||||
if (terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME) {
|
||||
computer.queueEvent("terminate");
|
||||
computer.terminate();
|
||||
}
|
||||
|
||||
if (shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME) {
|
||||
|
@@ -0,0 +1,96 @@
|
||||
// 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.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.util.FastColor;
|
||||
import net.minecraft.world.inventory.InventoryMenu;
|
||||
|
||||
/**
|
||||
* A model for {@linkplain PocketComputerItem pocket computers} placed on a lectern.
|
||||
*
|
||||
* @see CustomLecternRenderer
|
||||
*/
|
||||
public class LecternPocketModel {
|
||||
public static final ResourceLocation TEXTURE_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_normal");
|
||||
public static final ResourceLocation TEXTURE_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_advanced");
|
||||
public static final ResourceLocation TEXTURE_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_colour");
|
||||
public static final ResourceLocation TEXTURE_FRAME = new ResourceLocation(ComputerCraftAPI.MOD_ID, "entity/pocket_computer_frame");
|
||||
public static final ResourceLocation TEXTURE_LIGHT = new ResourceLocation(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);
|
||||
}
|
||||
|
||||
private void render(PoseStack poseStack, VertexConsumer buffer, int packedLight, int packedOverlay, int colour) {
|
||||
int red = FastColor.ARGB32.red(colour), green = FastColor.ARGB32.green(colour), blue = FastColor.ARGB32.blue(colour), alpha = FastColor.ARGB32.alpha(colour);
|
||||
root.render(poseStack, buffer, packedLight, packedOverlay, red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 PocketComputerItem#getColour(ItemStack) 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, 1, 1, 1, 1);
|
||||
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, 1, 1, 1, 1);
|
||||
}
|
||||
|
||||
render(poseStack, MATERIAL_LIGHT.buffer(bufferSource, RenderType::entityCutout), LightTexture.FULL_BRIGHT, packedOverlay, lightColour);
|
||||
}
|
||||
}
|
@@ -6,15 +6,28 @@ 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.PrintoutItem;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.util.ARGB32;
|
||||
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.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}.
|
||||
@@ -22,10 +35,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
|
||||
@@ -44,8 +64,46 @@ public class CustomLecternRenderer implements BlockEntityRenderer<CustomLecternB
|
||||
} else {
|
||||
printoutModel.renderPages(poseStack, vertexConsumer, packedLight, packedOverlay, PrintoutItem.getPageCount(item));
|
||||
}
|
||||
} else if (item.getItem() instanceof PocketComputerItem pocket) {
|
||||
var computer = ClientPocketComputers.get(item);
|
||||
|
||||
pocketModel.render(
|
||||
poseStack, buffer, packedLight, packedOverlay, pocket.getFamily(), pocket.getColour(item),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -38,8 +38,8 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
|
||||
int termWidth, termHeight;
|
||||
if (terminal == null) {
|
||||
termWidth = Config.pocketTermWidth;
|
||||
termHeight = Config.pocketTermHeight;
|
||||
termWidth = Config.DEFAULT_POCKET_TERM_WIDTH;
|
||||
termHeight = Config.DEFAULT_POCKET_TERM_HEIGHT;
|
||||
} else {
|
||||
termWidth = terminal.getWidth();
|
||||
termHeight = terminal.getHeight();
|
||||
|
@@ -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() {
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.data;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.model.LecternPocketModel;
|
||||
import dan200.computercraft.client.model.LecternPrintoutModel;
|
||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
|
||||
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
|
||||
@@ -54,9 +55,12 @@ public final class DataProviders {
|
||||
out.accept(new ResourceLocation("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(
|
||||
Stream.of(GuiSprites.TURTLE_NORMAL_SELECTED_SLOT, GuiSprites.TURTLE_ADVANCED_SELECTED_SLOT),
|
||||
// Buttons
|
||||
GuiSprites.TURNED_OFF.textures(),
|
||||
GuiSprites.TURNED_ON.textures(),
|
||||
|
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"sources": [
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_off_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/turned_on_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/buttons/terminate_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/turtle_normal_selected_slot"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/turtle_advanced_selected_slot"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_off"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_off_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_on"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/turned_on_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/terminate"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sprites/buttons/terminate_hover"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/border_normal"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/pocket_bottom_normal"},
|
||||
{"type": "minecraft:single", "resource": "computercraft:gui/sidebar_normal"},
|
||||
|
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
@@ -12,6 +12,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;
|
||||
@@ -26,6 +27,7 @@ import dan200.computercraft.shared.computer.core.ResourceMount;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
import dan200.computercraft.shared.details.ItemDetails;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Registry;
|
||||
@@ -39,6 +41,7 @@ import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIService {
|
||||
private final DetailRegistry<ItemStack> itemStackDetails = new DetailRegistryImpl<>(ItemDetails::fillBasic);
|
||||
@@ -134,4 +137,21 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic
|
||||
public final DetailRegistry<BlockReference> getBlockInWorldDetailRegistry() {
|
||||
return blockDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PrintoutContents getPrintoutContents(ItemStack stack) {
|
||||
return stack.getItem() instanceof PrintoutItem ? new PrintoutContentsImpl(stack) : null;
|
||||
}
|
||||
|
||||
public record PrintoutContentsImpl(ItemStack stack) implements PrintoutContents {
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return PrintoutItem.getTitle(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<String> getTextLines() {
|
||||
return Stream.of(PrintoutItem.getText(stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ import dan200.computercraft.shared.computer.blocks.CommandComputerBlock;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
|
||||
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.computer.items.CommandComputerItem;
|
||||
import dan200.computercraft.shared.computer.items.ComputerItem;
|
||||
@@ -89,7 +90,6 @@ import dan200.computercraft.shared.turtle.recipes.TurtleOverlayRecipe;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
|
||||
import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
||||
@@ -471,7 +471,7 @@ public final class ModRegistry {
|
||||
|
||||
ComputerCraftAPI.registerAPIFactory(computer -> {
|
||||
var turtle = computer.getComponent(ComputerComponents.TURTLE);
|
||||
var metrics = Objects.requireNonNull(computer.getComponent(ComponentMap.METRICS));
|
||||
var metrics = Objects.requireNonNull(computer.getComponent(ServerComputer.METRICS));
|
||||
return turtle == null ? null : new TurtleAPI(metrics, (TurtleAccessInternal) turtle);
|
||||
});
|
||||
|
||||
|
@@ -24,15 +24,13 @@ import dan200.computercraft.shared.computer.metrics.basic.AggregatedMetric;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.BasicComputerMetricsObserver;
|
||||
import dan200.computercraft.shared.computer.metrics.basic.ComputerMetrics;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.RelativeMovement;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@@ -260,18 +258,11 @@ public final class CommandComputerCraft {
|
||||
* @return The constant {@code 1}.
|
||||
*/
|
||||
private static int view(CommandSourceStack source, ServerComputer computer) throws CommandSyntaxException {
|
||||
var player = source.getPlayerOrException();
|
||||
new ComputerContainerData(computer, ItemStack.EMPTY).open(player, new MenuProvider() {
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return Component.translatable("gui.computercraft.view_computer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int id, Inventory player, Player entity) {
|
||||
return new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer);
|
||||
}
|
||||
});
|
||||
PlatformHelper.get().openMenu(
|
||||
source.getPlayerOrException(), Component.translatable("gui.computercraft.view_computer"),
|
||||
(id, player, entity) -> new ComputerMenuWithoutInventory(ModRegistry.Menus.COMPUTER.get(), id, player, p -> true, computer),
|
||||
new ComputerContainerData(computer, ItemStack.EMPTY)
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,7 @@ import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
|
||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.platform.RegistryEntry;
|
||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -161,7 +162,7 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
|
||||
var serverComputer = computer.createServerComputer();
|
||||
serverComputer.turnOn();
|
||||
|
||||
new ComputerContainerData(serverComputer, getItem(computer)).open(player, computer);
|
||||
PlatformHelper.get().openMenu(player, computer.getName(), computer, new ComputerContainerData(serverComputer, getItem(computer)));
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
}
|
||||
|
@@ -26,9 +26,10 @@ import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.LockCode;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.Nameable;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.MenuConstructor;
|
||||
import net.minecraft.world.level.block.GameMasterBlock;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
@@ -38,7 +39,7 @@ import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuProvider {
|
||||
public abstract class AbstractComputerBlockEntity extends BlockEntity implements Nameable, MenuConstructor {
|
||||
private static final String NBT_ID = "ComputerId";
|
||||
private static final String NBT_LABEL = "Label";
|
||||
private static final String NBT_ON = "On";
|
||||
@@ -310,6 +311,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return label;
|
||||
}
|
||||
|
||||
public final boolean isAdminOnly() {
|
||||
return getBlockState().getBlock() instanceof GameMasterBlock;
|
||||
}
|
||||
|
||||
public final void setComputerID(int id) {
|
||||
if (getLevel().isClientSide || computerID == id) return;
|
||||
|
||||
@@ -417,4 +422,9 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
public Component getDisplayName() {
|
||||
return Nameable.super.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onlyOpCanSetNbt() {
|
||||
return isAdminOnly();
|
||||
}
|
||||
}
|
||||
|
@@ -11,8 +11,7 @@ import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import dan200.computercraft.shared.config.ConfigSpec;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -33,10 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
|
||||
|
||||
@Override
|
||||
protected ServerComputer createComputer(int id) {
|
||||
return new ServerComputer(
|
||||
(ServerLevel) getLevel(), getBlockPos(), id, label,
|
||||
getFamily(), Config.computerTermWidth, Config.computerTermHeight,
|
||||
ComponentMap.empty()
|
||||
return new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
|
||||
.label(getLabel())
|
||||
.terminalSize(ConfigSpec.computerTermWidth.get(), ConfigSpec.computerTermHeight.get())
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,6 @@ import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.apis.ComputerAccess;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.computer.ApiLifecycle;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
@@ -26,11 +25,11 @@ import java.util.Map;
|
||||
final class ComputerSystem extends ComputerAccess implements IComputerSystem, ApiLifecycle {
|
||||
private final ServerComputer computer;
|
||||
private final IAPIEnvironment environment;
|
||||
private final ComponentMap components;
|
||||
private final Map<ComputerComponent<?>, Object> components;
|
||||
|
||||
private boolean active;
|
||||
|
||||
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, ComponentMap components) {
|
||||
ComputerSystem(ServerComputer computer, IAPIEnvironment environment, Map<ComputerComponent<?>, Object> components) {
|
||||
super(environment);
|
||||
this.computer = computer;
|
||||
this.environment = environment;
|
||||
@@ -95,7 +94,8 @@ final class ComputerSystem extends ComputerAccess implements IComputerSystem, Ap
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> @Nullable T getComponent(ComputerComponent<T> component) {
|
||||
return components.get(component);
|
||||
return (T) components.get(component);
|
||||
}
|
||||
}
|
||||
|
@@ -6,44 +6,33 @@ package dan200.computercraft.shared.computer.core;
|
||||
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Handles user-provided input, forwarding it to a computer. This is used
|
||||
* Handles user-provided input, forwarding it to a computer. This describes the "shape" of both the client-and
|
||||
* server-side input handlers.
|
||||
*
|
||||
* @see ServerInputHandler
|
||||
* @see ServerComputer
|
||||
*/
|
||||
public interface InputHandler {
|
||||
void queueEvent(String event, @Nullable Object[] arguments);
|
||||
void keyDown(int key, boolean repeat);
|
||||
|
||||
default void queueEvent(String event) {
|
||||
queueEvent(event, null);
|
||||
}
|
||||
void keyUp(int key);
|
||||
|
||||
default void keyDown(int key, boolean repeat) {
|
||||
queueEvent("key", new Object[]{ key, repeat });
|
||||
}
|
||||
void charTyped(byte chr);
|
||||
|
||||
default void keyUp(int key) {
|
||||
queueEvent("key_up", new Object[]{ key });
|
||||
}
|
||||
void paste(ByteBuffer contents);
|
||||
|
||||
default void mouseClick(int button, int x, int y) {
|
||||
queueEvent("mouse_click", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseClick(int button, int x, int y);
|
||||
|
||||
default void mouseUp(int button, int x, int y) {
|
||||
queueEvent("mouse_up", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseUp(int button, int x, int y);
|
||||
|
||||
default void mouseDrag(int button, int x, int y) {
|
||||
queueEvent("mouse_drag", new Object[]{ button, x, y });
|
||||
}
|
||||
void mouseDrag(int button, int x, int y);
|
||||
|
||||
default void mouseScroll(int direction, int x, int y) {
|
||||
queueEvent("mouse_scroll", new Object[]{ direction, x, y });
|
||||
}
|
||||
void mouseScroll(int direction, int x, int y);
|
||||
|
||||
void terminate();
|
||||
|
||||
void shutdown();
|
||||
|
||||
|
@@ -6,12 +6,14 @@ package dan200.computercraft.shared.computer.core;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.component.AdminComputer;
|
||||
import dan200.computercraft.api.component.ComputerComponent;
|
||||
import dan200.computercraft.api.component.ComputerComponents;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.WorkMonitor;
|
||||
import dan200.computercraft.core.computer.Computer;
|
||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.impl.ApiFactories;
|
||||
@@ -23,18 +25,21 @@ import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.client.ClientNetworkContext;
|
||||
import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworking;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
public class ServerComputer implements ComputerEnvironment, ComputerEvents.Receiver {
|
||||
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
|
||||
|
||||
private final int instanceID;
|
||||
private final UUID instanceUUID = UUID.randomUUID();
|
||||
|
||||
@@ -50,30 +55,25 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
|
||||
private int ticksSincePing;
|
||||
|
||||
public ServerComputer(
|
||||
ServerLevel level, BlockPos position, int computerID, @Nullable String label, ComputerFamily family, int terminalWidth, int terminalHeight,
|
||||
ComponentMap baseComponents
|
||||
) {
|
||||
public ServerComputer(ServerLevel level, BlockPos position, Properties properties) {
|
||||
this.level = level;
|
||||
this.position = position;
|
||||
this.family = family;
|
||||
this.family = properties.family;
|
||||
|
||||
var context = ServerContext.get(level.getServer());
|
||||
instanceID = context.registry().getUnusedInstanceID();
|
||||
terminal = new NetworkedTerminal(terminalWidth, terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
|
||||
terminal = new NetworkedTerminal(properties.terminalWidth, properties.terminalHeight, family != ComputerFamily.NORMAL, this::markTerminalChanged);
|
||||
metrics = context.metrics().createMetricObserver(this);
|
||||
|
||||
var componentBuilder = ComponentMap.builder();
|
||||
componentBuilder.add(ComponentMap.METRICS, metrics);
|
||||
properties.addComponent(METRICS, metrics);
|
||||
if (family == ComputerFamily.COMMAND) {
|
||||
componentBuilder.add(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
|
||||
properties.addComponent(ComputerComponents.ADMIN_COMPUTER, new AdminComputer() {
|
||||
});
|
||||
}
|
||||
componentBuilder.add(baseComponents);
|
||||
var components = componentBuilder.build();
|
||||
var components = Map.copyOf(properties.components);
|
||||
|
||||
computer = new Computer(context.computerContext(), this, terminal, computerID);
|
||||
computer.setLabel(label);
|
||||
computer = new Computer(context.computerContext(), this, terminal, properties.computerID);
|
||||
computer.setLabel(properties.label);
|
||||
|
||||
// Load in the externally registered APIs.
|
||||
for (var factory : ApiFactories.getAll()) {
|
||||
@@ -86,24 +86,24 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
public ComputerFamily getFamily() {
|
||||
public final ComputerFamily getFamily() {
|
||||
return family;
|
||||
}
|
||||
|
||||
public ServerLevel getLevel() {
|
||||
public final ServerLevel getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public BlockPos getPosition() {
|
||||
public final BlockPos getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(ServerLevel level, BlockPos pos) {
|
||||
public final void setPosition(ServerLevel level, BlockPos pos) {
|
||||
this.level = level;
|
||||
position = pos.immutable();
|
||||
}
|
||||
|
||||
protected void markTerminalChanged() {
|
||||
protected final void markTerminalChanged() {
|
||||
terminalChanged.set(true);
|
||||
}
|
||||
|
||||
@@ -117,11 +117,11 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
sendToAllInteracting(c -> new ComputerTerminalClientMessage(c, getTerminalState()));
|
||||
}
|
||||
|
||||
public TerminalState getTerminalState() {
|
||||
public final TerminalState getTerminalState() {
|
||||
return TerminalState.create(terminal);
|
||||
}
|
||||
|
||||
public void keepAlive() {
|
||||
public final void keepAlive() {
|
||||
ticksSincePing = 0;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
*
|
||||
* @return What sides on the computer have changed.
|
||||
*/
|
||||
public int pollRedstoneChanges() {
|
||||
public final int pollRedstoneChanges() {
|
||||
return computer.pollRedstoneChanges();
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
computer.unload();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
public final void close() {
|
||||
unload();
|
||||
ServerContext.get(level.getServer()).registry().remove(this);
|
||||
}
|
||||
@@ -167,7 +167,7 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
var server = level.getServer();
|
||||
|
||||
for (var player : server.getPlayerList().getPlayers()) {
|
||||
if (player.containerMenu instanceof ComputerMenu && ((ComputerMenu) player.containerMenu).getComputer() == this) {
|
||||
if (player.containerMenu instanceof ComputerMenu menu && menu.getComputer() == this) {
|
||||
ServerNetworking.sendToPlayer(createPacket.apply(player.containerMenu), player);
|
||||
}
|
||||
}
|
||||
@@ -176,97 +176,135 @@ public class ServerComputer implements InputHandler, ComputerEnvironment {
|
||||
protected void onRemoved() {
|
||||
}
|
||||
|
||||
public int getInstanceID() {
|
||||
public final int getInstanceID() {
|
||||
return instanceID;
|
||||
}
|
||||
|
||||
public UUID getInstanceUUID() {
|
||||
public final UUID getInstanceUUID() {
|
||||
return instanceUUID;
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
public final int getID() {
|
||||
return computer.getID();
|
||||
}
|
||||
|
||||
public @Nullable String getLabel() {
|
||||
public final @Nullable String getLabel() {
|
||||
return computer.getLabel();
|
||||
}
|
||||
|
||||
public boolean isOn() {
|
||||
public final boolean isOn() {
|
||||
return computer.isOn();
|
||||
}
|
||||
|
||||
public ComputerState getState() {
|
||||
public final ComputerState getState() {
|
||||
if (!computer.isOn()) return ComputerState.OFF;
|
||||
return computer.isBlinking() ? ComputerState.BLINKING : ComputerState.ON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void turnOn() {
|
||||
public final void turnOn() {
|
||||
computer.turnOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
public final void shutdown() {
|
||||
computer.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reboot() {
|
||||
public final void reboot() {
|
||||
computer.reboot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
public final void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
computer.queueEvent(event, arguments);
|
||||
}
|
||||
|
||||
public int getRedstoneOutput(ComputerSide side) {
|
||||
public final void queueEvent(String event) {
|
||||
queueEvent(event, null);
|
||||
}
|
||||
|
||||
public final int getRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? computer.getRedstone().getExternalOutput(side) : 0;
|
||||
}
|
||||
|
||||
public void setRedstoneInput(ComputerSide side, int level, int bundledState) {
|
||||
public final void setRedstoneInput(ComputerSide side, int level, int bundledState) {
|
||||
computer.getRedstone().setInput(side, level, bundledState);
|
||||
}
|
||||
|
||||
public int getBundledRedstoneOutput(ComputerSide side) {
|
||||
public final int getBundledRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? computer.getRedstone().getExternalBundledOutput(side) : 0;
|
||||
}
|
||||
|
||||
public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
|
||||
public final void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
|
||||
computer.getEnvironment().setPeripheral(side, peripheral);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IPeripheral getPeripheral(ComputerSide side) {
|
||||
public final IPeripheral getPeripheral(ComputerSide side) {
|
||||
return computer.getEnvironment().getPeripheral(side);
|
||||
}
|
||||
|
||||
public void setLabel(@Nullable String label) {
|
||||
public final void setLabel(@Nullable String label) {
|
||||
computer.setLabel(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTimeOfDay() {
|
||||
public final double getTimeOfDay() {
|
||||
return (level.getDayTime() + 6000) % 24000 / 1000.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDay() {
|
||||
public final int getDay() {
|
||||
return (int) ((level.getDayTime() + 6000) / 24000) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricsObserver getMetrics() {
|
||||
public final MetricsObserver getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public WorkMonitor getMainThreadMonitor() {
|
||||
public final WorkMonitor getMainThreadMonitor() {
|
||||
return computer.getMainThreadMonitor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable WritableMount createRootMount() {
|
||||
public final WritableMount createRootMount() {
|
||||
return ComputerCraftAPI.createSaveDirMount(level.getServer(), "computer/" + computer.getID(), Config.computerSpaceLimit);
|
||||
}
|
||||
|
||||
public static Properties properties(int computerID, ComputerFamily family) {
|
||||
return new Properties(computerID, family);
|
||||
}
|
||||
|
||||
public static final class Properties {
|
||||
private final int computerID;
|
||||
private @Nullable String label;
|
||||
private final ComputerFamily family;
|
||||
|
||||
private int terminalWidth = Config.DEFAULT_COMPUTER_TERM_WIDTH;
|
||||
private int terminalHeight = Config.DEFAULT_COMPUTER_TERM_HEIGHT;
|
||||
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
|
||||
|
||||
private Properties(int computerID, ComputerFamily family) {
|
||||
this.computerID = computerID;
|
||||
this.family = family;
|
||||
}
|
||||
|
||||
public Properties label(@Nullable String label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Properties terminalSize(int width, int height) {
|
||||
if (width <= 0 || height <= 0) throw new IllegalArgumentException("Terminal size must be positive");
|
||||
this.terminalWidth = width;
|
||||
this.terminalHeight = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> Properties addComponent(ComputerComponent<T> component, T value) {
|
||||
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
|
||||
components.put(component, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ package dan200.computercraft.shared.computer.menu;
|
||||
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||
import dan200.computercraft.shared.computer.upload.UploadResult;
|
||||
@@ -21,6 +23,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -48,21 +51,33 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] arguments) {
|
||||
owner.getComputer().queueEvent(event, arguments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyDown(int key, boolean repeat) {
|
||||
keysDown.add(key);
|
||||
owner.getComputer().keyDown(key, repeat);
|
||||
ComputerEvents.keyDown(owner.getComputer(), key, repeat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyUp(int key) {
|
||||
keysDown.remove(key);
|
||||
owner.getComputer().keyUp(key);
|
||||
ComputerEvents.keyUp(owner.getComputer(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void charTyped(byte chr) {
|
||||
if (StringUtil.isTypableChar(chr)) ComputerEvents.charTyped(owner.getComputer(), chr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paste(ByteBuffer contents) {
|
||||
if (contents.remaining() > 0 && isValidClipboard(contents)) ComputerEvents.paste(owner.getComputer(), contents);
|
||||
}
|
||||
|
||||
private static boolean isValidClipboard(ByteBuffer buffer) {
|
||||
for (int i = buffer.remaining(), max = buffer.limit(); i < max; i++) {
|
||||
if (!StringUtil.isTypableChar(buffer.get(i))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,7 +86,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = button;
|
||||
|
||||
owner.getComputer().mouseClick(button, x, y);
|
||||
ComputerEvents.mouseClick(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,7 +95,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = -1;
|
||||
|
||||
owner.getComputer().mouseUp(button, x, y);
|
||||
ComputerEvents.mouseUp(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +104,7 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseY = y;
|
||||
lastMouseDown = button;
|
||||
|
||||
owner.getComputer().mouseDrag(button, x, y);
|
||||
ComputerEvents.mouseDrag(owner.getComputer(), button, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,7 +112,12 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
lastMouseX = x;
|
||||
lastMouseY = y;
|
||||
|
||||
owner.getComputer().mouseScroll(direction, x, y);
|
||||
ComputerEvents.mouseScroll(owner.getComputer(), direction, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
owner.getComputer().queueEvent("terminate");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,9 +189,9 @@ public class ServerInputState<T extends AbstractContainerMenu & ComputerMenu> im
|
||||
public void close() {
|
||||
var computer = owner.getComputer();
|
||||
var keys = keysDown.iterator();
|
||||
while (keys.hasNext()) computer.keyUp(keys.nextInt());
|
||||
while (keys.hasNext()) ComputerEvents.keyUp(computer, keys.nextInt());
|
||||
|
||||
if (lastMouseDown != -1) computer.mouseUp(lastMouseDown, lastMouseX, lastMouseY);
|
||||
if (lastMouseDown != -1) ComputerEvents.mouseUp(computer, lastMouseDown, lastMouseX, lastMouseY);
|
||||
|
||||
keysDown.clear();
|
||||
lastMouseDown = -1;
|
||||
|
@@ -32,14 +32,14 @@ public final class Config {
|
||||
public static int advancedTurtleFuelLimit = 100000;
|
||||
public static boolean turtlesCanPush = true;
|
||||
|
||||
public static int computerTermWidth = 51;
|
||||
public static int computerTermHeight = 19;
|
||||
public static final int DEFAULT_COMPUTER_TERM_WIDTH = 51;
|
||||
public static final int DEFAULT_COMPUTER_TERM_HEIGHT = 19;
|
||||
|
||||
public static final int turtleTermWidth = 39;
|
||||
public static final int turtleTermHeight = 13;
|
||||
public static final int TURTLE_TERM_WIDTH = 39;
|
||||
public static final int TURTLE_TERM_HEIGHT = 13;
|
||||
|
||||
public static int pocketTermWidth = 26;
|
||||
public static int pocketTermHeight = 20;
|
||||
public static final int DEFAULT_POCKET_TERM_WIDTH = 26;
|
||||
public static final int DEFAULT_POCKET_TERM_HEIGHT = 20;
|
||||
|
||||
public static int monitorWidth = 8;
|
||||
public static int monitorHeight = 6;
|
||||
|
@@ -344,13 +344,13 @@ public final class ConfigSpec {
|
||||
.push("term_sizes");
|
||||
|
||||
builder.comment("Terminal size of computers.").push("computer");
|
||||
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.computerTermWidth, 1, 255);
|
||||
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.computerTermHeight, 1, 255);
|
||||
computerTermWidth = builder.comment("Width of computer terminal").defineInRange("width", Config.DEFAULT_COMPUTER_TERM_WIDTH, 1, 255);
|
||||
computerTermHeight = builder.comment("Height of computer terminal").defineInRange("height", Config.DEFAULT_COMPUTER_TERM_HEIGHT, 1, 255);
|
||||
builder.pop();
|
||||
|
||||
builder.comment("Terminal size of pocket computers.").push("pocket_computer");
|
||||
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.pocketTermWidth, 1, 255);
|
||||
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.pocketTermHeight, 1, 255);
|
||||
pocketTermWidth = builder.comment("Width of pocket computer terminal").defineInRange("width", Config.DEFAULT_POCKET_TERM_WIDTH, 1, 255);
|
||||
pocketTermHeight = builder.comment("Height of pocket computer terminal").defineInRange("height", Config.DEFAULT_POCKET_TERM_HEIGHT, 1, 255);
|
||||
builder.pop();
|
||||
|
||||
builder.comment("Maximum size of monitors (in blocks).").push("monitor");
|
||||
@@ -437,10 +437,6 @@ public final class ConfigSpec {
|
||||
Config.turtlesCanPush = turtlesCanPush.get();
|
||||
|
||||
// Terminal size
|
||||
Config.computerTermWidth = computerTermWidth.get();
|
||||
Config.computerTermHeight = computerTermHeight.get();
|
||||
Config.pocketTermWidth = pocketTermWidth.get();
|
||||
Config.pocketTermHeight = pocketTermHeight.get();
|
||||
Config.monitorWidth = monitorWidth.get();
|
||||
Config.monitorHeight = monitorHeight.get();
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.shared.lectern;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.util.BlockEntityHelpers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.stats.Stats;
|
||||
@@ -14,6 +15,7 @@ import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
@@ -21,8 +23,12 @@ import net.minecraft.world.level.BlockGetter;
|
||||
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.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}.
|
||||
@@ -55,6 +61,27 @@ public class CustomLecternBlock extends LecternBlock {
|
||||
if (level.getBlockEntity(pos) instanceof CustomLecternBlockEntity be) be.setItem(item.split(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of {@link Item#useOn(UseOnContext)} for items that can be placed on a lectern.
|
||||
*
|
||||
* @param context The context of this item usage action.
|
||||
* @return Whether the item was placed or not.
|
||||
*/
|
||||
public static InteractionResult defaultUseItemOn(UseOnContext context) {
|
||||
var level = context.getLevel();
|
||||
var blockPos = context.getClickedPos();
|
||||
var blockState = level.getBlockState(blockPos);
|
||||
if (blockState.is(Blocks.LECTERN) && !blockState.getValue(LecternBlock.HAS_BOOK)) {
|
||||
// If we have an empty lectern, place our book into it.
|
||||
if (!level.isClientSide) {
|
||||
CustomLecternBlock.replaceLectern(level, blockPos, blockState, context.getItemInHand());
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
} else {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom lectern and replace it with an empty vanilla one.
|
||||
*
|
||||
@@ -131,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);
|
||||
@@ -139,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();
|
||||
}
|
||||
|
@@ -9,27 +9,25 @@ import dan200.computercraft.shared.container.BasicContainer;
|
||||
import dan200.computercraft.shared.container.SingleContainerData;
|
||||
import dan200.computercraft.shared.media.PrintoutMenu;
|
||||
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.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;
|
||||
@@ -39,7 +37,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";
|
||||
|
||||
@@ -81,6 +79,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.
|
||||
*
|
||||
@@ -123,24 +127,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_LIMIT),
|
||||
player.openMenu(new SimpleMenuProvider((id, inventory, entity) -> new PrintoutMenu(
|
||||
id, new LecternContainer(), 0,
|
||||
p -> Container.stillValidBlockEntity(this, p, Container.DEFAULT_DISTANCE_LIMIT),
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -19,8 +19,6 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
@@ -56,18 +54,7 @@ public class PrintoutItem extends Item {
|
||||
|
||||
@Override
|
||||
public InteractionResult useOn(UseOnContext context) {
|
||||
var level = context.getLevel();
|
||||
var blockPos = context.getClickedPos();
|
||||
var blockState = level.getBlockState(blockPos);
|
||||
if (blockState.is(Blocks.LECTERN) && !blockState.getValue(LecternBlock.HAS_BOOK)) {
|
||||
// If we have an empty lectern, place our book into it.
|
||||
if (!level.isClientSide) {
|
||||
CustomLecternBlock.replaceLectern(level, blockPos, blockState, context.getItemInHand());
|
||||
}
|
||||
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||
} else {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
return CustomLecternBlock.defaultUseItemOn(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -27,9 +27,9 @@ public final class NetworkMessages {
|
||||
private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
|
||||
|
||||
public static final MessageType<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new);
|
||||
public static final MessageType<QueueEventServerMessage> QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new);
|
||||
public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
|
||||
public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new);
|
||||
public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(1, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
|
||||
public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(2, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new);
|
||||
public static final MessageType<PasteEventComputerMessage> PASTE_EVENT = registerServerbound(3, "paste_event", PasteEventComputerMessage.class, PasteEventComputerMessage::new);
|
||||
public static final MessageType<UploadFileMessage> UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new);
|
||||
|
||||
public static final MessageType<ChatTableClientMessage> CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new);
|
||||
|
@@ -7,9 +7,7 @@ package dan200.computercraft.shared.network.container;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
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;
|
||||
|
||||
@@ -21,16 +19,6 @@ import java.util.function.Function;
|
||||
public interface ContainerData {
|
||||
void toBytes(FriendlyByteBuf buf);
|
||||
|
||||
/**
|
||||
* Open a menu for a specific player using this data.
|
||||
*
|
||||
* @param player The player to open the menu for.
|
||||
* @param menu The underlying menu provider.
|
||||
*/
|
||||
default void open(Player player, MenuProvider menu) {
|
||||
PlatformHelper.get().openMenu(player, menu, this);
|
||||
}
|
||||
|
||||
static <C extends AbstractContainerMenu, T extends ContainerData> MenuType<C> toType(Function<FriendlyByteBuf, T> reader, Factory<C, T> factory) {
|
||||
return PlatformHelper.get().createMenuType(reader, factory);
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
switch (action) {
|
||||
case TERMINATE -> container.getInput().terminate();
|
||||
case TURN_ON -> container.getInput().turnOn();
|
||||
case REBOOT -> container.getInput().reboot();
|
||||
case SHUTDOWN -> container.getInput().shutdown();
|
||||
@@ -45,6 +46,7 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
TERMINATE,
|
||||
TURN_ON,
|
||||
SHUTDOWN,
|
||||
REBOOT
|
||||
|
@@ -35,8 +35,8 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
|
||||
@Override
|
||||
public void handle(ServerNetworkContext context) {
|
||||
Player player = context.getSender();
|
||||
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu) {
|
||||
handle(context, (ComputerMenu) player.containerMenu);
|
||||
if (player.containerMenu.containerId == containerId && player.containerMenu instanceof ComputerMenu menu) {
|
||||
handle(context, menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -37,10 +37,11 @@ public class KeyEventServerMessage extends ComputerServerMessage {
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
var input = container.getInput();
|
||||
if (type == Action.UP) {
|
||||
input.keyUp(key);
|
||||
} else {
|
||||
input.keyDown(key, type == Action.REPEAT);
|
||||
switch (type) {
|
||||
case UP -> input.keyUp(key);
|
||||
case DOWN -> input.keyDown(key, false);
|
||||
case REPEAT -> input.keyDown(key, true);
|
||||
case CHAR -> input.charTyped((byte) key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +51,6 @@ public class KeyEventServerMessage extends ComputerServerMessage {
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
DOWN, REPEAT, UP
|
||||
DOWN, REPEAT, UP, CHAR
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.network.server;
|
||||
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
import dan200.computercraft.shared.network.MessageType;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Paste a string on a {@link ServerComputer}.
|
||||
*
|
||||
* @see ServerInputHandler#paste(ByteBuffer)
|
||||
*/
|
||||
public class PasteEventComputerMessage extends ComputerServerMessage {
|
||||
private final ByteBuffer text;
|
||||
|
||||
public PasteEventComputerMessage(AbstractContainerMenu menu, ByteBuffer text) {
|
||||
super(menu);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public PasteEventComputerMessage(FriendlyByteBuf buf) {
|
||||
super(buf);
|
||||
|
||||
var length = buf.readVarInt();
|
||||
if (length > StringUtil.MAX_PASTE_LENGTH) {
|
||||
throw new DecoderException("ByteArray with size " + length + " is bigger than allowed " + StringUtil.MAX_PASTE_LENGTH);
|
||||
}
|
||||
|
||||
var text = new byte[length];
|
||||
buf.readBytes(text);
|
||||
this.text = ByteBuffer.wrap(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
super.write(buf);
|
||||
buf.writeVarInt(text.remaining());
|
||||
buf.writeBytes(text.duplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
container.getInput().paste(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType<PasteEventComputerMessage> type() {
|
||||
return NetworkMessages.PASTE_EVENT;
|
||||
}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.network.server;
|
||||
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.menu.ComputerMenu;
|
||||
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
|
||||
import dan200.computercraft.shared.network.MessageType;
|
||||
import dan200.computercraft.shared.network.NetworkMessages;
|
||||
import dan200.computercraft.shared.util.NBTUtil;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Queue an event on a {@link ServerComputer}.
|
||||
*
|
||||
* @see ServerInputHandler#queueEvent(String)
|
||||
*/
|
||||
public class QueueEventServerMessage extends ComputerServerMessage {
|
||||
private final String event;
|
||||
private final @Nullable Object[] args;
|
||||
|
||||
public QueueEventServerMessage(AbstractContainerMenu menu, String event, @Nullable Object[] args) {
|
||||
super(menu);
|
||||
this.event = event;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public QueueEventServerMessage(FriendlyByteBuf buf) {
|
||||
super(buf);
|
||||
event = buf.readUtf(Short.MAX_VALUE);
|
||||
|
||||
var args = buf.readNbt();
|
||||
this.args = args == null ? null : NBTUtil.decodeObjects(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
super.write(buf);
|
||||
buf.writeUtf(event);
|
||||
buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(ServerNetworkContext context, ComputerMenu container) {
|
||||
container.getInput().queueEvent(event, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType<QueueEventServerMessage> type() {
|
||||
return NetworkMessages.QUEUE_EVENT;
|
||||
}
|
||||
}
|
@@ -18,6 +18,7 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
@@ -237,7 +238,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
getLevel().setBlock(getBlockPos(), getBlockState()
|
||||
.setValue(MonitorBlock.STATE, MonitorEdgeState.fromConnections(
|
||||
yIndex < height - 1, yIndex > 0,
|
||||
xIndex > 0, xIndex < width - 1)), 2);
|
||||
xIndex > 0, xIndex < width - 1)), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
|
||||
// region Sizing and placement stuff
|
||||
|
@@ -20,6 +20,7 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
@@ -29,11 +30,15 @@ import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ServerPlayerGameMode;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.WorldlyContainer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.inventory.MenuConstructor;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
@@ -159,10 +164,11 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
|
||||
* Open a container using a specific {@link ContainerData}.
|
||||
*
|
||||
* @param player The player to open the menu for.
|
||||
* @param owner The underlying menu provider.
|
||||
* @param menu The menu data.
|
||||
* @param title The title for this menu.
|
||||
* @param menu The underlying menu constructor.
|
||||
* @param data The menu data.
|
||||
*/
|
||||
void openMenu(Player player, MenuProvider owner, ContainerData menu);
|
||||
void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data);
|
||||
|
||||
/**
|
||||
* Create a new {@link MessageType}.
|
||||
|
@@ -10,7 +10,7 @@ import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworking;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
@@ -44,8 +44,8 @@ public final class PocketBrain implements IPocketAccess {
|
||||
private int colour = -1;
|
||||
private int lightColour = -1;
|
||||
|
||||
public PocketBrain(PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
|
||||
this.computer = new PocketServerComputer(this, holder, computerID, label, family);
|
||||
public PocketBrain(PocketHolder holder, @Nullable UpgradeData<IPocketUpgrade> upgrade, ServerComputer.Properties properties) {
|
||||
this.computer = new PocketServerComputer(this, holder, properties);
|
||||
this.holder = holder;
|
||||
this.position = holder.pos();
|
||||
this.upgrade = UpgradeData.copyOf(upgrade);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,15 +5,13 @@
|
||||
package dan200.computercraft.shared.pocket.core;
|
||||
|
||||
import dan200.computercraft.api.component.ComputerComponents;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ComputerState;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.config.ConfigSpec;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
|
||||
import dan200.computercraft.shared.network.client.PocketComputerDeletedClientMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworking;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
@@ -41,10 +39,10 @@ public final class PocketServerComputer extends ServerComputer {
|
||||
|
||||
private Set<ServerPlayer> tracking = Set.of();
|
||||
|
||||
PocketServerComputer(PocketBrain brain, PocketHolder holder, int computerID, @Nullable String label, ComputerFamily family) {
|
||||
super(
|
||||
holder.level(), holder.blockPos(), computerID, label, family, Config.pocketTermWidth, Config.pocketTermHeight,
|
||||
ComponentMap.builder().add(ComputerComponents.POCKET, brain).build()
|
||||
PocketServerComputer(PocketBrain brain, PocketHolder holder, ServerComputer.Properties properties) {
|
||||
super(holder.level(), holder.blockPos(), properties
|
||||
.terminalSize(ConfigSpec.pocketTermWidth.get(), ConfigSpec.pocketTermHeight.get())
|
||||
.addComponent(ComputerComponents.POCKET, brain)
|
||||
);
|
||||
this.brain = brain;
|
||||
}
|
||||
@@ -74,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,56 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.pocket.inventory;
|
||||
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
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.item.ItemStack;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
|
||||
public class PocketComputerMenuProvider implements MenuProvider {
|
||||
private final ServerComputer computer;
|
||||
private final Component name;
|
||||
private final PocketComputerItem item;
|
||||
private final InteractionHand hand;
|
||||
private final boolean isTypingOnly;
|
||||
|
||||
public PocketComputerMenuProvider(ServerComputer computer, ItemStack stack, PocketComputerItem item, InteractionHand hand, boolean isTypingOnly) {
|
||||
this.computer = computer;
|
||||
name = stack.getHoverName();
|
||||
this.item = item;
|
||||
this.hand = hand;
|
||||
this.isTypingOnly = isTypingOnly;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Component getDisplayName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public AbstractContainerMenu createMenu(int id, Inventory inventory, Player entity) {
|
||||
return new ComputerMenuWithoutInventory(
|
||||
isTypingOnly ? ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get() : ModRegistry.Menus.COMPUTER.get(), id, inventory,
|
||||
p -> {
|
||||
var stack = p.getItemInHand(hand);
|
||||
return stack.getItem() == item && PocketComputerItem.getServerComputer(assertNonNull(entity.level().getServer()), stack) == computer;
|
||||
},
|
||||
computer
|
||||
);
|
||||
}
|
||||
}
|
@@ -12,18 +12,21 @@ import dan200.computercraft.api.pocket.IPocketUpgrade;
|
||||
import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputerRegistry;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import dan200.computercraft.shared.computer.inventory.ComputerMenuWithoutInventory;
|
||||
import dan200.computercraft.shared.computer.items.IComputerItem;
|
||||
import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.lectern.CustomLecternBlock;
|
||||
import dan200.computercraft.shared.network.container.ComputerContainerData;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import dan200.computercraft.shared.pocket.core.PocketBrain;
|
||||
import dan200.computercraft.shared.pocket.core.PocketHolder;
|
||||
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
|
||||
import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider;
|
||||
import dan200.computercraft.shared.util.IDAssigner;
|
||||
import dan200.computercraft.shared.util.InventoryUtil;
|
||||
import dan200.computercraft.shared.util.NBTUtil;
|
||||
@@ -42,6 +45,7 @@ import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -79,12 +83,20 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
/**
|
||||
* 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();
|
||||
@@ -137,11 +149,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
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
|
||||
@@ -151,12 +159,16 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult useOn(UseOnContext context) {
|
||||
return CustomLecternBlock.defaultUseItemOn(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResultHolder<ItemStack> use(Level world, Player player, InteractionHand hand) {
|
||||
var stack = player.getItemInHand(hand);
|
||||
@@ -169,20 +181,39 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
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) {
|
||||
var isTypingOnly = hand == InteractionHand.OFF_HAND;
|
||||
new ComputerContainerData(computer, stack).open(player, new PocketComputerMenuProvider(computer, stack, this, hand, isTypingOnly));
|
||||
}
|
||||
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);
|
||||
@@ -226,7 +257,11 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
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 = getComputerID(stack);
|
||||
@@ -235,15 +270,17 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
|
||||
setComputerID(stack, computerID);
|
||||
}
|
||||
|
||||
var brain = new PocketBrain(holder, getComputerID(stack), getLabel(stack), getFamily(), getUpgradeWithData(stack));
|
||||
var brain = new PocketBrain(
|
||||
holder, getUpgradeWithData(stack),
|
||||
ServerComputer.properties(getComputerID(stack), getFamily()).label(getLabel(stack))
|
||||
);
|
||||
var computer = brain.computer();
|
||||
|
||||
var tag = stack.getOrCreateTag();
|
||||
tag.putInt(NBT_SESSION, registry.getSessionID());
|
||||
tag.putUUID(NBT_INSTANCE, 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);
|
||||
|
||||
|
@@ -20,7 +20,6 @@ import dan200.computercraft.shared.config.Config;
|
||||
import dan200.computercraft.shared.container.BasicContainer;
|
||||
import dan200.computercraft.shared.turtle.core.TurtleBrain;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import dan200.computercraft.shared.util.ComponentMap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.NonNullList;
|
||||
@@ -74,10 +73,10 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
|
||||
|
||||
@Override
|
||||
protected ServerComputer createComputer(int id) {
|
||||
var computer = new ServerComputer(
|
||||
(ServerLevel) getLevel(), getBlockPos(), id, label,
|
||||
getFamily(), Config.turtleTermWidth, Config.turtleTermHeight,
|
||||
ComponentMap.builder().add(ComputerComponents.TURTLE, brain).build()
|
||||
var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
|
||||
.label(getLabel())
|
||||
.terminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT)
|
||||
.addComponent(ComputerComponents.TURTLE, brain)
|
||||
);
|
||||
brain.setupComputer(computer);
|
||||
return computer;
|
||||
|
@@ -36,6 +36,7 @@ import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.material.PushReaction;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
@@ -280,7 +281,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
try {
|
||||
// We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes
|
||||
// are sent to the client. We want to avoid doing a full block update until the turtle state is copied over.
|
||||
if (world.setBlock(pos, newState, 2)) {
|
||||
if (world.setBlock(pos, newState, Block.UPDATE_CLIENTS)) {
|
||||
var block = world.getBlockState(pos).getBlock();
|
||||
if (block == oldBlock.getBlock()) {
|
||||
var newTile = world.getBlockEntity(pos);
|
||||
@@ -691,7 +692,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
}
|
||||
|
||||
var aabb = new AABB(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
var list = world.getEntitiesOfClass(Entity.class, aabb, TurtleBrain::canPush);
|
||||
var list = world.getEntities((Entity) null, aabb, TurtleBrain::canPush);
|
||||
if (!list.isEmpty()) {
|
||||
double pushStep = 1.0f / ANIM_DURATION;
|
||||
var pushStepX = moveDir.getStepX() * pushStep;
|
||||
|
@@ -22,11 +22,10 @@ public class TurtleDetectCommand implements TurtleCommand {
|
||||
var direction = this.direction.toWorldDir(turtle);
|
||||
|
||||
// Check if thing in front is air or not
|
||||
var world = turtle.getLevel();
|
||||
var oldPosition = turtle.getPosition();
|
||||
var newPosition = oldPosition.relative(direction);
|
||||
var level = turtle.getLevel();
|
||||
var pos = turtle.getPosition().relative(direction);
|
||||
|
||||
return !WorldUtil.isLiquidBlock(world, newPosition) && !world.isEmptyBlock(newPosition)
|
||||
return !WorldUtil.isEmptyBlock(level.getBlockState(pos))
|
||||
? TurtleCommandResult.success()
|
||||
: TurtleCommandResult.failure();
|
||||
}
|
||||
|
@@ -13,9 +13,9 @@ import dan200.computercraft.shared.util.WorldUtil;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.material.PushReaction;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.shapes.Shapes;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
public class TurtleMoveCommand implements TurtleCommand {
|
||||
private final MoveDirection direction;
|
||||
@@ -30,57 +30,32 @@ public class TurtleMoveCommand implements TurtleCommand {
|
||||
var direction = this.direction.toWorldDir(turtle);
|
||||
|
||||
// Check if we can move
|
||||
var oldWorld = (ServerLevel) turtle.getLevel();
|
||||
var level = (ServerLevel) turtle.getLevel();
|
||||
var oldPosition = turtle.getPosition();
|
||||
var newPosition = oldPosition.relative(direction);
|
||||
|
||||
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, oldPosition, direction);
|
||||
var canEnterResult = canEnter(turtlePlayer, oldWorld, newPosition);
|
||||
if (!canEnterResult.isSuccess()) {
|
||||
return canEnterResult;
|
||||
}
|
||||
var canEnterResult = canEnter(turtlePlayer, level, newPosition);
|
||||
if (!canEnterResult.isSuccess()) return canEnterResult;
|
||||
|
||||
// Check existing block is air or replaceable
|
||||
var state = oldWorld.getBlockState(newPosition);
|
||||
if (!oldWorld.isEmptyBlock(newPosition) &&
|
||||
!WorldUtil.isLiquidBlock(oldWorld, newPosition) &&
|
||||
!state.canBeReplaced()) {
|
||||
// Check existing block is air or replaceable.
|
||||
var existingState = level.getBlockState(newPosition);
|
||||
if (!(WorldUtil.isEmptyBlock(existingState) || existingState.canBeReplaced())) {
|
||||
return TurtleCommandResult.failure("Movement obstructed");
|
||||
}
|
||||
|
||||
// Check there isn't anything in the way
|
||||
var collision = state.getCollisionShape(oldWorld, oldPosition).move(
|
||||
newPosition.getX(),
|
||||
newPosition.getY(),
|
||||
newPosition.getZ()
|
||||
);
|
||||
|
||||
if (!oldWorld.isUnobstructed(null, collision)) {
|
||||
if (!Config.turtlesCanPush || this.direction == MoveDirection.UP || this.direction == MoveDirection.DOWN) {
|
||||
return TurtleCommandResult.failure("Movement obstructed");
|
||||
}
|
||||
|
||||
// Check there is space for all the pushable entities to be pushed
|
||||
var list = oldWorld.getEntitiesOfClass(Entity.class, getBox(collision), x -> x != null && x.isAlive() && x.blocksBuilding);
|
||||
for (var entity : list) {
|
||||
var pushedBB = entity.getBoundingBox().move(
|
||||
direction.getStepX(),
|
||||
direction.getStepY(),
|
||||
direction.getStepZ()
|
||||
);
|
||||
if (!oldWorld.isUnobstructed(null, Shapes.create(pushedBB))) {
|
||||
return TurtleCommandResult.failure("Movement obstructed");
|
||||
}
|
||||
}
|
||||
// Check there isn't an entity in the way.
|
||||
var turtleShape = level.getBlockState(oldPosition).getCollisionShape(level, oldPosition)
|
||||
.move(newPosition.getX(), newPosition.getY(), newPosition.getZ());
|
||||
if (!level.isUnobstructed(null, turtleShape) && !canPushEntities(level, turtleShape.bounds())) {
|
||||
return TurtleCommandResult.failure("Movement obstructed");
|
||||
}
|
||||
|
||||
// Check fuel level
|
||||
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) {
|
||||
return TurtleCommandResult.failure("Out of fuel");
|
||||
}
|
||||
if (turtle.isFuelNeeded() && turtle.getFuelLevel() < 1) return TurtleCommandResult.failure("Out of fuel");
|
||||
|
||||
// Move
|
||||
if (!turtle.teleportTo(oldWorld, newPosition)) return TurtleCommandResult.failure("Movement failed");
|
||||
if (!turtle.teleportTo(level, newPosition)) return TurtleCommandResult.failure("Movement failed");
|
||||
|
||||
// Consume fuel
|
||||
turtle.consumeFuel(1);
|
||||
@@ -114,9 +89,20 @@ public class TurtleMoveCommand implements TurtleCommand {
|
||||
return TurtleCommandResult.success();
|
||||
}
|
||||
|
||||
private static AABB getBox(VoxelShape shape) {
|
||||
return shape.isEmpty() ? EMPTY_BOX : shape.bounds();
|
||||
}
|
||||
|
||||
private static final AABB EMPTY_BOX = new AABB(0, 0, 0, 0, 0, 0);
|
||||
/**
|
||||
* Determine if all entities in the given bounds can be pushed by the turtle.
|
||||
*
|
||||
* @param level The current level.
|
||||
* @param bounds The bounding box.
|
||||
* @return Whether all entities can be pushed.
|
||||
*/
|
||||
private boolean canPushEntities(Level level, AABB bounds) {
|
||||
if (!Config.turtlesCanPush) return false;
|
||||
|
||||
// Check there is space for all the pushable entities to be pushed
|
||||
return level.getEntities((Entity) null, bounds, e -> e.isAlive()
|
||||
&& !e.isSpectator() && e.blocksBuilding && e.getPistonPushReaction() == PushReaction.IGNORE
|
||||
).isEmpty();
|
||||
}
|
||||
}
|
||||
|
@@ -1,66 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.util;
|
||||
|
||||
import dan200.computercraft.api.component.ComputerComponent;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An immutable map of components.
|
||||
*/
|
||||
public final class ComponentMap {
|
||||
public static final ComputerComponent<MetricsObserver> METRICS = ComputerComponent.create("computercraft", "metrics");
|
||||
|
||||
private static final ComponentMap EMPTY = new ComponentMap(Map.of());
|
||||
|
||||
private final Map<ComputerComponent<?>, Object> components;
|
||||
|
||||
private ComponentMap(Map<ComputerComponent<?>, Object> components) {
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> @Nullable T get(ComputerComponent<T> component) {
|
||||
return (T) components.get(component);
|
||||
}
|
||||
|
||||
public static ComponentMap empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final Map<ComputerComponent<?>, Object> components = new HashMap<>();
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public <T> Builder add(ComputerComponent<T> component, T value) {
|
||||
addImpl(component, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(ComponentMap components) {
|
||||
for (var component : components.components.entrySet()) addImpl(component.getKey(), component.getValue());
|
||||
return this;
|
||||
}
|
||||
|
||||
private void addImpl(ComputerComponent<?> component, Object value) {
|
||||
if (components.containsKey(component)) throw new IllegalArgumentException(component + " is already set");
|
||||
components.put(component, value);
|
||||
}
|
||||
|
||||
public ComponentMap build() {
|
||||
return new ComponentMap(Map.copyOf(components));
|
||||
}
|
||||
}
|
||||
}
|
@@ -62,125 +62,33 @@ public final class NBTUtil {
|
||||
return childTag != null && childTag.getId() == Tag.TAG_COMPOUND ? (CompoundTag) childTag : emptyTag();
|
||||
}
|
||||
|
||||
private static @Nullable Tag toNBTTag(@Nullable Object object) {
|
||||
if (object == null) return null;
|
||||
if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0));
|
||||
if (object instanceof Number) return DoubleTag.valueOf(((Number) object).doubleValue());
|
||||
if (object instanceof String) return StringTag.valueOf(object.toString());
|
||||
if (object instanceof Map<?, ?> m) {
|
||||
var nbt = new CompoundTag();
|
||||
var i = 0;
|
||||
for (Map.Entry<?, ?> entry : m.entrySet()) {
|
||||
var key = toNBTTag(entry.getKey());
|
||||
var value = toNBTTag(entry.getKey());
|
||||
if (key != null && value != null) {
|
||||
nbt.put("k" + i, key);
|
||||
nbt.put("v" + i, value);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
nbt.putInt("len", m.size());
|
||||
return nbt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @Nullable CompoundTag encodeObjects(@Nullable Object[] objects) {
|
||||
if (objects == null || objects.length == 0) return null;
|
||||
|
||||
var nbt = new CompoundTag();
|
||||
nbt.putInt("len", objects.length);
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
var child = toNBTTag(objects[i]);
|
||||
if (child != null) nbt.put(Integer.toString(i), child);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
|
||||
private static @Nullable Object fromNBTTag(@Nullable Tag tag) {
|
||||
if (tag == null) return null;
|
||||
switch (tag.getId()) {
|
||||
case Tag.TAG_BYTE:
|
||||
return ((ByteTag) tag).getAsByte() > 0;
|
||||
case Tag.TAG_DOUBLE:
|
||||
return ((DoubleTag) tag).getAsDouble();
|
||||
default:
|
||||
case Tag.TAG_STRING:
|
||||
return tag.getAsString();
|
||||
case Tag.TAG_COMPOUND: {
|
||||
var c = (CompoundTag) tag;
|
||||
var len = c.getInt("len");
|
||||
Map<Object, Object> map = new HashMap<>(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = fromNBTTag(c.get("k" + i));
|
||||
var value = fromNBTTag(c.get("v" + i));
|
||||
if (key != null && value != null) map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Object toLua(@Nullable Tag tag) {
|
||||
if (tag == null) return null;
|
||||
|
||||
switch (tag.getId()) {
|
||||
case Tag.TAG_BYTE:
|
||||
case Tag.TAG_SHORT:
|
||||
case Tag.TAG_INT:
|
||||
case Tag.TAG_LONG:
|
||||
return ((NumericTag) tag).getAsLong();
|
||||
case Tag.TAG_FLOAT:
|
||||
case Tag.TAG_DOUBLE:
|
||||
return ((NumericTag) tag).getAsDouble();
|
||||
case Tag.TAG_STRING: // String
|
||||
return tag.getAsString();
|
||||
case Tag.TAG_COMPOUND: { // Compound
|
||||
return switch (tag.getId()) {
|
||||
case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong();
|
||||
case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble();
|
||||
case Tag.TAG_STRING -> tag.getAsString();
|
||||
case Tag.TAG_COMPOUND -> {
|
||||
var compound = (CompoundTag) tag;
|
||||
Map<String, Object> map = new HashMap<>(compound.size());
|
||||
for (var key : compound.getAllKeys()) {
|
||||
var value = toLua(compound.get(key));
|
||||
if (value != null) map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
yield map;
|
||||
}
|
||||
case Tag.TAG_LIST: {
|
||||
var list = (ListTag) tag;
|
||||
List<Object> map = new ArrayList<>(list.size());
|
||||
for (var value : list) map.add(toLua(value));
|
||||
return map;
|
||||
}
|
||||
case Tag.TAG_BYTE_ARRAY: {
|
||||
case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList();
|
||||
case Tag.TAG_BYTE_ARRAY -> {
|
||||
var array = ((ByteArrayTag) tag).getAsByteArray();
|
||||
List<Byte> map = new ArrayList<>(array.length);
|
||||
for (var b : array) map.add(b);
|
||||
return map;
|
||||
yield map;
|
||||
}
|
||||
case Tag.TAG_INT_ARRAY: {
|
||||
var array = ((IntArrayTag) tag).getAsIntArray();
|
||||
List<Integer> map = new ArrayList<>(array.length);
|
||||
for (var j : array) map.add(j);
|
||||
return map;
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable Object[] decodeObjects(CompoundTag tag) {
|
||||
var len = tag.getInt("len");
|
||||
if (len <= 0) return null;
|
||||
|
||||
var objects = new Object[len];
|
||||
for (var i = 0; i < len; i++) {
|
||||
var key = Integer.toString(i);
|
||||
if (tag.contains(key)) {
|
||||
objects[i] = fromNBTTag(tag.get(key));
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
case Tag.TAG_INT_ARRAY -> Arrays.stream(((IntArrayTag) tag).getAsIntArray()).boxed().toList();
|
||||
case Tag.TAG_LONG_ARRAY -> Arrays.stream(((LongArrayTag) tag).getAsLongArray()).boxed().toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@@ -29,9 +29,14 @@ import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class WorldUtil {
|
||||
@SuppressWarnings("deprecation")
|
||||
public static boolean isLiquidBlock(Level world, BlockPos pos) {
|
||||
if (!world.isInWorldBounds(pos)) return false;
|
||||
return world.getBlockState(pos).liquid();
|
||||
return world.isInWorldBounds(pos) && world.getBlockState(pos).liquid();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static boolean isEmptyBlock(BlockState state) {
|
||||
return state.isAir() || state.liquid();
|
||||
}
|
||||
|
||||
public static boolean isVecInside(VoxelShape shape, Vec3 vec) {
|
||||
|
After Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 179 B |
After Width: | Height: | Size: 119 B |
After Width: | Height: | Size: 92 B |
After Width: | Height: | Size: 152 B |
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 144 B After Width: | Height: | Size: 144 B |
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 476 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -25,6 +25,7 @@ import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
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.ClientboundCustomPayloadPacket;
|
||||
@@ -35,11 +36,11 @@ import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.CraftingContainer;
|
||||
import net.minecraft.world.inventory.MenuConstructor;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.minecraft.world.item.Item;
|
||||
@@ -140,7 +141,7 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openMenu(Player player, MenuProvider owner, ContainerData menu) {
|
||||
public void openMenu(Player player, Component title, MenuConstructor menu, ContainerData data) {
|
||||
throw new UnsupportedOperationException("Cannot open menu inside tests");
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,51 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.mixin.gametest;
|
||||
|
||||
import com.mojang.datafixers.DataFixer;
|
||||
import dan200.computercraft.gametest.core.TestHooks;
|
||||
import net.minecraft.gametest.framework.GameTestServer;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.Services;
|
||||
import net.minecraft.server.WorldStem;
|
||||
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
|
||||
import net.minecraft.server.packs.repository.PackRepository;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
@Mixin(GameTestServer.class)
|
||||
abstract class GameTestServerMixin extends MinecraftServer {
|
||||
GameTestServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
|
||||
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite {@link GameTestServer#waitUntilNextTick()} to wait for all computers to finish executing.
|
||||
* <p>
|
||||
* This is a little dangerous (breaks async behaviour of computers), but it forces tests to be deterministic.
|
||||
*
|
||||
* @reason See above. This is only in the test mod, so no risk of collision.
|
||||
* @author SquidDev.
|
||||
*/
|
||||
@Overwrite
|
||||
@Override
|
||||
public void waitUntilNextTick() {
|
||||
while (true) {
|
||||
runAllTasks();
|
||||
if (!haveTestsStarted() || TestHooks.areComputersIdle(this)) break;
|
||||
LockSupport.parkNanos(100_000);
|
||||
}
|
||||
}
|
||||
|
||||
@Shadow
|
||||
private boolean haveTestsStarted() {
|
||||
throw new AssertionError("Stub.");
|
||||
}
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.assertNoPeripheral
|
||||
import dan200.computercraft.gametest.api.assertPeripheral
|
||||
import dan200.computercraft.gametest.api.immediate
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.platform.ComponentAccess
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Direction
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Checks that we expose [ComponentAccess] for various blocks/block entities
|
||||
*/
|
||||
class Component_Test {
|
||||
@GameTest(template = "default")
|
||||
fun Peripheral(context: GameTestHelper) = context.immediate {
|
||||
val pos = BlockPos(2, 2, 2)
|
||||
// We fetch peripherals from the NORTH, as that is the default direction for modems. This is a bit of a hack,
|
||||
// but avoids having to override the block state.
|
||||
val side = Direction.NORTH
|
||||
|
||||
for ((block, type) in mapOf(
|
||||
// Computers
|
||||
ModRegistry.Blocks.COMPUTER_NORMAL to Optional.of("computer"),
|
||||
ModRegistry.Blocks.COMPUTER_ADVANCED to Optional.of("computer"),
|
||||
ModRegistry.Blocks.COMPUTER_COMMAND to Optional.empty(),
|
||||
// Turtles
|
||||
ModRegistry.Blocks.TURTLE_NORMAL to Optional.of("turtle"),
|
||||
ModRegistry.Blocks.TURTLE_ADVANCED to Optional.of("turtle"),
|
||||
// Peripherals
|
||||
ModRegistry.Blocks.SPEAKER to Optional.of("speaker"),
|
||||
ModRegistry.Blocks.DISK_DRIVE to Optional.of("drive"),
|
||||
ModRegistry.Blocks.PRINTER to Optional.of("printer"),
|
||||
ModRegistry.Blocks.MONITOR_NORMAL to Optional.of("monitor"),
|
||||
ModRegistry.Blocks.MONITOR_ADVANCED to Optional.of("monitor"),
|
||||
ModRegistry.Blocks.WIRELESS_MODEM_NORMAL to Optional.of("modem"),
|
||||
ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED to Optional.of("modem"),
|
||||
ModRegistry.Blocks.WIRED_MODEM_FULL to Optional.of("modem"),
|
||||
ModRegistry.Blocks.REDSTONE_RELAY to Optional.of("redstone_relay"),
|
||||
)) {
|
||||
context.setBlock(pos, block.get())
|
||||
if (type.isPresent) {
|
||||
context.assertPeripheral(pos, side, type.get())
|
||||
} else {
|
||||
context.assertNoPeripheral(pos, side)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -95,17 +95,6 @@ class Computer_Test {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check computers and turtles expose peripherals.
|
||||
*/
|
||||
@GameTest
|
||||
fun Computer_peripheral(context: GameTestHelper) = context.sequence {
|
||||
thenExecute {
|
||||
context.assertPeripheral(BlockPos(3, 2, 2), type = "computer")
|
||||
context.assertPeripheral(BlockPos(1, 2, 2), type = "turtle")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check chest peripherals are reattached with a new size.
|
||||
*/
|
||||
|
@@ -642,6 +642,27 @@ class Turtle_Test {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test turtles can push entities.
|
||||
*/
|
||||
@GameTest
|
||||
fun Move_push_entity(helper: GameTestHelper) = helper.sequence {
|
||||
thenOnComputer { turtle.up().await().assertArrayEquals(true) }
|
||||
thenIdle(9)
|
||||
thenExecute {
|
||||
// The turtle has moved up
|
||||
helper.assertBlockPresent(ModRegistry.Blocks.TURTLE_NORMAL.get(), BlockPos(2, 3, 2))
|
||||
|
||||
// As has the villager
|
||||
val pos = BlockPos(2, 4, 2)
|
||||
helper.assertEntityPresent(EntityType.VILLAGER, pos)
|
||||
|
||||
val villager = helper.getEntity(EntityType.VILLAGER)
|
||||
val expectedY = helper.absolutePos(pos).y - 0.125
|
||||
if (villager.y < expectedY) helper.fail("Expected villager at y>=$expectedY, but at ${villager.y}", pos)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a turtle can attack an entity and capture its drops.
|
||||
*/
|
||||
|
@@ -127,6 +127,14 @@ fun GameTestHelper.sequence(run: GameTestSequence.() -> Unit) {
|
||||
sequence.thenSucceed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a function immediately, and then succeed.
|
||||
*/
|
||||
fun GameTestHelper.immediate(run: () -> Unit) {
|
||||
run()
|
||||
succeed()
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom instance of [GameTestAssertPosException] which allows for longer error messages.
|
||||
*/
|
||||
@@ -232,15 +240,17 @@ private fun GameTestHelper.getPeripheralAt(pos: BlockPos, direction: Direction):
|
||||
|
||||
fun GameTestHelper.assertPeripheral(pos: BlockPos, direction: Direction = Direction.UP, type: String) {
|
||||
val peripheral = getPeripheralAt(pos, direction)
|
||||
val block = getBlockState(pos).block.name.string
|
||||
when {
|
||||
peripheral == null -> fail("No peripheral at position", pos)
|
||||
peripheral.type != type -> fail("Peripheral is of type ${peripheral.type}, expected $type", pos)
|
||||
peripheral == null -> fail("No peripheral for '$block'", pos)
|
||||
peripheral.type != type -> fail("Peripheral for '$block' is of type ${peripheral.type}, expected $type", pos)
|
||||
}
|
||||
}
|
||||
|
||||
fun GameTestHelper.assertNoPeripheral(pos: BlockPos, direction: Direction = Direction.UP) {
|
||||
val peripheral = getPeripheralAt(pos, direction)
|
||||
if (peripheral != null) fail("Expected no peripheral, got a ${peripheral.type}", pos)
|
||||
val block = getBlockState(pos).block.name
|
||||
if (peripheral != null) fail("Expected no peripheral for '$block', got a ${peripheral.type}", pos)
|
||||
}
|
||||
|
||||
fun GameTestHelper.assertExactlyItems(vararg expected: ItemStack, message: String? = null) {
|
||||
|
@@ -36,6 +36,10 @@ object ManagedComputers : ILuaMachine.Factory {
|
||||
private val LOGGER = LoggerFactory.getLogger(ManagedComputers::class.java)
|
||||
private val computers: MutableMap<String, Queue<suspend LuaTaskContext.() -> Unit>> = mutableMapOf()
|
||||
|
||||
internal fun reset() {
|
||||
computers.clear()
|
||||
}
|
||||
|
||||
internal fun enqueue(test: GameTestInfo, label: String, task: suspend LuaTaskContext.() -> Unit): Monitor {
|
||||
val monitor = Monitor(test, label)
|
||||
computers.computeIfAbsent(label) { ConcurrentLinkedDeque() }.add {
|
||||
|
@@ -5,6 +5,8 @@
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI
|
||||
import dan200.computercraft.core.ComputerContext
|
||||
import dan200.computercraft.core.computer.computerthread.ComputerThread
|
||||
import dan200.computercraft.gametest.*
|
||||
import dan200.computercraft.gametest.api.ClientGameTest
|
||||
import dan200.computercraft.gametest.api.TestTags
|
||||
@@ -23,6 +25,8 @@ import net.minecraft.world.phys.Vec3
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.lang.invoke.MethodHandle
|
||||
import java.lang.invoke.MethodHandles
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
@@ -69,6 +73,8 @@ object TestHooks {
|
||||
LOG.info("Cleaning up after last run")
|
||||
GameTestRunner.clearAllTests(server.overworld(), BlockPos(0, -60, 0), GameTestTicker.SINGLETON, 200)
|
||||
|
||||
ManagedComputers.reset()
|
||||
|
||||
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for
|
||||
// specific test batches without having to reset all computers.
|
||||
for (computer in ServerContext.get(server).registry().computers) {
|
||||
@@ -80,7 +86,11 @@ object TestHooks {
|
||||
CCTestCommand.importFiles(server)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
|
||||
|
||||
private val testClasses = listOf(
|
||||
Component_Test::class.java,
|
||||
Computer_Test::class.java,
|
||||
CraftOs_Test::class.java,
|
||||
Disk_Drive_Test::class.java,
|
||||
@@ -116,14 +126,6 @@ object TestHooks {
|
||||
}
|
||||
}
|
||||
|
||||
private val isCi = System.getenv("CI") != null
|
||||
|
||||
/**
|
||||
* Adjust the timeout of a test. This makes it 1.5 times longer when run under CI, as CI servers are less powerful
|
||||
* than our own.
|
||||
*/
|
||||
private fun adjustTimeout(timeout: Int): Int = if (isCi) timeout + (timeout / 2) else timeout
|
||||
|
||||
private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
|
||||
val className = testClass.simpleName.lowercase()
|
||||
val testName = className + "." + method.name.lowercase()
|
||||
@@ -135,7 +137,7 @@ object TestHooks {
|
||||
TestFunction(
|
||||
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
|
||||
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
testInfo.timeoutTicks,
|
||||
testInfo.setupTicks,
|
||||
testInfo.required, testInfo.requiredSuccesses, testInfo.attempts,
|
||||
) { value -> safeInvoke(method, value) },
|
||||
@@ -152,7 +154,7 @@ object TestHooks {
|
||||
testName,
|
||||
testName,
|
||||
testInfo.template.ifEmpty { testName },
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
testInfo.timeoutTicks,
|
||||
0,
|
||||
true,
|
||||
) { value -> safeInvoke(method, value) },
|
||||
@@ -200,3 +202,31 @@ object TestHooks {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nasty reflection to determine if computers are fully idle.
|
||||
*
|
||||
* This is horribly nasty, and should not be used as a model for any production code!
|
||||
*
|
||||
* @see [ComputerThread.isFullyIdle]
|
||||
* @see [dan200.computercraft.mixin.gametest.GameTestServerMixin]
|
||||
*/
|
||||
private object ComputerThreadReflection {
|
||||
private val lookup = MethodHandles.lookup()
|
||||
|
||||
@JvmField
|
||||
val computerContext: MethodHandle = lookup.unreflectGetter(
|
||||
ServerContext::class.java.getDeclaredField("context").also { it.isAccessible = true },
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val isFullyIdle: MethodHandle = lookup.unreflect(
|
||||
ComputerThread::class.java.getDeclaredMethod("isFullyIdle").also { it.isAccessible = true },
|
||||
)
|
||||
|
||||
fun isFullyIdle(context: ServerContext): Boolean {
|
||||
val computerContext = computerContext.invokeExact(context) as ComputerContext
|
||||
val computerThread = computerContext.computerScheduler() as ComputerThread
|
||||
return isFullyIdle.invokeExact(computerThread) as Boolean
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
|
||||
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
|
||||
* directory exists.
|
||||
*/
|
||||
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
override fun save(file: File) {
|
||||
try {
|
||||
Files.createDirectories(file.toPath().parent)
|
||||
|
@@ -11,6 +11,7 @@
|
||||
"GameTestInfoAccessor",
|
||||
"GameTestSequenceAccessor",
|
||||
"GameTestSequenceMixin",
|
||||
"GameTestServerMixin",
|
||||
"TestCommandAccessor"
|
||||
],
|
||||
"client": [
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
DataVersion: 2975,
|
||||
DataVersion: 3465,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
|
||||
@@ -33,19 +33,19 @@
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "minecraft:air"},
|
||||
{pos: [1, 1, 0], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "computercraft:turtle_advanced{facing:south,waterlogged:false}", nbt: {Fuel: 0, Items: [], On: 0b, Owner: {LowerId: -6876936588741668278L, Name: "Dev", UpperId: 4039158846114182220L}, Slot: 0, id: "computercraft:turtle_advanced"}},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 1, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 1, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "minecraft:air"},
|
||||
{pos: [2, 1, 3], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:west,waterlogged:false}", nbt: {ComputerId: 1, Label: "turtle_test.move_push_entity", Fuel: 80, Items: [], On: 1b, Slot: 0, id: "computercraft:turtle_normal"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "computercraft:computer_advanced{facing:north,state:off}", nbt: {On: 0b, id: "computercraft:computer_advanced"}},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 1, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 1, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 1, 4], state: "minecraft:air"},
|
||||
{pos: [4, 1, 0], state: "minecraft:air"},
|
||||
{pos: [4, 1, 1], state: "minecraft:air"},
|
||||
@@ -58,19 +58,19 @@
|
||||
{pos: [0, 2, 3], state: "minecraft:air"},
|
||||
{pos: [0, 2, 4], state: "minecraft:air"},
|
||||
{pos: [1, 2, 0], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 2, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 2, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 2, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 2, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 2, 4], state: "minecraft:air"},
|
||||
{pos: [4, 2, 0], state: "minecraft:air"},
|
||||
{pos: [4, 2, 1], state: "minecraft:air"},
|
||||
@@ -83,19 +83,19 @@
|
||||
{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, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 3, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 3, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:white_stained_glass"},
|
||||
{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, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 3, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 3, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
@@ -108,19 +108,19 @@
|
||||
{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, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 4, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 4, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:white_stained_glass"},
|
||||
{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, 1], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 4, 2], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 4, 3], state: "minecraft:white_stained_glass"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
@@ -128,11 +128,13 @@
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
entities: [
|
||||
{blockPos: [2, 1, 2], pos: [2.5d, 1.875d, 2.5d], nbt: {AbsorptionAmount: 0.0f, Age: 0, Air: 300s, ArmorDropChances: [0.085f, 0.085f, 0.085f, 0.085f], ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.5d, Name: "minecraft:generic.movement_speed"}, {Base: 48.0d, Modifiers: [{Amount: -0.01165046535152748d, Name: "Random spawn bonus", Operation: 1, UUID: [I; 1412502412, 1522745411, -1211155694, 2103054347]}], Name: "minecraft:generic.follow_range"}], Brain: {memories: {}}, CanPickUpLoot: 1b, DeathTime: 0s, FallDistance: 0.0f, FallFlying: 0b, Fire: -1s, FoodLevel: 0b, ForcedAge: 0, Gossips: [], HandDropChances: [0.085f, 0.085f], HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Inventory: [], Invulnerable: 0b, LastGossipDecay: 52357L, LastRestock: 0L, LeftHanded: 0b, Motion: [0.0d, -0.0784000015258789d, 0.0d], OnGround: 1b, PersistenceRequired: 0b, PortalCooldown: 0, Pos: [-33.5d, 58.875d, -21.5d], RestocksToday: 0, Rotation: [-102.704926f, 0.0f], UUID: [I; 164071932, -867285780, -1817215456, -2129864016], VillagerData: {level: 1, profession: "minecraft:none", type: "minecraft:desert"}, Xp: 0, id: "minecraft:villager"}}
|
||||
],
|
||||
palette: [
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:white_stained_glass",
|
||||
"minecraft:air",
|
||||
"computercraft:turtle_advanced{facing:south,waterlogged:false}",
|
||||
"computercraft:computer_advanced{facing:north,state:off}"
|
||||
"computercraft:turtle_normal{facing:west,waterlogged:false}"
|
||||
]
|
||||
}
|
@@ -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.
|
||||
|
@@ -31,7 +31,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class Computer {
|
||||
public class Computer implements ComputerEvents.Receiver {
|
||||
private static final int START_DELAY = 50;
|
||||
|
||||
// Various properties of the computer
|
||||
@@ -114,6 +114,7 @@ public class Computer {
|
||||
executor.queueStop(false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, @Nullable Object[] args) {
|
||||
executor.queueEvent(event, args);
|
||||
}
|
||||
|
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Built-in events that can be queued on a computer.
|
||||
*/
|
||||
public final class ComputerEvents {
|
||||
private ComputerEvents() {
|
||||
}
|
||||
|
||||
public static void keyDown(Receiver receiver, int key, boolean repeat) {
|
||||
receiver.queueEvent("key", new Object[]{ key, repeat });
|
||||
}
|
||||
|
||||
public static void keyUp(Receiver receiver, int key) {
|
||||
receiver.queueEvent("key_up", new Object[]{ key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Type a character on the computer.
|
||||
*
|
||||
* @param receiver The computer to queue the event on.
|
||||
* @param chr The character to type.
|
||||
* @see StringUtil#isTypableChar(byte)
|
||||
*/
|
||||
public static void charTyped(Receiver receiver, byte chr) {
|
||||
receiver.queueEvent("char", new Object[]{ new byte[]{ chr } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste a string.
|
||||
*
|
||||
* @param receiver The computer to queue the event on.
|
||||
* @param contents The string to paste.
|
||||
* @see StringUtil#getClipboardString(String)
|
||||
*/
|
||||
public static void paste(Receiver receiver, ByteBuffer contents) {
|
||||
receiver.queueEvent("paste", new Object[]{ contents });
|
||||
}
|
||||
|
||||
public static void mouseClick(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_click", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseUp(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_up", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseDrag(Receiver receiver, int button, int x, int y) {
|
||||
receiver.queueEvent("mouse_drag", new Object[]{ button, x, y });
|
||||
}
|
||||
|
||||
public static void mouseScroll(Receiver receiver, int direction, int x, int y) {
|
||||
receiver.queueEvent("mouse_scroll", new Object[]{ direction, x, y });
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that can receive computer events.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Receiver {
|
||||
void queueEvent(String event, @Nullable Object[] arguments);
|
||||
}
|
||||
}
|
@@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
|
||||
return computerQueueSize() > idleWorkers.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if no work is queued, and all workers are idle.
|
||||
*
|
||||
* @return If the threads are fully idle.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isFullyIdle() {
|
||||
return computerQueueSize() == 0 && idleWorkers.get() >= workerCount();
|
||||
}
|
||||
|
||||
private void workerFinished(WorkerThread worker) {
|
||||
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
||||
// worker finishes normally.
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -4,52 +4,118 @@
|
||||
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import dan200.computercraft.core.computer.ComputerEvents;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class StringUtil {
|
||||
public static final int MAX_PASTE_LENGTH = 512;
|
||||
|
||||
private StringUtil() {
|
||||
}
|
||||
|
||||
private static boolean isAllowed(char c) {
|
||||
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255);
|
||||
}
|
||||
|
||||
private static String removeSpecialCharacters(String text, int length) {
|
||||
var builder = new StringBuilder(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = text.charAt(i);
|
||||
builder.append(isAllowed(c) ? c : '?');
|
||||
/**
|
||||
* Convert a Unicode character to a terminal one.
|
||||
*
|
||||
* @param chr The Unicode 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 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 chr;
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
// Teletext block mosaics are *fairly* contiguous.
|
||||
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);
|
||||
}
|
||||
|
||||
public static String normaliseLabel(String text) {
|
||||
return removeSpecialCharacters(text, Math.min(32, text.length()));
|
||||
private static int unicodeToCraftOsFallback(int c) {
|
||||
return switch (c) {
|
||||
case 0x263A -> 1;
|
||||
case 0x263B -> 2;
|
||||
case 0x2665 -> 3;
|
||||
case 0x2666 -> 4;
|
||||
case 0x2663 -> 5;
|
||||
case 0x2660 -> 6;
|
||||
case 0x2022 -> 7;
|
||||
case 0x25D8 -> 8;
|
||||
case 0x2642 -> 11;
|
||||
case 0x2640 -> 12;
|
||||
case 0x266A -> 14;
|
||||
case 0x266B -> 15;
|
||||
case 0x25BA -> 16;
|
||||
case 0x25C4 -> 17;
|
||||
case 0x2195 -> 18;
|
||||
case 0x203C -> 19;
|
||||
case 0x25AC -> 22;
|
||||
case 0x21A8 -> 23;
|
||||
case 0x2191 -> 24;
|
||||
case 0x2193 -> 25;
|
||||
case 0x2192 -> 26;
|
||||
case 0x2190 -> 27;
|
||||
case 0x221F -> 28;
|
||||
case 0x2194 -> 29;
|
||||
case 0x25B2 -> 30;
|
||||
case 0x25BC -> 31;
|
||||
case 0x1FB99 -> 127;
|
||||
case 0x258C -> 149;
|
||||
default -> -1;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a string from the clipboard, suitable for pasting into a computer.
|
||||
* Check if a character is capable of being input and passed to a {@linkplain ComputerEvents#charTyped(ComputerEvents.Receiver, byte)
|
||||
* "char" event}.
|
||||
*
|
||||
* @param chr The character to check.
|
||||
* @return Whether this character can be typed.
|
||||
*/
|
||||
public static boolean isTypableChar(int chr) {
|
||||
return chr >= 0 && chr <= 255 && chr != 0 && chr != '\r' && chr != '\n';
|
||||
}
|
||||
|
||||
private static boolean isAllowedInLabel(char c) {
|
||||
// Limit to ASCII and latin1, excluding '§' (Minecraft's formatting character).
|
||||
return (c >= ' ' && c <= '~') || (c >= 161 && c <= 255 && c != 167);
|
||||
}
|
||||
|
||||
public static String normaliseLabel(String text) {
|
||||
var length = Math.min(32, text.length());
|
||||
var builder = new StringBuilder(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = text.charAt(i);
|
||||
builder.append(isAllowedInLabel(c) ? c : '?');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Java string to a Lua one (using the terminal charset), suitable for pasting into a computer.
|
||||
* <p>
|
||||
* This removes special characters and strips to the first line of text.
|
||||
*
|
||||
* @param clipboard The text from the clipboard.
|
||||
* @return The normalised clipboard text.
|
||||
* @return The encoded clipboard text.
|
||||
*/
|
||||
public static String normaliseClipboardString(String clipboard) {
|
||||
// Clip to the first occurrence of \r or \n
|
||||
var newLineIndex1 = clipboard.indexOf('\r');
|
||||
var newLineIndex2 = clipboard.indexOf('\n');
|
||||
public static ByteBuffer getClipboardString(String clipboard) {
|
||||
var output = new byte[Math.min(MAX_PASTE_LENGTH, clipboard.length())];
|
||||
var idx = 0;
|
||||
|
||||
int length;
|
||||
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
|
||||
length = Math.min(newLineIndex1, newLineIndex2);
|
||||
} else if (newLineIndex1 >= 0) {
|
||||
length = newLineIndex1;
|
||||
} else if (newLineIndex2 >= 0) {
|
||||
length = newLineIndex2;
|
||||
} else {
|
||||
length = clipboard.length();
|
||||
var iterator = clipboard.codePoints().iterator();
|
||||
while (iterator.hasNext() && idx <= output.length) {
|
||||
var chr = unicodeToTerminal(iterator.next());
|
||||
if (chr < 0) continue; // Strip out unconvertible characters
|
||||
if (!isTypableChar(chr)) break; // Stop at untypable ones.
|
||||
output[idx++] = (byte) chr;
|
||||
}
|
||||
|
||||
return removeSpecialCharacters(clipboard, Math.min(length, 512));
|
||||
return ByteBuffer.wrap(output, 0, idx).asReadOnlyBuffer();
|
||||
}
|
||||
}
|
||||
|
@@ -47,12 +47,22 @@ local function sortCoords(startX, startY, endX, endY)
|
||||
return minX, maxX, minY, maxY
|
||||
end
|
||||
|
||||
--- Parses an image from a multi-line string
|
||||
--
|
||||
-- @tparam string image The string containing the raw-image data.
|
||||
-- @treturn table The parsed image data, suitable for use with
|
||||
-- [`paintutils.drawImage`].
|
||||
-- @since 1.80pr1
|
||||
--[=[- Parses an image from a multi-line string
|
||||
|
||||
@tparam string image The string containing the raw-image data.
|
||||
@treturn table The parsed image data, suitable for use with [`paintutils.drawImage`].
|
||||
@usage Parse an image from a string, and draw it.
|
||||
|
||||
local image = paintutils.parseImage([[
|
||||
e e
|
||||
|
||||
e e
|
||||
eeee
|
||||
]])
|
||||
paintutils.drawImage(image, term.getCursorPos())
|
||||
|
||||
@since 1.80pr1
|
||||
]=]
|
||||
function parseImage(image)
|
||||
expect(1, image, "string")
|
||||
local tImage = {}
|
||||
|
@@ -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,22 @@
|
||||
# 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.
|
||||
|
||||
Several bug fixes:
|
||||
* Ignore unrepresentable characters in `char`/`paste` events.
|
||||
|
||||
# New features in CC: Tweaked 1.114.4
|
||||
|
||||
* Allow typing/pasting any character in the CC charset.
|
||||
|
||||
Several bug fixes:
|
||||
* Fix command computers being exposed as peripherals (Forge only).
|
||||
* Fix command computers having NBT set when placed in a Create contraption.
|
||||
* Use correct bounding box when checking for entities in turtle movement.
|
||||
|
||||
# New features in CC: Tweaked 1.114.3
|
||||
|
||||
* `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69).
|
||||
|
@@ -1,10 +1,11 @@
|
||||
New features in CC: Tweaked 1.114.3
|
||||
New features in CC: Tweaked 1.115.0
|
||||
|
||||
* `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69).
|
||||
* Update several translations.
|
||||
* 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.
|
||||
|
||||
Several bug fixes:
|
||||
* Fix `fs.isDriveRoot` returning true for non-existent files.
|
||||
* Fix possible memory leak when sending terminal contents.
|
||||
* 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;
|
||||
}
|
||||
}
|