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

View File

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

View File

@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties
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
mcVersion=1.20.4

View File

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

View File

@ -9,6 +9,7 @@
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.network.ClientNetworking;
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.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
@ -18,6 +19,7 @@
import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
@ -96,8 +98,8 @@ public void containerTick() {
getTerminal().update();
if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
new ItemToast(minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
.showOrReplace(minecraft.getToasts());
new ItemToast(minecraft(), displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
.showOrReplace(minecraft().getToasts());
uploadNagDeadline = Long.MAX_VALUE;
}
}
@ -206,7 +208,7 @@ public void onFilesDrop(List<Path> files) {
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) {
@ -222,9 +224,13 @@ public void uploadResult(UploadResult result, @Nullable Component message) {
}
private void alert(Component title, Component message) {
OptionScreen.show(minecraft, title, message,
List.of(OptionScreen.newButton(OK, b -> minecraft.setScreen(this))),
() -> minecraft.setScreen(this)
OptionScreen.show(minecraft(), title, message,
List.of(OptionScreen.newButton(OK, b -> 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.core.terminal.Terminal;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.MenuAccess;
@ -16,6 +18,7 @@
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nullable;
import java.util.Objects;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
@ -44,8 +47,8 @@ public T getMenu() {
protected void init() {
// First ensure we're still grabbing the mouse, so the user can look around. Then reset bits of state that
// grabbing unsets.
minecraft.mouseHandler.grabMouse();
minecraft.screen = this;
minecraft().mouseHandler.grabMouse();
minecraft().screen = this;
KeyMapping.releaseAll();
super.init();
@ -64,13 +67,13 @@ public final void tick() {
@Override
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);
}
@Override
public void onClose() {
minecraft.player.closeContainer();
Objects.requireNonNull(minecraft().player).closeContainer();
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) {
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 y = 10;
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;
}
}
private Minecraft minecraft() {
return Nullability.assertNonNull(minecraft);
}
}

View File

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

View File

@ -195,16 +195,16 @@ public boolean mouseDragged(double mouseX, double mouseY, int button, double v2,
}
@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 (!hasMouseSupport() || delta == 0) return false;
if (!hasMouseSupport() || deltaY == 0) return false;
var charX = (int) ((mouseX - innerX) / FONT_WIDTH);
var charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min(Math.max(charX, 0), terminal.getWidth() - 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;
lastMouseY = charY;

View File

@ -6,7 +6,6 @@
import dan200.computercraft.shared.computer.core.ComputerState;
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.network.client.PocketComputerDataMessage;
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) {
var computer = instances.get(instanceId);
if (computer == null) {
var terminal = new NetworkedTerminal(terminalData.width, terminalData.height, terminalData.colour);
instances.put(instanceId, computer = new PocketComputerData(state, lightColour, terminal));
instances.put(instanceId, new PocketComputerData(state, lightColour, terminalData));
} else {
computer.setState(state, lightColour);
computer.setState(state, lightColour, terminalData);
}
if (terminalData.hasTerminal()) terminalData.apply(computer.getTerminal());
}
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.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import javax.annotation.Nullable;
/**
* Clientside data about a pocket computer.
* <p>
@ -19,21 +22,21 @@
* @see PocketServerComputer The server-side pocket computer.
*/
public final class PocketComputerData {
private final NetworkedTerminal terminal;
private @Nullable NetworkedTerminal terminal;
private ComputerState state;
private int lightColour;
PocketComputerData(ComputerState state, int lightColour, NetworkedTerminal terminal) {
PocketComputerData(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state;
this.lightColour = lightColour;
this.terminal = terminal;
if (terminalData.hasTerminal()) terminal = terminalData.create();
}
public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1;
}
public NetworkedTerminal getTerminal() {
public @Nullable NetworkedTerminal getTerminal() {
return terminal;
}
@ -41,8 +44,16 @@ public ComputerState getState() {
return state;
}
void setState(ComputerState state, int lightColour) {
void setState(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state;
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.ItemStack;
import java.util.Objects;
/**
* 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);
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();
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
var label = turtle.getLabel();
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 font = this.font;

View File

@ -8,6 +8,7 @@
import dan200.computercraft.impl.RegistryHelper;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.Registry;
import dan200.computercraft.shared.integration.ExternalModTags;
import net.minecraft.data.tags.ItemTagsProvider;
import net.minecraft.data.tags.TagsProvider;
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(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) {

View File

@ -223,7 +223,7 @@ private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPo
var offsetSide = dir.getOpposite();
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));
}

View File

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

View File

@ -18,10 +18,9 @@
* states, etc...
*/
public class TerminalState {
public final boolean colour;
public final int width;
public final int height;
private final boolean colour;
private final int width;
private final int height;
@Nullable
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.
var stack = media.stack().copy();
mount = media.media().createDataMount(stack, (ServerLevel) level);
mount = media.media().createDataMount(stack, (ServerLevel) getLevel());
updateMediaStack(stack, immediate);
return mount;

View File

@ -4,7 +4,10 @@
package dan200.computercraft.shared.peripheral.modem;
import net.minecraft.core.BlockPos;
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.VoxelShape;
@ -22,4 +25,17 @@ public static VoxelShape getBounds(Direction facing) {
var direction = facing.ordinal();
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.shared.ModRegistry;
import dan200.computercraft.shared.peripheral.modem.ModemShapes;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.util.WaterloggableHelpers;
import dan200.computercraft.shared.util.WorldUtil;
@ -183,7 +184,7 @@ public BlockState updateShape(BlockState state, Direction side, BlockState other
// Pop our modem if needed.
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 (!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();
if (facing == null) return true;
return canSupportCenter(world, pos.relative(facing), facing.getOpposite());
return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite());
}
@Nullable

View File

@ -107,7 +107,7 @@ private Direction getModemDirection() {
void neighborChanged(BlockPos neighbour) {
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();
}
}
@ -163,7 +163,7 @@ private void updateBlockState() {
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
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
public boolean canSurvive(BlockState state, LevelReader world, BlockPos pos) {
var facing = state.getValue(FACING);
return canSupportCenter(world, pos.relative(facing), facing.getOpposite());
return ModemShapes.canSupport(world, pos.relative(facing), facing.getOpposite());
}
@Nullable

View File

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

View File

@ -174,7 +174,7 @@ private ServerMonitor createServerMonitor() {
} else {
// Otherwise fetch the origin and attempt to get its monitor
// 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;
return serverMonitor = monitor.createServerMonitor();
@ -416,7 +416,7 @@ private void contractNeighbours() {
@Nullable
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)) {
monitor.resize(width, height);
return monitor;

View File

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

View File

@ -165,7 +165,7 @@ public Direction getDirection() {
public void setDirection(Direction dir) {
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();
updateInputsImmediately();

View File

@ -11,7 +11,8 @@
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.
@ -42,6 +43,7 @@ private static NetworkedTerminal randomTerminal() {
private static void checkEqual(Terminal expected, Terminal actual) {
assertNotNull(expected, "Expected 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.getWidth(), actual.getWidth(), "Widths must match");
@ -51,13 +53,6 @@ private static void checkEqual(Terminal expected, Terminal actual) {
}
private static NetworkedTerminal read(FriendlyByteBuf buffer) {
var state = new TerminalState(buffer);
assertTrue(state.colour);
if (!state.hasTerminal()) return null;
var other = new NetworkedTerminal(state.width, state.height, true);
state.apply(other);
return other;
return new TerminalState(buffer).create();
}
}

View File

@ -34,6 +34,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
@ -82,7 +83,7 @@ private static void export(Path root, ImageRenderer renderer) throws IOException
}
// 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)) {
var result = recipe.value().getResultItem(level.registryAccess());
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)!!
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")
}
// Update the terminal contents again.
@ -57,7 +57,7 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)!!
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")
}
}

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)
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,
Printout_Test::class.java,
Recipe_Test::class.java,
Speaker_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
* the timer fires, a {@code timer} event will be added to the queue with
* the ID returned from this function as the first parameter.
* the timer fires, a [`timer`] event will be added to the queue with the ID
* returned from this function as the first parameter.
* <p>
* As with [sleep][`os.sleep`], {@code timer} will automatically be rounded up
* to the nearest multiple of 0.05 seconds, as it waits for a fixed amount
* of world ticks.
* As with [sleep][`os.sleep`], the time will automatically be rounded up to
* the nearest multiple of 0.05 seconds, as it waits for a fixed amount of
* world ticks.
*
* @param timer The number of seconds until the timer fires.
* @return The ID of the new timer. This can be used to filter the
* {@code timer} event, or {@link #cancelTimer cancel the timer}.
* @param time The number of seconds until the timer fires.
* @return The ID of the new timer. This can be used to filter the [`timer`]
* event, or {@linkplain #cancelTimer cancel the timer}.
* @throws LuaException If the time is below zero.
* @see #cancelTimer To cancel a timer.
*/
@LuaFunction
public final int startTimer(double timer) throws LuaException {
return apiEnvironment.startTimer(Math.round(checkFinite(0, timer) / 0.05));
public final int startTimer(double time) throws LuaException {
return apiEnvironment.startTimer(Math.round(checkFinite(0, time) / 0.05));
}
/**
* Cancels a timer previously started with startTimer. This will stop the
* timer from firing.
* Cancels a timer previously started with {@link #startTimer(double)}. This
* will stop the timer from firing.
*
* @param token The ID of the timer to cancel.
* @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
* optional time to format.
* <p>
* The format string takes the same formats as C's {@code strftime} function
* (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it
* can be prefixed with an exclamation mark ({@code !}) to use UTC time
* instead of the server's local timezone.
* The format string takes the same formats as C's [strftime](http://www.cplusplus.com/reference/ctime/strftime/)
* function. The format string can also be prefixed with an exclamation mark
* ({@code !}) to use UTC time instead of the server's local timezone.
* <p>
* 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,

View File

@ -13,6 +13,11 @@
-- @module vector
-- @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.
--
-- This is suitable for representing both position and directional vectors.
@ -27,6 +32,9 @@ local vector = {
-- @usage v1:add(v2)
-- @usage v1 + v2
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(
self.x + o.x,
self.y + o.y,
@ -42,6 +50,9 @@ local vector = {
-- @usage v1:sub(v2)
-- @usage v1 - v2
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(
self.x - o.x,
self.y - o.y,
@ -52,30 +63,36 @@ local vector = {
--- Multiplies a vector by a scalar value.
--
-- @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)`.
-- @usage v:mul(3)
-- @usage v * 3
mul = function(self, m)
-- @usage vector.new(1, 2, 3):mul(3)
-- @usage vector.new(1, 2, 3) * 3
mul = function(self, factor)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, factor, "number")
return vector.new(
self.x * m,
self.y * m,
self.z * m
self.x * factor,
self.y * factor,
self.z * factor
)
end,
--- Divides a vector by a scalar value.
--
-- @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)`.
-- @usage v:div(3)
-- @usage v / 3
div = function(self, m)
-- @usage vector.new(1, 2, 3):div(3)
-- @usage vector.new(1, 2, 3) / 3
div = function(self, factor)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, factor, "number")
return vector.new(
self.x / m,
self.y / m,
self.z / m
self.x / factor,
self.y / factor,
self.z / factor
)
end,
@ -83,8 +100,9 @@ local vector = {
--
-- @tparam Vector self The vector to negate.
-- @treturn Vector The negated vector.
-- @usage -v
-- @usage -vector.new(1, 2, 3)
unm = function(self)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
return vector.new(
-self.x,
-self.y,
@ -99,6 +117,9 @@ local vector = {
-- @treturn Vector The dot product of `self` and `o`.
-- @usage v1:dot(v2)
dot = function(self, o)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
return self.x * o.x + self.y * o.y + self.z * o.z
end,
@ -109,6 +130,9 @@ local vector = {
-- @treturn Vector The cross product of `self` and `o`.
-- @usage v1:cross(v2)
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(
self.y * o.z - self.z * o.y,
self.z * o.x - self.x * o.z,
@ -120,6 +144,7 @@ local vector = {
-- @tparam Vector self This vector.
-- @treturn number The length of this vector.
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)
end,
@ -141,6 +166,9 @@ local vector = {
-- nearest 0.5.
-- @treturn Vector The rounded vector.
round = function(self, tolerance)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
expect(2, tolerance, "number", "nil")
tolerance = tolerance or 1.0
return vector.new(
math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance,
@ -156,6 +184,8 @@ local vector = {
-- @usage v:tostring()
-- @usage tostring(v)
tostring = function(self)
if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
return self.x .. "," .. self.y .. "," .. self.z
end,
@ -165,11 +195,15 @@ local vector = {
-- @tparam Vector other The second vector to compare to.
-- @treturn boolean Whether or not the vectors are equal.
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
end,
}
local vmetatable = {
vmetatable = {
__name = "vector",
__index = vector,
__add = vector.add,
__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
* 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.
* Remove custom breaking progress of modems on Forge.
* Add `speaker sound` command (fatboychummy).
Several bug fixes:
* Fix client and server DFPWM transcoders getting out of sync.
* Fix `turtle.suck` reporting incorrect error when failing to suck items.
* Fix pocket computers displaying state (blinking, modem light) for the wrong computer.
* Fix crash when wrapping an invalid BE as a generic peripheral.
* Chest peripherals now reattach when a chest is converted into a double chest.
* Fix `speaker` program not resolving files relative to the current directory.
* Skip main-thread tasks if the peripheral is detached.
* Fix internal Lua VM errors if yielding inside `__tostring`.
* 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.
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 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
-- current terminal.
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
elseif cmd == "play" then
local _, file, name = ...
if not file then
error("Usage: speaker play <file or url> [speaker]", 0)
end
local speaker = get_speakers(name)[1]
local handle, err
@ -128,9 +132,47 @@ elseif cmd == "play" then
end
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
local programName = arg[0] or fs.getName(shell.getRunningProgram())
print("Usage:")
print(programName .. " play <file or url> [speaker]")
print(programName .. " sound <sound> [volume] [pitch] [speaker]")
print(programName .. " stop [speaker]")
end

View File

@ -117,7 +117,7 @@ shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build(
completion.peripheral
))
shell.setCompletionFunction("rom/programs/fun/speaker.lua", completion.build(
{ completion.choice, { "play ", "stop " } },
{ completion.choice, { "play ", "sound ", "stop " } },
function(shell, text, previous)
if previous[2] == "play" then return completion.file(shell, text, previous, true)
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.HitResult;
import java.util.Objects;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
public class ComputerCraftClient {
@ -79,7 +81,7 @@ public static void init() {
if (hit.getType() != HitResult.Type.BLOCK) return ItemStack.EMPTY;
var pos = ((BlockHitResult) hit).getBlockPos();
var level = Minecraft.getInstance().level;
var level = Objects.requireNonNull(Minecraft.getInstance().level);
var state = level.getBlockState(pos);
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.shared.details.FluidData;
import dan200.computercraft.shared.peripheral.generic.ComponentLookup;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@ -60,7 +61,7 @@ private record CapabilityLookup<T>(BlockCapability<T, Direction> capability) imp
@Nullable
@Override
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.LuaFunction;
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.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
@ -53,7 +55,7 @@ public int pushFluid(
var location = computer.getAvailablePeripheral(toName);
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");
int actualLimit = limit.orElse(Integer.MAX_VALUE);
@ -78,7 +80,7 @@ public int pullFluid(
var location = computer.getAvailablePeripheral(fromName);
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");
int actualLimit = limit.orElse(Integer.MAX_VALUE);
@ -90,14 +92,17 @@ public int pullFluid(
}
@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 (blockEntity.isRemoved()) return null;
var level = blockEntity.getLevel();
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;
}

View File

@ -8,7 +8,9 @@
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.platform.ForgeContainerTransfer;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.level.block.entity.BlockEntity;
@ -73,7 +75,7 @@ public int pushItems(
var location = computer.getAvailablePeripheral(toName);
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");
// Validate slots
@ -95,7 +97,7 @@ public int pullItems(
var location = computer.getAvailablePeripheral(fromName);
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");
// Validate slots
@ -108,14 +110,17 @@ public int pullItems(
}
@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 (blockEntity.isRemoved()) return null;
var level = blockEntity.getLevel();
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;
}

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.ImmutableSetMultimap
import com.uber.nullaway.LibraryModels
import com.uber.nullaway.LibraryModels.FieldRef.fieldRef
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.
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;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.core.ComputerContext;
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.AddressRule;
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.TextBuffer;
import dan200.computercraft.core.util.Colour;
@ -31,6 +36,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
@ -55,37 +61,58 @@ public class Main {
private static final Logger LOG = LoggerFactory.getLogger(Main.class);
private static final boolean DEBUG = Checks.DEBUG;
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+)$");
}
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 static Path parsePath(String path) throws ParseException {
try {
return Path.of(path);
} catch (InvalidPathException e) {
throw new ParseException("'" + path + "' is not a valid path (" + e.getReason() + ")");
}
}
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")
private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, Class<T> klass, @Nullable T defaultValue) throws ParseException {
return cli.hasOption(opt) ? getParsedOptionValue(cli, opt, klass) : defaultValue;
private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, ValueParser<T> parser, @Nullable T defaultValue) throws ParseException {
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 {
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()
.desc("The path to the resources directory")
.build());
@ -98,6 +125,12 @@ public static void main(String[] args) throws InterruptedException {
options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains")
.desc("Allow accessing local domains with the HTTP API.")
.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")
.desc("Print help message")
@ -107,6 +140,7 @@ public static void main(String[] args) throws InterruptedException {
Path computerDirectory;
TermSize termSize;
boolean allowLocalDomains;
List<MountPaths> mounts, readOnlyMounts;
try {
var cli = new DefaultParser().parse(options, args);
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");
resourcesDirectory = getParsedOptionValue(cli, resourceOpt, Path.class);
computerDirectory = getParsedOptionValue(cli, computerOpt, Path.class, null);
termSize = getParsedOptionValue(cli, termSizeOpt, TermSize.class, TermSize.DEFAULT);
resourcesDirectory = parsePath(cli.getOptionValue(resourceOpt));
computerDirectory = getParsedOptionValue(cli, computerOpt, Main::parsePath, null);
termSize = getParsedOptionValue(cli, termSizeOpt, TermSize::parse, TermSize.DEFAULT);
allowLocalDomains = cli.hasOption(allowLocalDomainsOpt);
mounts = getParsedOptionValues(cli, mountOpt, MountPaths::parse);
readOnlyMounts = getParsedOptionValues(cli, mountRoOpt, MountPaths::parse);
} catch (ParseException e) {
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)),
0
);
computer.addApi(new FileMounter(computer.getAPIEnvironment(), readOnlyMounts, mounts));
computer.turnOn();
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 MARGIN = 2;
private static final int PIXEL_WIDTH = 6;