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

This commit is contained in:
Jonathan Coates 2024-04-07 21:56:22 +01:00
commit 22bd5309ba
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
51 changed files with 871 additions and 160 deletions

View File

@ -9,16 +9,16 @@ jobs:
steps: steps:
- name: 📥 Clone repository - name: 📥 Clone repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 📥 Set up Java - name: 📥 Set up Java
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: 'temurin' distribution: 'temurin'
- name: 📥 Setup Gradle - name: 📥 Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
with: with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
@ -82,16 +82,16 @@ jobs:
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Java - name: Set up Java
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
with: with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}

View File

@ -13,16 +13,16 @@ jobs:
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Java - name: Set up Java
uses: actions/setup-java@v1 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
with: with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }} cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}

View File

@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties # Mod properties
isUnstable=true isUnstable=true
modVersion=1.110.0 modVersion=1.110.2
# Minecraft properties: We want to configure this here so we can read it in settings.gradle # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.20.4 mcVersion=1.20.4

View File

@ -63,11 +63,11 @@ fabric-loom = "1.5.7"
githubRelease = "2.5.2" githubRelease = "2.5.2"
gradleVersions = "0.50.0" gradleVersions = "0.50.0"
ideaExt = "1.1.7" ideaExt = "1.1.7"
illuaminate = "0.1.0-69-gf294ab2" illuaminate = "0.1.0-71-g378d86e"
lwjgl = "3.3.3" lwjgl = "3.3.3"
minotaur = "2.8.7" minotaur = "2.8.7"
neoGradle = "7.0.100" neoGradle = "7.0.100"
nullAway = "0.9.9" nullAway = "0.10.25"
spotless = "6.23.3" spotless = "6.23.3"
taskTree = "2.1.1" taskTree = "2.1.1"
teavm = "0.10.0-SQUID.3" teavm = "0.10.0-SQUID.3"

View File

@ -9,6 +9,7 @@
import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.network.ClientNetworking; import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.InputHandler; import dan200.computercraft.shared.computer.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
@ -18,6 +19,7 @@
import dan200.computercraft.shared.network.server.UploadFileMessage; import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.Util; import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
@ -96,8 +98,8 @@ public void containerTick() {
getTerminal().update(); getTerminal().update();
if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) { if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
new ItemToast(minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN) new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
.showOrReplace(minecraft.getToasts()); .showOrReplace(minecraft().getToasts());
uploadNagDeadline = Long.MAX_VALUE; uploadNagDeadline = Long.MAX_VALUE;
} }
} }
@ -206,7 +208,7 @@ public void onFilesDrop(List<Path> files) {
return; return;
} }
if (toUpload.size() > 0) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer); if (!toUpload.isEmpty()) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer);
} }
public void uploadResult(UploadResult result, @Nullable Component message) { public void uploadResult(UploadResult result, @Nullable Component message) {
@ -222,9 +224,13 @@ public void uploadResult(UploadResult result, @Nullable Component message) {
} }
private void alert(Component title, Component message) { private void alert(Component title, Component message) {
OptionScreen.show(minecraft, title, message, OptionScreen.show(minecraft(), title, message,
List.of(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))), List.of(OptionScreen.newButton(OK, b -> minecraft().setScreen(this))),
() -> minecraft.setScreen(this) () -> minecraft().setScreen(this)
); );
} }
private Minecraft minecraft() {
return Nullability.assertNonNull(minecraft);
}
} }

View File

@ -6,8 +6,10 @@
import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.KeyMapping; import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess; import net.minecraft.client.gui.screens.inventory.MenuAccess;
@ -16,6 +18,7 @@
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Objects;
import static dan200.computercraft.core.util.Nullability.assertNonNull; import static dan200.computercraft.core.util.Nullability.assertNonNull;
@ -44,8 +47,8 @@ public T getMenu() {
protected void init() { protected void init() {
// First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that // First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that
// grabbing unsets. // grabbing unsets.
minecraft.mouseHandler.grabMouse(); minecraft().mouseHandler.grabMouse();
minecraft.screen = this; minecraft().screen = this;
KeyMapping.releaseAll(); KeyMapping.releaseAll();
super.init(); super.init();
@ -64,13 +67,13 @@ public final void tick() {
@Override @Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
minecraft.player.getInventory().swapPaint(scrollX); Objects.requireNonNull(minecraft().player).getInventory().swapPaint(scrollY);
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
} }
@Override @Override
public void onClose() { public void onClose() {
minecraft.player.closeContainer(); Objects.requireNonNull(minecraft().player).closeContainer();
super.onClose(); super.onClose();
} }
@ -93,12 +96,16 @@ public final boolean keyPressed(int key, int scancode, int modifiers) {
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) {
super.render(graphics, mouseX, mouseY, partialTicks); super.render(graphics, mouseX, mouseY, partialTicks);
var font = minecraft.font; var font = minecraft().font;
var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8)); var lines = font.split(Component.translatable("gui.computercraft.pocket_computer_overlay"), (int) (width * 0.8));
var y = 10; var y = 10;
for (var line : lines) { for (var line : lines) {
graphics.drawString(font, line, (width / 2) - (minecraft.font.width(line) / 2), y, 0xFFFFFF, true); graphics.drawString(font, line, (width / 2) - (font.width(line) / 2), y, 0xFFFFFF, true);
y += 9; y += 9;
} }
} }
private Minecraft minecraft() {
return Nullability.assertNonNull(minecraft);
}
} }

View File

@ -66,13 +66,13 @@ public boolean keyPressed(int key, int scancode, int modifiers) {
@Override @Override
public boolean mouseScrolled(double x, double y, double deltaX, double deltaY) { public boolean mouseScrolled(double x, double y, double deltaX, double deltaY) {
if (super.mouseScrolled(x, y, deltaX, deltaY)) return true; if (super.mouseScrolled(x, y, deltaX, deltaY)) return true;
if (deltaX < 0) { if (deltaY < 0) {
// Scroll up goes to the next page // Scroll up goes to the next page
if (page < pages - 1) page++; if (page < pages - 1) page++;
return true; return true;
} }
if (deltaX > 0) { if (deltaY > 0) {
// Scroll down goes to the previous page // Scroll down goes to the previous page
if (page > 0) page--; if (page > 0) page--;
return true; return true;

View File

@ -195,16 +195,16 @@ public boolean mouseDragged(double mouseX, double mouseY, int button, double v2,
} }
@Override @Override
public boolean mouseScrolled(double mouseX, double mouseY, double delta, double deltaY) { public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) {
if (!inTermRegion(mouseX, mouseY)) return false; if (!inTermRegion(mouseX, mouseY)) return false;
if (!hasMouseSupport() || delta == 0) return false; if (!hasMouseSupport() || deltaY == 0) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH); var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT); var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1); charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 1);
charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1); charY = Math.min(Math.max(charY, 0), terminal.getHeight() - 1);
computer.mouseScroll(delta < 0 ? 1 : -1, charX + 1, charY + 1); computer.mouseScroll(deltaY < 0 ? 1 : -1, charX + 1, charY + 1);
lastMouseX = charX; lastMouseX = charX;
lastMouseY = charY; lastMouseY = charY;

View File

@ -6,7 +6,6 @@
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer; import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage; import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.pocket.items.PocketComputerItem; import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@ -47,13 +46,10 @@ public static void remove(UUID id) {
public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) { public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
var computer = instances.get(instanceId); var computer = instances.get(instanceId);
if (computer == null) { if (computer == null) {
var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour); instances.put(instanceId, new PocketComputerData(state, lightColour, terminalData));
instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
} else { } else {
computer.setState(state, lightColour); computer.setState(state, lightColour, terminalData);
} }
if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
} }
public static @Nullable PocketComputerData get(ItemStack stack) { public static @Nullable PocketComputerData get(ItemStack stack) {

View File

@ -6,8 +6,11 @@
import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal; import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.pocket.core.PocketServerComputer; import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import javax.annotation.Nullable;
/** /**
* Clientside data about a pocket computer. * Clientside data about a pocket computer.
* <p> * <p>
@ -19,21 +22,21 @@
* @see PocketServerComputer The server-side pocket computer. * @see PocketServerComputer The server-side pocket computer.
*/ */
public final class PocketComputerData { public final class PocketComputerData {
private final NetworkedTerminal terminal; private @Nullable NetworkedTerminal terminal;
private ComputerState state; private ComputerState state;
private int lightColour; private int lightColour;
PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) { PocketComputerData(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state; this.state = state;
this.lightColour = lightColour; this.lightColour = lightColour;
this.terminal = terminal; if (terminalData.hasTerminal()) terminal = terminalData.create();
} }
public int getLightState() { public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1; return state != ComputerState.OFF ? lightColour : -1;
} }
public NetworkedTerminal getTerminal() { public @Nullable NetworkedTerminal getTerminal() {
return terminal; return terminal;
} }
@ -41,8 +44,16 @@ public ComputerState getState() {
return state; return state;
} }
void setState(ComputerState state, int lightColour) { void setState(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state; this.state = state;
this.lightColour = lightColour; this.lightColour = lightColour;
if (terminalData.hasTerminal()) {
if (terminal == null) {
terminal = terminalData.create();
} else {
terminalData.apply(terminal);
}
}
} }
} }

View File

@ -17,6 +17,8 @@
import net.minecraft.world.item.ItemDisplayContext; import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import java.util.Objects;
/** /**
* A base class for items which have map-like rendering when held in the hand. * A base class for items which have map-like rendering when held in the hand.
* *
@ -35,7 +37,7 @@ public abstract class ItemMapLikeRenderer {
protected abstract void renderItem(PoseStack transform, MultiBufferSource render, ItemStack stack, int light); protected abstract void renderItem(PoseStack transform, MultiBufferSource render, ItemStack stack, int light);
public void renderItemFirstPerson(PoseStack transform, MultiBufferSource render, int lightTexture, InteractionHand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack) { public void renderItemFirstPerson(PoseStack transform, MultiBufferSource render, int lightTexture, InteractionHand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack) {
Player player = Minecraft.getInstance().player; Player player = Objects.requireNonNull(Minecraft.getInstance().player);
transform.pushPose(); transform.pushPose();
if (hand == InteractionHand.MAIN_HAND && player.getOffhandItem().isEmpty()) { if (hand == InteractionHand.MAIN_HAND && player.getOffhandItem().isEmpty()) {

View File

@ -55,7 +55,7 @@ public void render(TurtleBlockEntity turtle, float partialTicks, PoseStack trans
// Render the label // Render the label
var label = turtle.getLabel(); var label = turtle.getLabel();
var hit = renderer.cameraHitResult; var hit = renderer.cameraHitResult;
if (label != null && hit.getType() == HitResult.Type.BLOCK && turtle.getBlockPos().equals(((BlockHitResult) hit).getBlockPos())) { if (label != null && hit != null && hit.getType() == HitResult.Type.BLOCK && turtle.getBlockPos().equals(((BlockHitResult) hit).getBlockPos())) {
var mc = Minecraft.getInstance(); var mc = Minecraft.getInstance();
var font = this.font; var font = this.font;

View File

@ -8,6 +8,7 @@
import dan200.computercraft.impl.RegistryHelper; import dan200.computercraft.impl.RegistryHelper;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import dan200.computercraft.shared.integration.ExternalModTags;
import net.minecraft.data.tags.ItemTagsProvider; import net.minecraft.data.tags.ItemTagsProvider;
import net.minecraft.data.tags.TagsProvider; import net.minecraft.data.tags.TagsProvider;
import net.minecraft.tags.BlockTags; import net.minecraft.tags.BlockTags;
@ -82,6 +83,12 @@ public static void blockTags(TagConsumer<Block> tags) {
); );
tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get()); tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get());
tags.tag(ExternalModTags.Blocks.CREATE_BRITTLE).add(
ModRegistry.Blocks.CABLE.get(),
ModRegistry.Blocks.WIRELESS_MODEM_NORMAL.get(),
ModRegistry.Blocks.WIRELESS_MODEM_ADVANCED.get()
);
} }
public static void itemTags(ItemTagConsumer tags) { public static void itemTags(ItemTagConsumer tags) {

View File

@ -223,7 +223,7 @@ private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPo
var offsetSide = dir.getOpposite(); var offsetSide = dir.getOpposite();
var localDir = remapToLocalSide(dir); var localDir = remapToLocalSide(dir);
computer.setRedstoneInput(localDir, RedstoneUtil.getRedstoneInput(level, targetPos, dir)); computer.setRedstoneInput(localDir, RedstoneUtil.getRedstoneInput(getLevel(), targetPos, dir));
computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide)); computer.setBundledRedstoneInput(localDir, BundledRedstone.getOutput(getLevel(), targetPos, offsetSide));
} }

View File

@ -28,13 +28,14 @@ public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
} }
void update() { void update() {
var it = getComputers().iterator(); var it = computersByInstanceUuid.values().iterator();
while (it.hasNext()) { while (it.hasNext()) {
var computer = it.next(); var computer = it.next();
if (computer.hasTimedOut()) { if (computer.hasTimedOut()) {
computer.unload(); computer.unload();
computer.onRemoved(); computer.onRemoved();
it.remove(); it.remove();
computersByInstanceUuid.remove(computer.getInstanceUUID());
} else { } else {
computer.tickServer(); computer.tickServer();
} }

View File

@ -18,10 +18,9 @@
* states, etc... * states, etc...
*/ */
public class TerminalState { public class TerminalState {
public final boolean colour; private final boolean colour;
private final int width;
public final int width; private final int height;
public final int height;
@Nullable @Nullable
private final ByteBuf buffer; private final ByteBuf buffer;

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.integration;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
/**
* Tags defined by external mods.
*/
public final class ExternalModTags {
private ExternalModTags() {
}
/**
* Block tags defined by external mods.
*/
public static final class Blocks {
private Blocks() {
}
/**
* Create's "brittle" tag, used to determine if this block needs to be moved before its neighbours.
*
* @see <a href="https://github.com/Creators-of-Create/Create/blob/mc1.20.1/dev/src/main/java/com/simibubi/create/content/contraptions/BlockMovementChecks.java">{@code BlockMovementChecks}</a>
*/
public static final TagKey<Block> CREATE_BRITTLE = make("create", "brittle");
private static TagKey<Block> make(String mod, String name) {
return TagKey.create(Registries.BLOCK, new ResourceLocation(mod, name));
}
}
}

View File

@ -303,7 +303,7 @@ synchronized MountResult setDiskLabel(@Nullable String label) {
// Set the id (if needed) and write it back to the media stack. // Set the id (if needed) and write it back to the media stack.
var stack = media.stack().copy(); var stack = media.stack().copy();
mount = media.media().createDataMount(stack, (ServerLevel) level); mount = media.media().createDataMount(stack, (ServerLevel) getLevel());
updateMediaStack(stack, immediate); updateMediaStack(stack, immediate);
return mount; return mount;

View File

@ -4,7 +4,10 @@
package dan200.computercraft.shared.peripheral.modem; package dan200.computercraft.shared.peripheral.modem;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.phys.shapes.VoxelShape;
@ -22,4 +25,17 @@ public static VoxelShape getBounds(Direction facing) {
var direction = facing.ordinal(); var direction = facing.ordinal();
return direction < BOXES.length ? BOXES[direction] : Shapes.block(); return direction < BOXES.length ? BOXES[direction] : Shapes.block();
} }
/**
* Determine if a block can support a modem.
*
* @param level The current level.
* @param pos The position of the adjacent block.
* @param side The side the modem will be placed against.
* @return Whether this block can support a modem.
*/
public static boolean canSupport(LevelReader level, BlockPos pos, Direction side) {
// TODO(1.20.4): Check the side is a full-block instead.
return Block.canSupportCenter(level, pos, side);
}
} }

View File

@ -6,6 +6,7 @@
import dan200.computercraft.annotations.ForgeOverride; import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.peripheral.modem.ModemShapes;
import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.util.WaterloggableHelpers; import dan200.computercraft.shared.util.WaterloggableHelpers;
import dan200.computercraft.shared.util.WorldUtil; import dan200.computercraft.shared.util.WorldUtil;
@ -183,7 +184,7 @@ public BlockState updateShape(BlockState state, Direction side, BlockState other
// Pop our modem if needed. // Pop our modem if needed.
var dir = state.getValue(MODEM).getFacing(); var dir = state.getValue(MODEM).getFacing();
if (dir != null && dir.equals(side) && !canSupportCenter(level, otherPos, side.getOpposite())) { if (dir != null && dir.equals(side) && !ModemShapes.canSupport(level, otherPos, side.getOpposite())) {
// If we've no cable, follow normal Minecraft logic and just remove the block. // If we've no cable, follow normal Minecraft logic and just remove the block.
if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock(); if (!state.getValue(CABLE)) return getFluidState(state).createLegacyBlock();
@ -212,7 +213,7 @@ public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
var facing = state.getValue(MODEM).getFacing(); var facing = state.getValue(MODEM).getFacing();
if (facing == null) return true; if (facing == null) return true;
return canSupportCenter(world, pos.relative(facing), facing.getOpposite()); return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite());
} }
@Nullable @Nullable

View File

@ -107,7 +107,7 @@ private Direction getModemDirection() {
void neighborChanged(BlockPos neighbour) { void neighborChanged(BlockPos neighbour) {
var dir = getModemDirection(); var dir = getModemDirection();
if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) { if (!getLevel().isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
queueRefreshPeripheral(); queueRefreshPeripheral();
} }
} }
@ -163,7 +163,7 @@ private void updateBlockState() {
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral()); .from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
if (oldVariant != newVariant) { if (oldVariant != newVariant) {
level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant)); getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
} }
} }

View File

@ -84,7 +84,7 @@ public BlockState updateShape(BlockState state, Direction side, BlockState other
@Deprecated @Deprecated
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) { public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
var facing = state.getValue(FACING); var facing = state.getValue(FACING);
return canSupportCenter(world, pos.relative(facing), facing.getOpposite()); return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite());
} }
@Nullable @Nullable

View File

@ -56,8 +56,11 @@ public boolean pollTerminalChanged() {
void read(TerminalState state) { void read(TerminalState state) {
if (state.hasTerminal()) { if (state.hasTerminal()) {
if (terminal == null) terminal = new NetworkedTerminal(state.width, state.height, state.colour); if (terminal == null) {
state.apply(terminal); terminal = state.create();
} else {
state.apply(terminal);
}
terminalChanged = true; terminalChanged = true;
} else { } else {
if (terminal != null) { if (terminal != null) {

View File

@ -174,7 +174,7 @@ private ServerMonitor createServerMonitor() {
} else { } else {
// Otherwise fetch the origin and attempt to get its monitor // Otherwise fetch the origin and attempt to get its monitor
// Note this may load chunks, but we don't really have a choice here. // Note this may load chunks, but we don't really have a choice here.
var te = level.getBlockEntity(toWorldPos(0, 0)); var te = getLevel().getBlockEntity(toWorldPos(0, 0));
if (!(te instanceof MonitorBlockEntity monitor)) return null; if (!(te instanceof MonitorBlockEntity monitor)) return null;
return serverMonitor = monitor.createServerMonitor(); return serverMonitor = monitor.createServerMonitor();
@ -416,7 +416,7 @@ private void contractNeighbours() {
@Nullable @Nullable
private MonitorBlockEntity tryResizeAt(BlockPos pos, int width, int height) { private MonitorBlockEntity tryResizeAt(BlockPos pos, int width, int height) {
var tile = level.getBlockEntity(pos); var tile = getLevel().getBlockEntity(pos);
if (tile instanceof MonitorBlockEntity monitor && isCompatible(monitor)) { if (tile instanceof MonitorBlockEntity monitor && isCompatible(monitor)) {
monitor.resize(width, height); monitor.resize(width, height);
return monitor; return monitor;

View File

@ -19,15 +19,16 @@
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage; import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking; import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.util.PauseAwareTimer; import dan200.computercraft.shared.util.PauseAwareTimer;
import net.minecraft.ResourceLocationException;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.game.ClientboundSoundPacket; import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource; import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import net.minecraft.world.item.RecordItem;
import net.minecraft.world.level.block.state.properties.NoteBlockInstrument; import net.minecraft.world.level.block.state.properties.NoteBlockInstrument;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -252,15 +253,15 @@ public final boolean playSound(ILuaContext context, String name, Optional<Double
var volume = (float) clampVolume(checkFinite(1, volumeA.orElse(1.0))); var volume = (float) clampVolume(checkFinite(1, volumeA.orElse(1.0)));
var pitch = (float) checkFinite(2, pitchA.orElse(1.0)); var pitch = (float) checkFinite(2, pitchA.orElse(1.0));
ResourceLocation identifier; var identifier = ResourceLocation.tryParse(name);
try { if (identifier == null) throw new LuaException("Malformed sound name '" + name + "' ");
identifier = new ResourceLocation(name);
} catch (ResourceLocationException e) { // Prevent playing music discs.
throw new LuaException("Malformed sound name '" + name + "' "); var soundEvent = BuiltInRegistries.SOUND_EVENT.get(identifier);
} if (soundEvent != null && RecordItem.getBySound(soundEvent) != null) return false;
synchronized (lock) { synchronized (lock) {
if (dfpwmState != null && dfpwmState.isPlaying()) return false; if (pendingSound != null || (dfpwmState != null && dfpwmState.isPlaying())) return false;
dfpwmState = null; dfpwmState = null;
pendingSound = new PendingSound<>(identifier, volume, pitch); pendingSound = new PendingSound<>(identifier, volume, pitch);
return true; return true;

View File

@ -165,7 +165,7 @@ public Direction getDirection() {
public void setDirection(Direction dir) { public void setDirection(Direction dir) {
if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH; if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
level.setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir)); getLevel().setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));
updateRedstone(); updateRedstone();
updateInputsImmediately(); updateInputsImmediately();

View File

@ -11,7 +11,8 @@
import java.util.Random; import java.util.Random;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/** /**
* Tests {@link TerminalState} round tripping works as expected. * Tests {@link TerminalState} round tripping works as expected.
@ -42,6 +43,7 @@ private static NetworkedTerminal randomTerminal() {
private static void checkEqual(Terminal expected, Terminal actual) { private static void checkEqual(Terminal expected, Terminal actual) {
assertNotNull(expected, "Expected cannot be null"); assertNotNull(expected, "Expected cannot be null");
assertNotNull(actual, "Actual cannot be null"); assertNotNull(actual, "Actual cannot be null");
assertEquals(expected.isColour(), actual.isColour(), "isColour must match");
assertEquals(expected.getHeight(), actual.getHeight(), "Heights must match"); assertEquals(expected.getHeight(), actual.getHeight(), "Heights must match");
assertEquals(expected.getWidth(), actual.getWidth(), "Widths must match"); assertEquals(expected.getWidth(), actual.getWidth(), "Widths must match");
@ -51,13 +53,6 @@ private static void checkEqual(Terminal expected, Terminal actual) {
} }
private static NetworkedTerminal read(FriendlyByteBuf buffer) { private static NetworkedTerminal read(FriendlyByteBuf buffer) {
var state = new TerminalState(buffer); return new TerminalState(buffer).create();
assertTrue(state.colour);
if (!state.hasTerminal()) return null;
var other = new NetworkedTerminal(state.width, state.height, true);
state.apply(other);
return other;
} }
} }

View File

@ -34,6 +34,7 @@
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
@ -82,7 +83,7 @@ private static void export(Path root, ImageRenderer renderer) throws IOException
} }
// Now find all CC recipes. // Now find all CC recipes.
var level = Minecraft.getInstance().level; var level = Objects.requireNonNull(Minecraft.getInstance().level);
for (var recipe : level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) { for (var recipe : level.getRecipeManager().getAllRecipesFor(RecipeType.CRAFTING)) {
var result = recipe.value().getResultItem(level.registryAccess()); var result = recipe.value().getResultItem(level.registryAccess());
if (!RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, result.getItem()).getNamespace().equals(ComputerCraftAPI.MOD_ID)) { if (!RegistryHelper.getKeyOrThrow(BuiltInRegistries.ITEM, result.getItem()).getNamespace().equals(ComputerCraftAPI.MOD_ID)) {

View File

@ -41,7 +41,7 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!! val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.ON, pocketComputer.state) assertEquals(ComputerState.ON, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal!!
assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced") assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced")
} }
// Update the terminal contents again. // Update the terminal contents again.
@ -57,7 +57,7 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!! val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
assertEquals(ComputerState.BLINKING, pocketComputer.state) assertEquals(ComputerState.BLINKING, pocketComputer.state)
val term = pocketComputer.terminal val term = pocketComputer.terminal!!
assertEquals("Updated text :)", term.getLine(0).toString().trim(), "Terminal contents is synced") assertEquals("Updated text :)", term.getLine(0).toString().trim(), "Terminal contents is synced")
} }
} }

View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.gametest
import dan200.computercraft.gametest.api.sequence
import dan200.computercraft.gametest.api.thenOnComputer
import dan200.computercraft.gametest.api.tryMultipleTimes
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral
import dan200.computercraft.test.core.assertArrayEquals
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.sounds.SoundEvents
class Speaker_Test {
/**
* [SpeakerPeripheral.playSound] fails if there is already a sound queued.
*/
@GameTest
fun Fails_to_play_multiple_sounds(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
callPeripheral("right", "playSound", SoundEvents.NOTE_BLOCK_HARP.key().location().toString())
.assertArrayEquals(true)
tryMultipleTimes(2) { // We could technically call this a tick later, so try twice
callPeripheral("right", "playSound", SoundEvents.NOTE_BLOCK_HARP.key().location().toString())
.assertArrayEquals(false)
}
}
}
/**
* [SpeakerPeripheral.playSound] will not play records.
*/
@GameTest
fun Will_not_play_record(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
callPeripheral("right", "playSound", SoundEvents.MUSIC_DISC_PIGSTEP.location.toString())
.assertArrayEquals(false)
}
}
}

View File

@ -323,3 +323,16 @@ private fun getName(type: BlockEntityType<*>): ResourceLocation =
val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false) val hit = BlockHitResult(Vec3.atCenterOf(absolutePos), direction, absolutePos, false)
stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit)) stack.useOn(UseOnContext(player, InteractionHand.MAIN_HAND, hit))
} }
/**
* Run a function multiple times until it succeeds.
*/
inline fun tryMultipleTimes(count: Int, action: () -> Unit) {
for (remaining in count - 1 downTo 0) {
try {
action()
} catch (e: AssertionError) {
if (remaining == 0) throw e
}
}
}

View File

@ -87,6 +87,7 @@ fun onServerStarted(server: MinecraftServer) {
Printer_Test::class.java, Printer_Test::class.java,
Printout_Test::class.java, Printout_Test::class.java,
Recipe_Test::class.java, Recipe_Test::class.java,
Speaker_Test::class.java,
Turtle_Test::class.java, Turtle_Test::class.java,
) )

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{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:speaker{facing:north}", nbt: {id: "computercraft:speaker"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{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: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "speaker_test.fails_to_play_multiple_sounds", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{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: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{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, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft: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, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:speaker{facing:north}",
"computercraft:computer_normal{facing:north,state:on}"
]
}

View File

@ -0,0 +1,138 @@
{
DataVersion: 3465,
size: [5, 5, 5],
data: [
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "minecraft:air"},
{pos: [0, 1, 1], state: "minecraft:air"},
{pos: [0, 1, 2], state: "minecraft:air"},
{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:speaker{facing:north}", nbt: {id: "computercraft:speaker"}},
{pos: [1, 1, 3], state: "minecraft:air"},
{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: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "speaker_test.will_not_play_record", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "minecraft:air"},
{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: "minecraft:air"},
{pos: [3, 1, 3], state: "minecraft:air"},
{pos: [3, 1, 4], state: "minecraft:air"},
{pos: [4, 1, 0], state: "minecraft:air"},
{pos: [4, 1, 1], state: "minecraft:air"},
{pos: [4, 1, 2], state: "minecraft:air"},
{pos: [4, 1, 3], state: "minecraft:air"},
{pos: [4, 1, 4], state: "minecraft:air"},
{pos: [0, 2, 0], state: "minecraft:air"},
{pos: [0, 2, 1], state: "minecraft:air"},
{pos: [0, 2, 2], state: "minecraft:air"},
{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, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:air"},
{pos: [2, 2, 2], state: "minecraft:air"},
{pos: [2, 2, 3], state: "minecraft:air"},
{pos: [2, 2, 4], state: "minecraft: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, 4], state: "minecraft:air"},
{pos: [4, 2, 0], state: "minecraft:air"},
{pos: [4, 2, 1], state: "minecraft:air"},
{pos: [4, 2, 2], state: "minecraft:air"},
{pos: [4, 2, 3], state: "minecraft:air"},
{pos: [4, 2, 4], state: "minecraft:air"},
{pos: [0, 3, 0], state: "minecraft:air"},
{pos: [0, 3, 1], state: "minecraft:air"},
{pos: [0, 3, 2], state: "minecraft:air"},
{pos: [0, 3, 3], state: "minecraft:air"},
{pos: [0, 3, 4], state: "minecraft:air"},
{pos: [1, 3, 0], state: "minecraft:air"},
{pos: [1, 3, 1], state: "minecraft:air"},
{pos: [1, 3, 2], state: "minecraft:air"},
{pos: [1, 3, 3], state: "minecraft:air"},
{pos: [1, 3, 4], state: "minecraft:air"},
{pos: [2, 3, 0], state: "minecraft:air"},
{pos: [2, 3, 1], state: "minecraft:air"},
{pos: [2, 3, 2], state: "minecraft:air"},
{pos: [2, 3, 3], state: "minecraft:air"},
{pos: [2, 3, 4], state: "minecraft:air"},
{pos: [3, 3, 0], state: "minecraft:air"},
{pos: [3, 3, 1], state: "minecraft:air"},
{pos: [3, 3, 2], state: "minecraft:air"},
{pos: [3, 3, 3], state: "minecraft:air"},
{pos: [3, 3, 4], state: "minecraft:air"},
{pos: [4, 3, 0], state: "minecraft:air"},
{pos: [4, 3, 1], state: "minecraft:air"},
{pos: [4, 3, 2], state: "minecraft:air"},
{pos: [4, 3, 3], state: "minecraft:air"},
{pos: [4, 3, 4], state: "minecraft:air"},
{pos: [0, 4, 0], state: "minecraft:air"},
{pos: [0, 4, 1], state: "minecraft:air"},
{pos: [0, 4, 2], state: "minecraft:air"},
{pos: [0, 4, 3], state: "minecraft:air"},
{pos: [0, 4, 4], state: "minecraft:air"},
{pos: [1, 4, 0], state: "minecraft:air"},
{pos: [1, 4, 1], state: "minecraft:air"},
{pos: [1, 4, 2], state: "minecraft:air"},
{pos: [1, 4, 3], state: "minecraft:air"},
{pos: [1, 4, 4], state: "minecraft:air"},
{pos: [2, 4, 0], state: "minecraft:air"},
{pos: [2, 4, 1], state: "minecraft:air"},
{pos: [2, 4, 2], state: "minecraft:air"},
{pos: [2, 4, 3], state: "minecraft:air"},
{pos: [2, 4, 4], state: "minecraft:air"},
{pos: [3, 4, 0], state: "minecraft:air"},
{pos: [3, 4, 1], state: "minecraft:air"},
{pos: [3, 4, 2], state: "minecraft:air"},
{pos: [3, 4, 3], state: "minecraft:air"},
{pos: [3, 4, 4], state: "minecraft:air"},
{pos: [4, 4, 0], state: "minecraft:air"},
{pos: [4, 4, 1], state: "minecraft:air"},
{pos: [4, 4, 2], state: "minecraft:air"},
{pos: [4, 4, 3], state: "minecraft:air"},
{pos: [4, 4, 4], state: "minecraft:air"}
],
entities: [],
palette: [
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:speaker{facing:north}",
"computercraft:computer_normal{facing:north,state:on}"
]
}

View File

@ -143,27 +143,27 @@ public final void queueEvent(String name, IArguments args) throws LuaException {
/** /**
* Starts a timer that will run for the specified number of seconds. Once * Starts a timer that will run for the specified number of seconds. Once
* the timer fires, a {@code timer} event will be added to the queue with * the timer fires, a [`timer`] event will be added to the queue with the ID
* the ID returned from this function as the first parameter. * returned from this function as the first parameter.
* <p> * <p>
* As with [sleep][`os.sleep`], {@code timer} will automatically be rounded up * As with [sleep][`os.sleep`], the time will automatically be rounded up to
* to the nearest multiple of 0.05 seconds, as it waits for a fixed amount * the nearest multiple of 0.05 seconds, as it waits for a fixed amount of
* of world ticks. * world ticks.
* *
* @param timer The number of seconds until the timer fires. * @param time The number of seconds until the timer fires.
* @return The ID of the new timer. This can be used to filter the * @return The ID of the new timer. This can be used to filter the [`timer`]
* {@code timer} event, or {@link #cancelTimer cancel the timer}. * event, or {@linkplain #cancelTimer cancel the timer}.
* @throws LuaException If the time is below zero. * @throws LuaException If the time is below zero.
* @see #cancelTimer To cancel a timer. * @see #cancelTimer To cancel a timer.
*/ */
@LuaFunction @LuaFunction
public final int startTimer(double timer) throws LuaException { public final int startTimer(double time) throws LuaException {
return apiEnvironment.startTimer(Math.round(checkFinite(0, timer) / 0.05)); return apiEnvironment.startTimer(Math.round(checkFinite(0, time) / 0.05));
} }
/** /**
* Cancels a timer previously started with startTimer. This will stop the * Cancels a timer previously started with {@link #startTimer(double)}. This
* timer from firing. * will stop the timer from firing.
* *
* @param token The ID of the timer to cancel. * @param token The ID of the timer to cancel.
* @cc.since 1.6 * @cc.since 1.6
@ -399,10 +399,9 @@ public final long epoch(Optional<String> args) throws LuaException {
* Returns a date string (or table) using a specified format string and * Returns a date string (or table) using a specified format string and
* optional time to format. * optional time to format.
* <p> * <p>
* The format string takes the same formats as C's {@code strftime} function * The format string takes the same formats as C's [strftime](http://www.cplusplus.com/reference/ctime/strftime/)
* (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it * function. The format string can also be prefixed with an exclamation mark
* can be prefixed with an exclamation mark ({@code !}) to use UTC time * ({@code !}) to use UTC time instead of the server's local timezone.
* instead of the server's local timezone.
* <p> * <p>
* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a * If the format is exactly {@code *t} (optionally prefixed with {@code !}), a
* table will be returned instead. This table has fields for the year, month, * table will be returned instead. This table has fields for the year, month,

View File

@ -13,6 +13,11 @@
-- @module vector -- @module vector
-- @since 1.31 -- @since 1.31
local getmetatable = getmetatable
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local vmetatable
--- A 3-dimensional vector, with `x`, `y`, and `z` values. --- A 3-dimensional vector, with `x`, `y`, and `z` values.
-- --
-- This is suitable for representing both position and directional vectors. -- This is suitable for representing both position and directional vectors.
@ -27,6 +32,9 @@ local vector = {
-- @usage v1:add(v2) -- @usage v1:add(v2)
-- @usage v1 + v2 -- @usage v1 + v2
add = function(self, o) add = function(self, o)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
return vector.new( return vector.new(
self.x + o.x, self.x + o.x,
self.y + o.y, self.y + o.y,
@ -42,6 +50,9 @@ local vector = {
-- @usage v1:sub(v2) -- @usage v1:sub(v2)
-- @usage v1 - v2 -- @usage v1 - v2
sub = function(self, o) sub = function(self, o)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
return vector.new( return vector.new(
self.x - o.x, self.x - o.x,
self.y - o.y, self.y - o.y,
@ -52,30 +63,36 @@ local vector = {
--- Multiplies a vector by a scalar value. --- Multiplies a vector by a scalar value.
-- --
-- @tparam Vector self The vector to multiply. -- @tparam Vector self The vector to multiply.
-- @tparam number m The scalar value to multiply with. -- @tparam number factor The scalar value to multiply with.
-- @treturn Vector A vector with value `(x * m, y * m, z * m)`. -- @treturn Vector A vector with value `(x * m, y * m, z * m)`.
-- @usage v:mul(3) -- @usage vector.new(1, 2, 3):mul(3)
-- @usage v * 3 -- @usage vector.new(1, 2, 3) * 3
mul = function(self, m) mul = function(self, factor)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, factor, "number")
return vector.new( return vector.new(
self.x * m, self.x * factor,
self.y * m, self.y * factor,
self.z * m self.z * factor
) )
end, end,
--- Divides a vector by a scalar value. --- Divides a vector by a scalar value.
-- --
-- @tparam Vector self The vector to divide. -- @tparam Vector self The vector to divide.
-- @tparam number m The scalar value to divide by. -- @tparam number factor The scalar value to divide by.
-- @treturn Vector A vector with value `(x / m, y / m, z / m)`. -- @treturn Vector A vector with value `(x / m, y / m, z / m)`.
-- @usage v:div(3) -- @usage vector.new(1, 2, 3):div(3)
-- @usage v / 3 -- @usage vector.new(1, 2, 3) / 3
div = function(self, m) div = function(self, factor)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, factor, "number")
return vector.new( return vector.new(
self.x / m, self.x / factor,
self.y / m, self.y / factor,
self.z / m self.z / factor
) )
end, end,
@ -83,8 +100,9 @@ local vector = {
-- --
-- @tparam Vector self The vector to negate. -- @tparam Vector self The vector to negate.
-- @treturn Vector The negated vector. -- @treturn Vector The negated vector.
-- @usage -v -- @usage -vector.new(1, 2, 3)
unm = function(self) unm = function(self)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
return vector.new( return vector.new(
-self.x, -self.x,
-self.y, -self.y,
@ -99,6 +117,9 @@ local vector = {
-- @treturn Vector The dot product of `self` and `o`. -- @treturn Vector The dot product of `self` and `o`.
-- @usage v1:dot(v2) -- @usage v1:dot(v2)
dot = function(self, o) dot = function(self, o)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
return self.x * o.x + self.y * o.y + self.z * o.z return self.x * o.x + self.y * o.y + self.z * o.z
end, end,
@ -109,6 +130,9 @@ local vector = {
-- @treturn Vector The cross product of `self` and `o`. -- @treturn Vector The cross product of `self` and `o`.
-- @usage v1:cross(v2) -- @usage v1:cross(v2)
cross = function(self, o) cross = function(self, o)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
return vector.new( return vector.new(
self.y * o.z - self.z * o.y, self.y * o.z - self.z * o.y,
self.z * o.x - self.x * o.z, self.z * o.x - self.x * o.z,
@ -120,6 +144,7 @@ local vector = {
-- @tparam Vector self This vector. -- @tparam Vector self This vector.
-- @treturn number The length of this vector. -- @treturn number The length of this vector.
length = function(self) length = function(self)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
end, end,
@ -141,6 +166,9 @@ local vector = {
-- nearest 0.5. -- nearest 0.5.
-- @treturn Vector The rounded vector. -- @treturn Vector The rounded vector.
round = function(self, tolerance) round = function(self, tolerance)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, tolerance, "number", "nil")
tolerance = tolerance or 1.0 tolerance = tolerance or 1.0
return vector.new( return vector.new(
math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance, math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance,
@ -156,6 +184,8 @@ local vector = {
-- @usage v:tostring() -- @usage v:tostring()
-- @usage tostring(v) -- @usage tostring(v)
tostring = function(self) tostring = function(self)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
return self.x .. "," .. self.y .. "," .. self.z return self.x .. "," .. self.y .. "," .. self.z
end, end,
@ -165,11 +195,15 @@ local vector = {
-- @tparam Vector other The second vector to compare to. -- @tparam Vector other The second vector to compare to.
-- @treturn boolean Whether or not the vectors are equal. -- @treturn boolean Whether or not the vectors are equal.
equals = function(self, other) equals = function(self, other)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(other) ~= vmetatable then expect(2, other, "vector") end
return self.x == other.x and self.y == other.y and self.z == other.z return self.x == other.x and self.y == other.y and self.z == other.z
end, end,
} }
local vmetatable = { vmetatable = {
__name = "vector",
__index = vector, __index = vector,
__add = vector.add, __add = vector.add,
__sub = vector.sub, __sub = vector.sub,

View File

@ -1,3 +1,22 @@
# New features in CC: Tweaked 1.110.2
* Add `speaker sound` command (fatboychummy).
Several bug fixes:
* Improve error when calling `speaker play` with no path (fatboychummy).
* Prevent playing music discs with `speaker.playSound`.
* Various documentation fixes (cyberbit).
* Fix generic peripherals not being able to transfer to some inventories on Forge.
* Fix rare crash when holding a pocket computer.
* Fix modems breaking when moved by Create.
* Fix crash when rendering a turtle through an Immersive Portals portal.
# New features in CC: Tweaked 1.110.1
Several bug fixes:
* Fix computers not turning on after they're unloaded/not-ticked for a while.
* Fix networking cables sometimes not connecting on Forge.
# New features in CC: Tweaked 1.110.0 # New features in CC: Tweaked 1.110.0
* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command. * Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command.

View File

@ -1,16 +1,14 @@
New features in CC: Tweaked 1.110.0 New features in CC: Tweaked 1.110.2
* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command. * Add `speaker sound` command (fatboychummy).
* Remove custom breaking progress of modems on Forge.
Several bug fixes: Several bug fixes:
* Fix client and server DFPWM transcoders getting out of sync. * Improve error when calling `speaker play` with no path (fatboychummy).
* Fix `turtle.suck` reporting incorrect error when failing to suck items. * Prevent playing music discs with `speaker.playSound`.
* Fix pocket computers displaying state (blinking, modem light) for the wrong computer. * Various documentation fixes (cyberbit).
* Fix crash when wrapping an invalid BE as a generic peripheral. * Fix generic peripherals not being able to transfer to some inventories on Forge.
* Chest peripherals now reattach when a chest is converted into a double chest. * Fix rare crash when holding a pocket computer.
* Fix `speaker` program not resolving files relative to the current directory. * Fix modems breaking when moved by Create.
* Skip main-thread tasks if the peripheral is detached. * Fix crash when rendering a turtle through an Immersive Portals portal.
* Fix internal Lua VM errors if yielding inside `__tostring`.
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -89,7 +89,7 @@ end
-- --
-- @tparam table image An image, as returned from [`load`] or [`parse`]. -- @tparam table image An image, as returned from [`load`] or [`parse`].
-- @tparam number xPos The x position to start drawing at. -- @tparam number xPos The x position to start drawing at.
-- @tparam number xPos The y position to start drawing at. -- @tparam number yPos The y position to start drawing at.
-- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the -- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the
-- current terminal. -- current terminal.
local function draw(image, xPos, yPos, target) local function draw(image, xPos, yPos, target)

View File

@ -43,6 +43,10 @@ if cmd == "stop" then
for _, speaker in pairs(get_speakers(name)) do speaker.stop() end for _, speaker in pairs(get_speakers(name)) do speaker.stop() end
elseif cmd == "play" then elseif cmd == "play" then
local _, file, name = ... local _, file, name = ...
if not file then
error("Usage: speaker play <file or url> [speaker]", 0)
end
local speaker = get_speakers(name)[1] local speaker = get_speakers(name)[1]
local handle, err local handle, err
@ -128,9 +132,47 @@ elseif cmd == "play" then
end end
handle.close() handle.close()
elseif cmd == "sound" then
local _, sound, volume, pitch, name = ...
if not sound then
error("Usage: speaker sound <sound> [volume] [pitch] [speaker]", 0)
return
end
if volume then
volume = tonumber(volume)
if not volume then
error("Volume must be a number", 0)
end
if volume < 0 or volume > 3 then
error("Volume must be between 0 and 3", 0)
end
end
if pitch then
pitch = tonumber(pitch)
if not pitch then
error("Pitch must be a number", 0)
end
if pitch < 0 or pitch > 2 then
error("Pitch must be between 0 and 2", 0)
end
end
local speaker = get_speakers(name)[1]
if speaker.playSound(sound, volume, pitch) then
print(("Played sound %q on speaker %q with volume %s and pitch %s."):format(
sound, peripheral.getName(speaker), volume or 1, pitch or 1
))
else
error(("Could not play sound %q"):format(sound), 0)
end
else else
local programName = arg[0] or fs.getName(shell.getRunningProgram()) local programName = arg[0] or fs.getName(shell.getRunningProgram())
print("Usage:") print("Usage:")
print(programName .. " play <file or url> [speaker]") print(programName .. " play <file or url> [speaker]")
print(programName .. " sound <sound> [volume] [pitch] [speaker]")
print(programName .. " stop [speaker]") print(programName .. " stop [speaker]")
end end

View File

@ -117,7 +117,7 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build(
completion.peripheral completion.peripheral
)) ))
shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build( shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build(
{ completion.choice, { "play ", "stop " } }, { completion.choice, { "play ", "sound ", "stop " } },
function(shell, text, previous) function(shell, text, previous)
if previous[2] == "play" then return completion.file(shell, text, previous, true) if previous[2] == "play" then return completion.file(shell, text, previous, true)
elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false) elseif previous[2] == "stop" then return completion.peripheral(shell, text, previous, false)

View File

@ -0,0 +1,71 @@
-- SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
describe("The vector library", function()
local vec = vector.new(1, 2, 3)
describe("vector.add", function()
it("validates arguments", function()
expect.error(vec.add, nil, vec):eq("bad argument #1 (vector expected, got nil)")
expect.error(vec.add, vec, nil):eq("bad argument #2 (vector expected, got nil)")
end)
it("returns the correct value", function()
expect(vector.new(1, 2, 3) + vector.new(6, 4, 2)):eq(vector.new(7, 6, 5))
end)
end)
describe("vector.sub", function()
it("validates arguments", function()
expect.error(vec.sub, nil, vec):eq("bad argument #1 (vector expected, got nil)")
expect.error(vec.sub, vec, nil):eq("bad argument #2 (vector expected, got nil)")
end)
it("returns the correct value", function()
expect(vector.new(6, 4, 2) - vector.new(1, 2, 3)):eq(vector.new(5, 2, -1))
end)
end)
describe("vector.mul", function()
it("validates arguments", function()
expect.error(vec.mul, nil, vec):eq("bad argument #1 (vector expected, got nil)")
expect.error(vec.mul, vec, nil):eq("bad argument #2 (number expected, got nil)")
end)
it("returns the correct value", function()
expect(vector.new(1, 2, 3) * 2):eq(vector.new(2, 4, 6))
end)
end)
describe("vector.div", function()
it("validates arguments", function()
expect.error(vec.div, nil, vec):eq("bad argument #1 (vector expected, got nil)")
expect.error(vec.div, vec, nil):eq("bad argument #2 (number expected, got nil)")
end)
it("returns the correct value", function()
expect(vector.new(1, 2, 3) / 2):eq(vector.new(0.5, 1, 1.5))
end)
end)
describe("vector.unm", function()
it("validates arguments", function()
expect.error(vec.unm, nil):eq("bad argument #1 (vector expected, got nil)")
end)
it("returns the correct value", function()
expect(-vector.new(2, 3, 6)):eq(vector.new(-2, -3, -6))
end)
end)
describe("vector.length", function()
it("validates arguments", function()
expect.error(vec.length, nil):eq("bad argument #1 (vector expected, got nil)")
end)
it("returns the correct value", function()
expect(vector.new(2, 3, 6):length()):eq(7)
end)
end)
end)

View File

@ -33,6 +33,8 @@
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.HitResult;
import java.util.Objects;
import static dan200.computercraft.core.util.Nullability.assertNonNull; import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class ComputerCraftClient { public class ComputerCraftClient {
@ -79,7 +81,7 @@ public static void init() {
if (hit.getType() != HitResult.Type.BLOCK) return ItemStack.EMPTY; if (hit.getType() != HitResult.Type.BLOCK) return ItemStack.EMPTY;
var pos = ((BlockHitResult) hit).getBlockPos(); var pos = ((BlockHitResult) hit).getBlockPos();
var level = Minecraft.getInstance().level; var level = Objects.requireNonNull(Minecraft.getInstance().level);
var state = level.getBlockState(pos); var state = level.getBlockState(pos);
if (!(state.getBlock() instanceof CableBlock cable)) return ItemStack.EMPTY; if (!(state.getBlock() instanceof CableBlock cable)) return ItemStack.EMPTY;

View File

@ -0,0 +1,4 @@
{
"replace": false,
"values": ["computercraft:cable", "computercraft:wireless_modem_normal", "computercraft:wireless_modem_advanced"]
}

View File

@ -0,0 +1 @@
{"values": ["computercraft:cable", "computercraft:wireless_modem_normal", "computercraft:wireless_modem_advanced"]}

View File

@ -10,6 +10,7 @@
import dan200.computercraft.impl.detail.DetailRegistryImpl; import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.shared.details.FluidData; import dan200.computercraft.shared.details.FluidData;
import dan200.computercraft.shared.peripheral.generic.ComponentLookup; import dan200.computercraft.shared.peripheral.generic.ComponentLookup;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
@ -60,7 +61,7 @@ private record CapabilityLookup<T>(BlockCapability<T, Direction> capability) imp
@Nullable @Nullable
@Override @Override
public T find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side) { public T find(ServerLevel level, BlockPos pos, BlockState state, BlockEntity blockEntity, Direction side) {
return level.getCapability(capability, pos, state, blockEntity, side); return CapabilityUtil.getCapability(level, capability, pos, state, blockEntity, side);
} }
} }
} }

View File

@ -8,6 +8,8 @@
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
@ -53,7 +55,7 @@ public int pushFluid(
var location = computer.getAvailablePeripheral(toName); var location = computer.getAvailablePeripheral(toName);
if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); if (location == null) throw new LuaException("Target '" + toName + "' does not exist");
var to = extractHandler(location.getTarget()); var to = extractHandler(location);
if (to == null) throw new LuaException("Target '" + toName + "' is not an tank"); if (to == null) throw new LuaException("Target '" + toName + "' is not an tank");
int actualLimit = limit.orElse(Integer.MAX_VALUE); int actualLimit = limit.orElse(Integer.MAX_VALUE);
@ -78,7 +80,7 @@ public int pullFluid(
var location = computer.getAvailablePeripheral(fromName); var location = computer.getAvailablePeripheral(fromName);
if (location == null) throw new LuaException("Target '" + fromName + "' does not exist"); if (location == null) throw new LuaException("Target '" + fromName + "' does not exist");
var from = extractHandler(location.getTarget()); var from = extractHandler(location);
if (from == null) throw new LuaException("Target '" + fromName + "' is not an tank"); if (from == null) throw new LuaException("Target '" + fromName + "' is not an tank");
int actualLimit = limit.orElse(Integer.MAX_VALUE); int actualLimit = limit.orElse(Integer.MAX_VALUE);
@ -90,14 +92,17 @@ public int pullFluid(
} }
@Nullable @Nullable
private static IFluidHandler extractHandler(@Nullable Object object) { private static IFluidHandler extractHandler(IPeripheral peripheral) {
var object = peripheral.getTarget();
var direction = peripheral instanceof dan200.computercraft.shared.peripheral.generic.GenericPeripheral sided ? sided.side() : null;
if (object instanceof BlockEntity blockEntity) { if (object instanceof BlockEntity blockEntity) {
if (blockEntity.isRemoved()) return null; if (blockEntity.isRemoved()) return null;
var level = blockEntity.getLevel(); var level = blockEntity.getLevel();
if (!(level instanceof ServerLevel serverLevel)) return null; if (!(level instanceof ServerLevel serverLevel)) return null;
var result = serverLevel.getCapability(Capabilities.FluidHandler.BLOCK, blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, null); var result = CapabilityUtil.getCapability(serverLevel, Capabilities.FluidHandler.BLOCK, blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, direction);
if (result != null) return result; if (result != null) return result;
} }

View File

@ -8,7 +8,9 @@
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.platform.ForgeContainerTransfer; import dan200.computercraft.shared.platform.ForgeContainerTransfer;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
@ -73,7 +75,7 @@ public int pushItems(
var location = computer.getAvailablePeripheral(toName); var location = computer.getAvailablePeripheral(toName);
if (location == null) throw new LuaException("Target '" + toName + "' does not exist"); if (location == null) throw new LuaException("Target '" + toName + "' does not exist");
var to = extractHandler(location.getTarget()); var to = extractHandler(location);
if (to == null) throw new LuaException("Target '" + toName + "' is not an inventory"); if (to == null) throw new LuaException("Target '" + toName + "' is not an inventory");
// Validate slots // Validate slots
@ -95,7 +97,7 @@ public int pullItems(
var location = computer.getAvailablePeripheral(fromName); var location = computer.getAvailablePeripheral(fromName);
if (location == null) throw new LuaException("Source '" + fromName + "' does not exist"); if (location == null) throw new LuaException("Source '" + fromName + "' does not exist");
var from = extractHandler(location.getTarget()); var from = extractHandler(location);
if (from == null) throw new LuaException("Source '" + fromName + "' is not an inventory"); if (from == null) throw new LuaException("Source '" + fromName + "' is not an inventory");
// Validate slots // Validate slots
@ -108,14 +110,17 @@ public int pullItems(
} }
@Nullable @Nullable
private static IItemHandler extractHandler(@Nullable Object object) { private static IItemHandler extractHandler(IPeripheral peripheral) {
var object = peripheral.getTarget();
var direction = peripheral instanceof dan200.computercraft.shared.peripheral.generic.GenericPeripheral sided ? sided.side() : null;
if (object instanceof BlockEntity blockEntity) { if (object instanceof BlockEntity blockEntity) {
if (blockEntity.isRemoved()) return null; if (blockEntity.isRemoved()) return null;
var level = blockEntity.getLevel(); var level = blockEntity.getLevel();
if (!(level instanceof ServerLevel serverLevel)) return null; if (!(level instanceof ServerLevel serverLevel)) return null;
var result = serverLevel.getCapability(Capabilities.ItemHandler.BLOCK, blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, null); var result = CapabilityUtil.getCapability(serverLevel, Capabilities.ItemHandler.BLOCK, blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity, direction);
if (result != null) return result; if (result != null) return result;
} }

View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.capabilities.BlockCapability;
import org.jetbrains.annotations.Nullable;
public final class CapabilityUtil {
private CapabilityUtil() {
}
/**
* Find a capability, preferring the internal/null side but falling back to a given side if a mod doesn't support
* the internal one.
*
* @param level The server level.
* @param capability The capability to get.
* @param pos The block position.
* @param state The block state.
* @param blockEntity The block entity.
* @param side The side we'll fall back to.
* @param <T> The type of the underlying capability.
* @return The extracted capability, if present.
*/
public static <T> @Nullable T getCapability(ServerLevel level, BlockCapability<T, @Nullable Direction> capability, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, @Nullable Direction side) {
var cap = level.getCapability(capability, pos, state, blockEntity, null);
return cap == null && side != null ? level.getCapability(capability, pos, state, blockEntity, side) : cap;
}
}

View File

@ -7,6 +7,7 @@
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.common.collect.ImmutableSetMultimap import com.google.common.collect.ImmutableSetMultimap
import com.uber.nullaway.LibraryModels import com.uber.nullaway.LibraryModels
import com.uber.nullaway.LibraryModels.FieldRef.fieldRef
import com.uber.nullaway.LibraryModels.MethodRef.methodRef import com.uber.nullaway.LibraryModels.MethodRef.methodRef
/** /**
@ -34,4 +35,9 @@ override fun nonNullReturns(): ImmutableSet<LibraryModels.MethodRef> = Immutable
// Reasoning about nullability of BlockEntity.getLevel() is awkward. For now, assume it's non-null. // Reasoning about nullability of BlockEntity.getLevel() is awkward. For now, assume it's non-null.
methodRef("net.minecraft.world.level.block.entity.BlockEntity", "getLevel()"), methodRef("net.minecraft.world.level.block.entity.BlockEntity", "getLevel()"),
) )
override fun nullableFields(): ImmutableSet<LibraryModels.FieldRef> = ImmutableSet.of(
// This inherits from Minecraft.hitResult, and so can also be null.
fieldRef("net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher", "cameraHitResult"),
)
} }

View File

@ -5,11 +5,16 @@
package cc.tweaked.standalone; package cc.tweaked.standalone;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.core.ComputerContext; import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.CoreConfig; import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.http.options.Action; import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.AddressRule; import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer; import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
@ -31,6 +36,7 @@
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.OptionalInt; import java.util.OptionalInt;
@ -55,37 +61,58 @@ public class Main {
private static final Logger LOG = LoggerFactory.getLogger(Main.class); private static final Logger LOG = LoggerFactory.getLogger(Main.class);
private static final boolean DEBUG = Checks.DEBUG; private static final boolean DEBUG = Checks.DEBUG;
private record TermSize(int width, int height) { private static Path parsePath(String path) throws ParseException {
public static final TermSize DEFAULT = new TermSize(51, 19); try {
public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$"); return Path.of(path);
} } catch (InvalidPathException e) {
throw new ParseException("'" + path + "' is not a valid path (" + e.getReason() + ")");
private static <T> T getParsedOptionValue(CommandLine cli, Option opt, Class<T> klass) throws ParseException {
var res = cli.getOptionValue(opt);
if (klass == Path.class) {
try {
return klass.cast(Path.of(res));
} catch (InvalidPathException e) {
throw new ParseException("'" + res + "' is not a valid path (" + e.getReason() + ")");
}
} else if (klass == TermSize.class) {
var matcher = TermSize.PATTERN.matcher(res);
if (!matcher.matches()) throw new ParseException("'" + res + "' is not a valid terminal size.");
return klass.cast(new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))));
} else {
return klass.cast(TypeHandler.createValue(res, klass));
} }
} }
private record TermSize(int width, int height) {
public static final TermSize DEFAULT = new TermSize(51, 19);
public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$");
public static TermSize parse(String value) throws ParseException {
var matcher = TermSize.PATTERN.matcher(value);
if (!matcher.matches()) throw new ParseException("'" + value + "' is not a valid terminal size.");
return new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
}
}
private record MountPaths(Path src, String dest) {
public static final Pattern PATTERN = Pattern.compile("^([^:]+):([^:]+)$");
public static MountPaths parse(String value) throws ParseException {
var matcher = MountPaths.PATTERN.matcher(value);
if (!matcher.matches()) throw new ParseException("'" + value + "' is not a mount spec.");
return new MountPaths(parsePath(matcher.group(1)), matcher.group(2));
}
}
private interface ValueParser<T> {
T parse(String path) throws ParseException;
}
@Contract("_, _, _, !null -> !null") @Contract("_, _, _, !null -> !null")
private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, Class<T> klass, @Nullable T defaultValue) throws ParseException { private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, ValueParser<T> parser, @Nullable T defaultValue) throws ParseException {
return cli.hasOption(opt) ? getParsedOptionValue(cli, opt, klass) : defaultValue; return cli.hasOption(opt) ? parser.parse(cli.getOptionValue(opt)) : defaultValue;
}
private static <T> List<T> getParsedOptionValues(CommandLine cli, Option opt, ValueParser<T> parser) throws ParseException {
var values = cli.getOptionValues(opt);
if (values == null) return List.of();
List<T> parsedValues = new ArrayList<>(values.length);
for (var value : values) parsedValues.add(parser.parse(value));
return List.copyOf(parsedValues);
} }
public static void main(String[] args) throws InterruptedException { public static void main(String[] args) throws InterruptedException {
var options = new Options(); var options = new Options();
Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt; Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt, mountOpt, mountRoOpt;
options.addOption(resourceOpt = Option.builder("r").argName("PATH").longOpt("resources").hasArg() options.addOption(resourceOpt = Option.builder("r").argName("PATH").longOpt("resources").hasArg()
.desc("The path to the resources directory") .desc("The path to the resources directory")
.build()); .build());
@ -98,6 +125,12 @@ public static void main(String[] args) throws InterruptedException {
options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains") options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains")
.desc("Allow accessing local domains with the HTTP API.") .desc("Allow accessing local domains with the HTTP API.")
.build()); .build());
options.addOption(mountOpt = Option.builder().longOpt("mount").hasArg().argName("SRC:DEST")
.desc("Mount a folder SRC at directory DEST on the computer.")
.build());
options.addOption(mountRoOpt = Option.builder().longOpt("mount-ro").hasArg().argName("SRC:DEST")
.desc("Mount a read-only folder SRC at directory DEST on the computer.")
.build());
options.addOption(helpOpt = Option.builder("h").longOpt("help") options.addOption(helpOpt = Option.builder("h").longOpt("help")
.desc("Print help message") .desc("Print help message")
@ -107,6 +140,7 @@ public static void main(String[] args) throws InterruptedException {
Path computerDirectory; Path computerDirectory;
TermSize termSize; TermSize termSize;
boolean allowLocalDomains; boolean allowLocalDomains;
List<MountPaths> mounts, readOnlyMounts;
try { try {
var cli = new DefaultParser().parse(options, args); var cli = new DefaultParser().parse(options, args);
if (cli.hasOption(helpOpt)) { if (cli.hasOption(helpOpt)) {
@ -115,10 +149,12 @@ public static void main(String[] args) throws InterruptedException {
} }
if (!cli.hasOption(resourceOpt)) throw new ParseException("--resources directory is required"); if (!cli.hasOption(resourceOpt)) throw new ParseException("--resources directory is required");
resourcesDirectory = getParsedOptionValue(cli, resourceOpt, Path.class); resourcesDirectory = parsePath(cli.getOptionValue(resourceOpt));
computerDirectory = getParsedOptionValue(cli, computerOpt, Path.class, null); computerDirectory = getParsedOptionValue(cli, computerOpt, Main::parsePath, null);
termSize = getParsedOptionValue(cli, termSizeOpt, TermSize.class, TermSize.DEFAULT); termSize = getParsedOptionValue(cli, termSizeOpt, TermSize::parse, TermSize.DEFAULT);
allowLocalDomains = cli.hasOption(allowLocalDomainsOpt); allowLocalDomains = cli.hasOption(allowLocalDomainsOpt);
mounts = getParsedOptionValues(cli, mountOpt, MountPaths::parse);
readOnlyMounts = getParsedOptionValues(cli, mountRoOpt, MountPaths::parse);
} catch (ParseException e) { } catch (ParseException e) {
System.err.println(e.getLocalizedMessage()); System.err.println(e.getLocalizedMessage());
@ -143,6 +179,7 @@ public static void main(String[] args) throws InterruptedException {
new Terminal(termSize.width(), termSize.height(), true, () -> isDirty.set(true)), new Terminal(termSize.width(), termSize.height(), true, () -> isDirty.set(true)),
0 0
); );
computer.addApi(new FileMounter(computer.getAPIEnvironment(), readOnlyMounts, mounts));
computer.turnOn(); computer.turnOn();
runAndInit(gl, computer, isDirty); runAndInit(gl, computer, isDirty);
@ -154,6 +191,41 @@ public static void main(String[] args) throws InterruptedException {
} }
} }
/**
* An {@link ILuaAPI} which is used to mount additional files, but does not expose any new globals/methods.
*/
private static final class FileMounter implements ILuaAPI {
private final IAPIEnvironment environment;
private final List<MountPaths> readOnlyMounts;
private final List<MountPaths> mounts;
FileMounter(IAPIEnvironment environment, List<MountPaths> readOnlyMounts, List<MountPaths> mounts) {
this.environment = environment;
this.readOnlyMounts = readOnlyMounts;
this.mounts = mounts;
}
@Override
public String[] getNames() {
return new String[0];
}
@Override
public void startup() {
try {
var fs = environment.getFileSystem();
for (var mount : readOnlyMounts) {
fs.mount(mount.dest(), mount.dest(), new FileMount(mount.src()));
}
for (var mount : mounts) {
fs.mount(mount.dest(), mount.dest(), new WritableFileMount(mount.src().toFile(), 1_000_000));
}
} catch (FileSystemException e) {
throw new IllegalStateException(e);
}
}
}
private static final int SCALE = 2; private static final int SCALE = 2;
private static final int MARGIN = 2; private static final int MARGIN = 2;
private static final int PIXEL_WIDTH = 6; private static final int PIXEL_WIDTH = 6;