Merge branch 'mc-1.20.x' into mc-1.20.x-speaker-sound

This commit is contained in:
Jonathan Coates 2024-03-29 09:44:50 +00:00
commit f9c9b6b119
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
101 changed files with 2432 additions and 781 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

@ -40,10 +40,6 @@ repositories {
val mainMaven = maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
// Until https://github.com/SpongePowered/Mixin/pull/593 is merged
includeModule("org.spongepowered", "mixin")
}
}
exclusiveContent {
@ -113,6 +109,8 @@ sourceSets.all {
option("NullAway:CastToNonNullMethod", "dan200.computercraft.core.util.Nullability.assertNonNull")
option("NullAway:CheckOptionalEmptiness")
option("NullAway:AcknowledgeRestrictiveAnnotations")
excludedPaths = ".*/jmh_generated/.*"
}
}
}

View File

@ -13,8 +13,12 @@ SPDX-License-Identifier: MPL-2.0
<property name="tabWidth" value="4"/>
<property name="charset" value="UTF-8" />
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressionFilter">
<property name="file" value="${config_loc}/suppressions.xml" />
<property name="file" value="${config_loc}/suppressions.xml" />
</module>
<module name="BeforeExecutionExclusionFileFilter">

View File

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

View File

@ -26,7 +26,7 @@ slf4j = "2.0.1"
asm = "9.6"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = "0.9.1"
cobalt = "0.9.2"
commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
@ -55,7 +55,7 @@ jmh = "1.37"
# Build tools
cctJavadoc = "1.8.2"
checkstyle = "10.12.6"
checkstyle = "10.14.1"
curseForgeGradle = "1.0.14"
errorProne-core = "2.23.0"
errorProne-plugin = "3.1.0"
@ -64,16 +64,15 @@ forgeGradle = "6.0.20"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-69-gf294ab2"
illuaminate = "0.1.0-71-g378d86e"
librarian = "1.+"
lwjgl = "3.3.3"
minotaur = "2.+"
mixinGradle = "0.7.38"
nullAway = "0.9.9"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.10.0-SQUID.3"
vanillaExtract = "0.1.1"
vanillaExtract = "0.1.2"
versionCatalogUpdate = "0.8.1"
[libraries]
@ -171,7 +170,6 @@ githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "g
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

20
gradlew.bat vendored
View File

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

@ -29,7 +29,7 @@ private Services() {
* @throws IllegalStateException When the service cannot be loaded.
*/
public static <T> T load(Class<T> klass) {
var services = ServiceLoader.load(klass).stream().toList();
var services = ServiceLoader.load(klass, klass.getClassLoader()).stream().toList();
return switch (services.size()) {
case 1 -> services.get(0).get();
case 0 -> throw new IllegalStateException("Cannot find service for " + klass.getName());

View File

@ -22,6 +22,7 @@
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.CommandComputerCraft;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
@ -77,8 +78,10 @@ public static void register() {
/**
* Register any client-side objects which must be done on the main thread.
*
* @param itemProperties Callback to register item properties.
*/
public static void registerMainThread() {
public static void registerMainThread(RegisterItemProperty itemProperties) {
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.COMPUTER.get(), ComputerScreen::new);
MenuScreens.<AbstractComputerMenu, ComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER.get(), ComputerScreen::new);
MenuScreens.<AbstractComputerMenu, NoTermComputerScreen<AbstractComputerMenu>>register(ModRegistry.Menus.POCKET_COMPUTER_NO_TERM.get(), NoTermComputerScreen::new);
@ -90,11 +93,14 @@ public static void registerMainThread() {
MenuScreens.<ViewComputerMenu, ComputerScreen<ViewComputerMenu>>register(ModRegistry.Menus.VIEW_COMPUTER.get(), ComputerScreen::new);
registerItemProperty("state",
new UnclampedPropertyFunction((stack, world, player, random) -> ClientPocketComputers.get(stack).getState().ordinal()),
registerItemProperty(itemProperties, "state",
new UnclampedPropertyFunction((stack, world, player, random) -> {
var computer = ClientPocketComputers.get(stack);
return (computer == null ? ComputerState.OFF : computer.getState()).ordinal();
}),
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
);
registerItemProperty("coloured",
registerItemProperty(itemProperties, "coloured",
(stack, world, player, random) -> IColouredItem.getColourBasic(stack) != -1 ? 1 : 0,
ModRegistry.Items.POCKET_COMPUTER_NORMAL, ModRegistry.Items.POCKET_COMPUTER_ADVANCED
);
@ -115,9 +121,17 @@ public static void registerTurtleModellers(RegisterTurtleUpgradeModeller registe
}
@SafeVarargs
private static void registerItemProperty(String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
private static void registerItemProperty(RegisterItemProperty itemProperties, String name, ClampedItemPropertyFunction getter, Supplier<? extends Item>... items) {
var id = new ResourceLocation(ComputerCraftAPI.MOD_ID, name);
for (var item : items) ItemProperties.register(item.get(), id, getter);
for (var item : items) itemProperties.register(item.get(), id, getter);
}
/**
* Register an item property via {@link ItemProperties#register}. Forge and Fabric expose different methods, so we
* supply this via mod-loader-specific code.
*/
public interface RegisterItemProperty {
void register(Item item, ResourceLocation name, ClampedItemPropertyFunction property);
}
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
@ -155,17 +169,14 @@ public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register)
}
private static int getPocketColour(ItemStack stack, int layer) {
switch (layer) {
case 0:
default:
return 0xFFFFFF;
case 1: // Frame colour
return IColouredItem.getColourBasic(stack);
case 2: { // Light colour
var light = ClientPocketComputers.get(stack).getLightState();
return light == -1 ? Colour.BLACK.getHex() : light;
return switch (layer) {
default -> 0xFFFFFF;
case 1 -> IColouredItem.getColourBasic(stack); // Frame colour
case 2 -> { // Light colour
var computer = ClientPocketComputers.get(stack);
yield computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
}
}
};
}
private static int getTurtleColour(ItemStack stack, int layer) {

View File

@ -17,6 +17,7 @@
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
@ -27,7 +28,6 @@
import net.minecraft.world.level.Level;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
@ -67,19 +67,17 @@ public void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable
}
@Override
public void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal) {
var computer = ClientPocketComputers.get(instanceId, terminal.colour);
computer.setState(state, lightState);
if (terminal.hasTerminal()) computer.setTerminal(terminal);
public void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal) {
ClientPocketComputers.setState(instanceId, state, lightState, terminal);
}
@Override
public void handlePocketComputerDeleted(int instanceId) {
public void handlePocketComputerDeleted(UUID instanceId) {
ClientPocketComputers.remove(instanceId);
}
@Override
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) {
public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio buffer) {
SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer);
}

View File

@ -4,21 +4,25 @@
package dan200.computercraft.client.pocket;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.client.PocketComputerDataMessage;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Maps {@link ServerComputer#getInstanceID()} to locals {@link PocketComputerData}.
* Maps {@link ServerComputer#getInstanceUUID()} to locals {@link PocketComputerData}.
* <p>
* This is populated by {@link PocketComputerDataMessage} and accessed when rendering pocket computers
*/
public final class ClientPocketComputers {
private static final Int2ObjectMap<PocketComputerData> instances = new Int2ObjectOpenHashMap<>();
private static final Map<UUID, PocketComputerData> instances = new HashMap<>();
private ClientPocketComputers() {
}
@ -27,25 +31,29 @@ public static void reset() {
instances.clear();
}
public static void remove(int id) {
public static void remove(UUID id) {
instances.remove(id);
}
/**
* Get or create a pocket computer.
* Set the state of a pocket computer.
*
* @param instanceId The instance ID of the pocket computer.
* @param advanced Whether this computer has an advanced terminal.
* @return The pocket computer data.
* @param instanceId The instance ID of the pocket computer.
* @param state The computer state of the pocket computer.
* @param lightColour The current colour of the modem light.
* @param terminalData The current terminal contents.
*/
public static PocketComputerData get(int instanceId, boolean advanced) {
public static void setState(UUID instanceId, ComputerState state, int lightColour, TerminalState terminalData) {
var computer = instances.get(instanceId);
if (computer == null) instances.put(instanceId, computer = new PocketComputerData(advanced));
return computer;
if (computer == null) {
instances.put(instanceId, new PocketComputerData(state, lightColour, terminalData));
} else {
computer.setState(state, lightColour, terminalData);
}
}
public static PocketComputerData get(ItemStack stack) {
var family = stack.getItem() instanceof PocketComputerItem computer ? computer.getFamily() : ComputerFamily.NORMAL;
return get(PocketComputerItem.getInstanceID(stack), family != ComputerFamily.NORMAL);
public static @Nullable PocketComputerData get(ItemStack stack) {
var id = PocketComputerItem.getInstanceID(stack);
return id == null ? null : instances.get(id);
}
}

View File

@ -4,13 +4,13 @@
package dan200.computercraft.client.pocket;
import dan200.computercraft.core.terminal.Terminal;
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.config.Config;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import javax.annotation.Nullable;
/**
* Clientside data about a pocket computer.
* <p>
@ -21,20 +21,22 @@
* @see ClientPocketComputers The registry which holds pocket computers.
* @see PocketServerComputer The server-side pocket computer.
*/
public class PocketComputerData {
private final NetworkedTerminal terminal;
private ComputerState state = ComputerState.OFF;
private int lightColour = -1;
public final class PocketComputerData {
private @Nullable NetworkedTerminal terminal;
private ComputerState state;
private int lightColour;
public PocketComputerData(boolean colour) {
terminal = new NetworkedTerminal(Config.pocketTermWidth, Config.pocketTermHeight, colour);
PocketComputerData(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state;
this.lightColour = lightColour;
if (terminalData.hasTerminal()) terminal = terminalData.create();
}
public int getLightState() {
return state != ComputerState.OFF ? lightColour : -1;
}
public Terminal getTerminal() {
public @Nullable NetworkedTerminal getTerminal() {
return terminal;
}
@ -42,12 +44,16 @@ public ComputerState getState() {
return state;
}
public void setState(ComputerState state, int lightColour) {
void setState(ComputerState state, int lightColour, TerminalState terminalData) {
this.state = state;
this.lightColour = lightColour;
}
public void setTerminal(TerminalState state) {
state.apply(terminal);
if (terminalData.hasTerminal()) {
if (terminal == null) {
terminal = terminalData.create();
} else {
terminalData.apply(terminal);
}
}
}
}

View File

@ -11,6 +11,7 @@
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.world.item.ItemStack;
@ -32,10 +33,16 @@ private PocketItemRenderer() {
@Override
protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, ItemStack stack, int light) {
var computer = ClientPocketComputers.get(stack);
var terminal = computer.getTerminal();
var terminal = computer == null ? null : computer.getTerminal();
var termWidth = terminal.getWidth();
var termHeight = terminal.getHeight();
int termWidth, termHeight;
if (terminal == null) {
termWidth = Config.pocketTermWidth;
termHeight = Config.pocketTermHeight;
} else {
termWidth = terminal.getWidth();
termHeight = terminal.getHeight();
}
var width = termWidth * FONT_WIDTH + MARGIN * 2;
var height = termHeight * FONT_HEIGHT + MARGIN * 2;
@ -60,14 +67,15 @@ protected void renderItem(PoseStack transform, MultiBufferSource bufferSource, I
renderFrame(matrix, bufferSource, family, frameColour, light, width, height);
// Render the light
var lightColour = ClientPocketComputers.get(stack).getLightState();
if (lightColour == -1) lightColour = Colour.BLACK.getHex();
var lightColour = computer == null || computer.getLightState() == -1 ? Colour.BLACK.getHex() : computer.getLightState();
renderLight(transform, bufferSource, lightColour, width, height);
FixedWidthFontRenderer.drawTerminal(
FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL)),
MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN
);
var quadEmitter = FixedWidthFontRenderer.toVertexConsumer(transform, bufferSource.getBuffer(RenderTypes.TERMINAL));
if (terminal == null) {
FixedWidthFontRenderer.drawEmptyTerminal(quadEmitter, 0, 0, width, height);
} else {
FixedWidthFontRenderer.drawTerminal(quadEmitter, MARGIN, MARGIN, terminal, MARGIN, MARGIN, MARGIN, MARGIN);
}
transform.popPose();
}

View File

@ -5,6 +5,7 @@
package dan200.computercraft.client.sound;
import com.mojang.blaze3d.audio.Channel;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.sounds.AudioStream;
@ -36,7 +37,7 @@ class DfpwmStream implements AudioStream {
/**
* The {@link Channel} which this sound is playing on.
*
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
*/
@Nullable
Channel channel;
@ -44,21 +45,23 @@ class DfpwmStream implements AudioStream {
/**
* The underlying {@link SoundEngine} executor.
*
* @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer)
* @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio)
* @see SoundEngine#executor
*/
@Nullable
Executor executor;
private int charge = 0; // q
private int strength = 0; // s
private int lowPassCharge;
private boolean previousBit = false;
DfpwmStream() {
}
void push(ByteBuffer input) {
void push(EncodedAudio audio) {
var charge = audio.charge();
var strength = audio.strength();
var previousBit = audio.previousBit();
var input = audio.audio();
var readable = input.remaining();
var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder());

View File

@ -6,12 +6,12 @@
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
/**
* An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound.
@ -25,7 +25,7 @@ public class SpeakerInstance {
SpeakerInstance() {
}
private void pushAudio(ByteBuffer buffer) {
private void pushAudio(EncodedAudio buffer) {
var sound = this.sound;
var stream = currentStream;
@ -43,7 +43,7 @@ private void pushAudio(ByteBuffer buffer) {
}
}
public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) {
public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) {
pushAudio(buffer);
var soundManager = Minecraft.getInstance().getSoundManager();

View File

@ -6,6 +6,7 @@
import dan200.computercraft.api.network.wired.WiredNetworkChange;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.PeripheralHelpers;
import java.util.Collections;
import java.util.HashMap;
@ -52,7 +53,7 @@ static WiredNetworkChangeImpl changeOf(Map<String, IPeripheral> oldPeripherals,
var oldValue = entry.getValue();
if (newPeripherals.containsKey(oldKey)) {
var rightValue = added.get(oldKey);
if (oldValue.equals(rightValue)) {
if (PeripheralHelpers.equals(oldValue, rightValue)) {
added.remove(oldKey);
} else {
removed.put(oldKey, oldValue);

View File

@ -134,7 +134,7 @@ private static int dump(CommandSourceStack source) {
} else if (b.getLevel() == world) {
return 1;
} else {
return Integer.compare(a.getInstanceID(), b.getInstanceID());
return a.getInstanceUUID().compareTo(b.getInstanceUUID());
}
});
@ -159,7 +159,8 @@ private static int dump(CommandSourceStack source) {
*/
private static int dumpComputer(CommandSourceStack source, ServerComputer computer) {
var table = new TableBuilder("Dump");
table.row(header("Instance"), text(Integer.toString(computer.getInstanceID())));
table.row(header("Instance ID"), text(Integer.toString(computer.getInstanceID())));
table.row(header("Instance UUID"), text(computer.getInstanceUUID().toString()));
table.row(header("Id"), text(Integer.toString(computer.getID())));
table.row(header("Label"), text(computer.getLabel()));
table.row(header("On"), bool(computer.isOn()));
@ -338,11 +339,7 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve
if (computer == null) {
out.append("#" + computerId + " ").append(coloured("(unloaded)", ChatFormatting.GRAY));
} else {
out.append(link(
text("#" + computerId + " ").append(coloured("(instance " + computer.getInstanceID() + ")", ChatFormatting.GRAY)),
makeComputerCommand("dump", computer),
Component.translatable("commands.computercraft.dump.action")
));
out.append(makeComputerDumpCommand(computer));
}
// And, if we're a player, some useful links
@ -372,10 +369,6 @@ private static Component linkComputer(CommandSourceStack source, @Nullable Serve
return out;
}
private static String makeComputerCommand(String command, ServerComputer computer) {
return String.format("/computercraft %s @c[instance=%d]", command, computer.getInstanceID());
}
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
return link(
@ -392,7 +385,7 @@ private static Component linkPosition(CommandSourceStack context, ServerComputer
var file = new File(ServerContext.get(source.getServer()).storageDir().toFile(), "computer/" + id);
if (!file.isDirectory()) return null;
return link(
return clientLink(
text("\u270E"),
"/" + CLIENT_OPEN_FOLDER + " " + id,
Component.translatable("commands.computercraft.dump.open_path")

View File

@ -16,7 +16,10 @@
import net.minecraft.advancements.critereon.MinMaxBounds;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.commands.arguments.UuidArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
@ -30,17 +33,21 @@
import static dan200.computercraft.shared.command.CommandUtils.suggestOnServer;
import static dan200.computercraft.shared.command.Exceptions.*;
import static dan200.computercraft.shared.command.arguments.ArgumentParserUtils.consume;
import static dan200.computercraft.shared.command.text.ChatHelpers.makeComputerDumpCommand;
public record ComputerSelector(
String selector,
OptionalInt instanceId,
@Nullable UUID instanceUuid,
OptionalInt computerId,
@Nullable String label,
@Nullable ComputerFamily family,
@Nullable AABB bounds,
@Nullable MinMaxBounds.Doubles range
) {
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null);
private static final ComputerSelector all = new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null);
private static UuidArgument uuidArgument = UuidArgument.uuid();
/**
* A {@link ComputerSelector} which matches all computers.
@ -59,8 +66,13 @@ public static ComputerSelector all() {
*/
public Stream<ServerComputer> find(CommandSourceStack source) {
var context = ServerContext.get(source.getServer());
if (instanceId.isPresent()) {
var computer = context.registry().get(instanceId.getAsInt());
if (instanceId().isPresent()) {
var computer = context.registry().get(instanceId().getAsInt());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
}
if (instanceUuid() != null) {
var computer = context.registry().get(instanceUuid());
return computer != null && matches(source, computer) ? Stream.of(computer) : Stream.of();
}
@ -79,7 +91,7 @@ public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxExc
if (computers.isEmpty()) throw COMPUTER_ARG_NONE.create(selector);
if (computers.size() == 1) return computers.iterator().next();
var builder = new StringBuilder();
var builder = MutableComponent.create(ComponentContents.EMPTY);
var first = true;
for (var computer : computers) {
if (first) {
@ -88,12 +100,12 @@ public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxExc
builder.append(", ");
}
builder.append(computer.getInstanceID());
builder.append(makeComputerDumpCommand(computer));
}
// We have an incorrect number of computers: throw an error
throw COMPUTER_ARG_MANY.create(selector, builder.toString());
throw COMPUTER_ARG_MANY.create(selector, builder);
}
/**
@ -105,6 +117,7 @@ public ServerComputer findOne(CommandSourceStack source) throws CommandSyntaxExc
*/
public boolean matches(CommandSourceStack source, ServerComputer computer) {
return (instanceId().isEmpty() || computer.getInstanceID() == instanceId().getAsInt())
&& (instanceUuid() == null || computer.getInstanceUUID().equals(instanceUuid()))
&& (computerId().isEmpty() || computer.getID() == computerId().getAsInt())
&& (label == null || Objects.equals(computer.getLabel(), label))
&& (family == null || computer.getFamily() == family)
@ -127,6 +140,7 @@ public static ComputerSelector parse(StringReader reader) throws CommandSyntaxEx
if (consume(reader, "@c[")) {
parseSelector(builder, reader);
} else {
// TODO(1.20.5): Only parse computer ids here.
var kind = reader.peek();
if (kind == '@') {
reader.skip();
@ -143,7 +157,7 @@ public static ComputerSelector parse(StringReader reader) throws CommandSyntaxEx
}
var selector = reader.getString().substring(start, reader.getCursor());
return new ComputerSelector(selector, builder.instanceId, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
return new ComputerSelector(selector, builder.instanceId, builder.instanceUuid, builder.computerId, builder.label, builder.family, builder.bounds, builder.range);
}
private static void parseSelector(Builder builder, StringReader reader) throws CommandSyntaxException {
@ -260,6 +274,7 @@ private static SuggestionsBuilder suggestions(StringReader reader) {
private static final class Builder {
private OptionalInt instanceId = OptionalInt.empty();
private @Nullable UUID instanceUuid = null;
private OptionalInt computerId = OptionalInt.empty();
private @Nullable String label;
private @Nullable ComputerFamily family;
@ -282,8 +297,8 @@ public static Map<String, Option> options() {
var optionList = new Option[]{
new Option(
"instance",
(reader, builder) -> builder.instanceId = OptionalInt.of(reader.readInt()),
suggestComputers(c -> Integer.toString(c.getInstanceID()))
(reader, builder) -> builder.instanceUuid = uuidArgument.parse(reader),
suggestComputers(c -> c.getInstanceUUID().toString())
),
new Option(
"id",

View File

@ -4,6 +4,8 @@
package dan200.computercraft.shared.command.text;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.ClickEvent;
@ -53,6 +55,13 @@ public static Component link(MutableComponent component, String command, Compone
return link(component, new ClickEvent(ClickEvent.Action.RUN_COMMAND, command), toolTip);
}
public static Component clientLink(MutableComponent component, String command, Component toolTip) {
var event = PlatformHelper.get().canClickRunClientCommand()
? new ClickEvent(ClickEvent.Action.RUN_COMMAND, command)
: new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command);
return link(component, event, toolTip);
}
public static Component link(Component component, ClickEvent click, Component toolTip) {
var style = component.getStyle();
@ -73,4 +82,16 @@ public static MutableComponent copy(String text) {
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("gui.computercraft.tooltip.copy")))
);
}
public static String makeComputerCommand(String command, ServerComputer computer) {
return String.format("/computercraft %s @c[instance=%s]", command, computer.getInstanceUUID());
}
public static Component makeComputerDumpCommand(ServerComputer computer) {
return link(
text("#" + computer.getID()),
makeComputerCommand("dump", computer),
Component.translatable("commands.computercraft.dump.action")
);
}
}

View File

@ -23,6 +23,7 @@
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
@ -174,6 +175,15 @@ public final void onNeighborChange(BlockState state, LevelReader world, BlockPos
if (be instanceof AbstractComputerBlockEntity computer) computer.neighborChanged(neighbour);
}
@Override
@Deprecated
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
var be = level.getBlockEntity(pos);
if (be instanceof AbstractComputerBlockEntity computer) computer.neighbourShapeChanged(direction);
return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
}
@Nullable
@Override
@Deprecated

View File

@ -36,13 +36,14 @@
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractComputerBlockEntity extends BlockEntity implements IComputerBlockEntity, Nameable, MenuProvider {
private static final String NBT_ID = "ComputerId";
private static final String NBT_LABEL = "Label";
private static final String NBT_ON = "On";
private int instanceID = -1;
private @Nullable UUID instanceID = null;
private int computerID = -1;
protected @Nullable String label = null;
private boolean on = false;
@ -66,7 +67,7 @@ protected void unload() {
var computer = getServerComputer();
if (computer != null) computer.close();
instanceID = -1;
instanceID = null;
}
@Override
@ -122,7 +123,7 @@ protected void serverTick() {
// Update any peripherals that have changed.
if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) {
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(computer, direction);
if (DirectionUtil.isSet(invalidSides, direction)) refreshPeripheral(computer, direction);
}
}
@ -294,7 +295,18 @@ public void neighborChanged(BlockPos neighbour) {
// If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
// handle this incorrectly.
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, getBlockPos().relative(dir));
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
invalidSides = DirectionUtil.ALL_SIDES; // Mark all peripherals as dirty.
}
/**
* Called when a neighbour block's shape changes.
* <p>
* Unlike {@link #neighborChanged(BlockPos)}, we don't update redstone, only peripherals.
*
* @param direction The side that changed.
*/
public void neighbourShapeChanged(Direction direction) {
invalidSides |= 1 << direction.ordinal();
}
/**
@ -401,7 +413,7 @@ protected void loadClient(CompoundTag nbt) {
}
protected void transferStateFrom(AbstractComputerBlockEntity copy) {
if (copy.computerID != computerID || copy.instanceID != instanceID) {
if (copy.computerID != computerID || !Objects.equals(copy.instanceID, instanceID)) {
unload();
instanceID = copy.instanceID;
computerID = copy.computerID;
@ -411,7 +423,7 @@ protected void transferStateFrom(AbstractComputerBlockEntity copy) {
lockCode = copy.lockCode;
BlockEntityHelpers.updateBlock(this);
}
copy.instanceID = -1;
copy.instanceID = null;
}
@Override

View File

@ -26,11 +26,13 @@
import net.minecraft.world.inventory.AbstractContainerMenu;
import javax.annotation.Nullable;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
public class ServerComputer implements InputHandler, ComputerEnvironment {
private final int instanceID;
private final UUID instanceUUID = UUID.randomUUID();
private ServerLevel level;
private BlockPos position;
@ -119,9 +121,9 @@ public int pollAndResetChanges() {
return computer.pollAndResetChanges();
}
public int register() {
ServerContext.get(level.getServer()).registry().add(instanceID, this);
return instanceID;
public UUID register() {
ServerContext.get(level.getServer()).registry().add(this);
return instanceUUID;
}
void unload() {
@ -130,7 +132,7 @@ void unload() {
public void close() {
unload();
ServerContext.get(level.getServer()).registry().remove(instanceID);
ServerContext.get(level.getServer()).registry().remove(this);
}
private void sendToAllInteracting(Function<AbstractContainerMenu, NetworkMessage<ClientNetworkContext>> createPacket) {
@ -150,6 +152,10 @@ public int getInstanceID() {
return instanceID;
}
public UUID getInstanceUUID() {
return instanceUUID;
}
public int getID() {
return computer.getID();
}

View File

@ -8,14 +8,14 @@
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Random;
import java.util.*;
public class ServerComputerRegistry {
private static final Random RANDOM = new Random();
private final int sessionId = RANDOM.nextInt();
private final Int2ObjectMap<ServerComputer> computers = new Int2ObjectOpenHashMap<>();
private final Int2ObjectMap<ServerComputer> computersByInstanceId = new Int2ObjectOpenHashMap<>();
private final Map<UUID, ServerComputer> computersByInstanceUuid = new HashMap<>();
private int nextInstanceId;
public int getSessionID() {
@ -28,50 +28,64 @@ int getUnusedInstanceID() {
@Nullable
public ServerComputer get(int instanceID) {
return instanceID >= 0 ? computers.get(instanceID) : null;
return instanceID >= 0 ? computersByInstanceId.get(instanceID) : null;
}
@Nullable
public ServerComputer get(int sessionId, int instanceId) {
public ServerComputer get(@Nullable UUID instanceID) {
return instanceID != null ? computersByInstanceUuid.get(instanceID) : null;
}
@Nullable
public ServerComputer get(int sessionId, @Nullable UUID instanceId) {
return sessionId == this.sessionId ? get(instanceId) : null;
}
void update() {
var it = getComputers().iterator();
var it = computersByInstanceId.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();
}
}
}
void add(int instanceID, ServerComputer computer) {
remove(instanceID);
computers.put(instanceID, computer);
nextInstanceId = Math.max(nextInstanceId, instanceID + 1);
}
void add(ServerComputer computer) {
var instanceID = computer.getInstanceID();
var instanceUUID = computer.getInstanceUUID();
void remove(int instanceID) {
var computer = get(instanceID);
if (computer != null) {
computer.unload();
computer.onRemoved();
if (computersByInstanceId.containsKey(instanceID)) {
throw new IllegalStateException("Duplicate computer " + instanceID);
}
computers.remove(instanceID);
if (computersByInstanceUuid.containsKey(instanceUUID)) {
throw new IllegalStateException("Duplicate computer " + instanceUUID);
}
computersByInstanceId.put(instanceID, computer);
computersByInstanceUuid.put(instanceUUID, computer);
}
void remove(ServerComputer computer) {
computer.unload();
computer.onRemoved();
computersByInstanceId.remove(computer.getInstanceID());
computersByInstanceUuid.remove(computer.getInstanceUUID());
}
void close() {
for (var computer : getComputers()) computer.unload();
computers.clear();
computersByInstanceId.clear();
computersByInstanceUuid.clear();
}
public Collection<ServerComputer> getComputers() {
return computers.values();
return computersByInstanceId.values();
}
}

View File

@ -25,7 +25,7 @@ public ViewComputerMenu(int id, Inventory player, ComputerContainerData data) {
private static boolean canInteractWith(ServerComputer computer, Player player) {
// If this computer no longer exists then discard it.
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceID()) != computer) {
if (ServerContext.get(computer.getLevel().getServer()).registry().get(computer.getInstanceUUID()) != computer) {
return false;
}

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

@ -8,6 +8,7 @@
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
@ -15,7 +16,6 @@
import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
@ -30,11 +30,11 @@ public interface ClientNetworkContext {
void handlePlayRecord(BlockPos pos, @Nullable SoundEvent sound, @Nullable String name);
void handlePocketComputerData(int instanceId, ComputerState state, int lightState, TerminalState terminal);
void handlePocketComputerData(UUID instanceId, ComputerState state, int lightState, TerminalState terminal);
void handlePocketComputerDeleted(int instanceId);
void handlePocketComputerDeleted(UUID instanceId);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio);
void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio);
void handleSpeakerMove(UUID source, SpeakerPosition.Message position);

View File

@ -13,24 +13,26 @@
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
/**
* Provides additional data about a client computer, such as its ID and current state.
*/
public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkContext> {
private final int instanceId;
private final UUID clientId;
private final ComputerState state;
private final int lightState;
private final TerminalState terminal;
public PocketComputerDataMessage(PocketServerComputer computer, boolean sendTerminal) {
instanceId = computer.getInstanceID();
clientId = computer.getInstanceUUID();
state = computer.getState();
lightState = computer.getLight();
terminal = sendTerminal ? computer.getTerminalState() : new TerminalState((NetworkedTerminal) null);
}
public PocketComputerDataMessage(FriendlyByteBuf buf) {
instanceId = buf.readVarInt();
clientId = buf.readUUID();
state = buf.readEnum(ComputerState.class);
lightState = buf.readVarInt();
terminal = new TerminalState(buf);
@ -38,7 +40,7 @@ public PocketComputerDataMessage(FriendlyByteBuf buf) {
@Override
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
buf.writeUUID(clientId);
buf.writeEnum(state);
buf.writeVarInt(lightState);
terminal.write(buf);
@ -46,7 +48,7 @@ public void write(FriendlyByteBuf buf) {
@Override
public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal);
context.handlePocketComputerData(clientId, state, lightState, terminal);
}
@Override

View File

@ -9,21 +9,23 @@
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import java.util.UUID;
public class PocketComputerDeletedClientMessage implements NetworkMessage<ClientNetworkContext> {
private final int instanceId;
private final UUID instanceId;
public PocketComputerDeletedClientMessage(int instanceId) {
public PocketComputerDeletedClientMessage(UUID instanceId) {
this.instanceId = instanceId;
}
public PocketComputerDeletedClientMessage(FriendlyByteBuf buffer) {
instanceId = buffer.readVarInt();
instanceId = buffer.readUUID();
}
@Override
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
buf.writeUUID(instanceId);
}
@Override

View File

@ -7,11 +7,11 @@
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
@ -24,10 +24,10 @@
public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkContext> {
private final UUID source;
private final SpeakerPosition.Message pos;
private final ByteBuffer content;
private final EncodedAudio content;
private final float volume;
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) {
public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, EncodedAudio content) {
this.source = source;
this.pos = pos.asMessage();
this.content = content;
@ -38,10 +38,7 @@ public SpeakerAudioClientMessage(FriendlyByteBuf buf) {
source = buf.readUUID();
pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat();
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
content = ByteBuffer.wrap(bytes);
content = EncodedAudio.read(buf);
}
@Override
@ -49,7 +46,7 @@ public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
buf.writeBytes(content.duplicate());
content.write(buf);
}
@Override

View File

@ -11,7 +11,7 @@
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.platform.RegistryWrappers;
import dan200.computercraft.core.computer.GuardedLuaContext;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntity;
@ -27,13 +27,16 @@ public final class GenericPeripheral implements IDynamicPeripheral {
private final Set<String> additionalTypes;
private final List<SaturatedMethod> methods;
GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set<String> additionalTypes, List<SaturatedMethod> methods) {
private @Nullable GuardedLuaContext contextWrapper;
private final GuardedLuaContext.Guard guard;
GenericPeripheral(BlockEntity tile, Direction side, String type, Set<String> additionalTypes, List<SaturatedMethod> methods) {
this.side = side;
var type = RegistryWrappers.BLOCK_ENTITY_TYPES.getKey(tile.getType());
this.tile = tile;
this.type = name != null ? name : type.toString();
this.type = type;
this.additionalTypes = additionalTypes;
this.methods = methods;
this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos());
}
public Direction side() {
@ -49,7 +52,12 @@ public String[] getMethodNames() {
@Override
public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException {
return methods.get(method).apply(context, computer, arguments);
var contextWrapper = this.contextWrapper;
if (contextWrapper == null || !contextWrapper.wraps(context)) {
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, guard);
}
return methods.get(method).apply(contextWrapper, computer, arguments);
}
@Override

View File

@ -10,6 +10,9 @@
import dan200.computercraft.core.methods.PeripheralMethod;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
@ -25,6 +28,8 @@
* See the platform-specific peripheral providers for the usage of this.
*/
final class GenericPeripheralBuilder {
private static final Logger LOG = LoggerFactory.getLogger(GenericPeripheralBuilder.class);
private @Nullable String name;
private final Set<String> additionalTypes = new HashSet<>(0);
private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
@ -33,8 +38,24 @@ final class GenericPeripheralBuilder {
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
if (methods.isEmpty()) return null;
String type;
if (name == null) {
var typeId = BlockEntityType.getKey(blockEntity.getType());
if (typeId == null) {
LOG.error(
"Block entity {} for {} was not registered. Skipping creating a generic peripheral for it.",
blockEntity, blockEntity.getBlockState().getBlock()
);
return null;
}
type = typeId.toString();
} else {
type = name;
}
methods.trimToSize();
return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
return new GenericPeripheral(blockEntity, side, type, additionalTypes, methods);
}
void addMethod(Object target, String name, PeripheralMethod method, @Nullable NamedMethod<PeripheralMethod> info) {

View File

@ -129,7 +129,6 @@ public boolean onCustomDestroyBlock(BlockState state, Level world, BlockPos pos,
world.setBlockAndUpdate(pos, correctConnections(world, pos, newState));
cable.modemChanged();
cable.connectionsChanged();
if (!world.isClientSide && !player.getAbilities().instabuild) {
Block.popResource(world, pos, item);
@ -162,10 +161,7 @@ public ItemStack getCloneItemStack(BlockState state, @Nullable HitResult hit, Bl
@Override
public void setPlacedBy(Level world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack stack) {
var tile = world.getBlockEntity(pos);
if (tile instanceof CableBlockEntity cable) {
if (cable.hasCable()) cable.connectionsChanged();
}
if (tile instanceof CableBlockEntity cable && cable.hasCable()) cable.connectionsChanged();
super.setPlacedBy(world, pos, state, placer, stack);
}
@ -177,14 +173,37 @@ public FluidState getFluidState(BlockState state) {
@Override
@Deprecated
public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor world, BlockPos pos, BlockPos otherPos) {
WaterloggableHelpers.updateShape(state, world, pos);
public BlockState updateShape(BlockState state, Direction side, BlockState otherState, LevelAccessor level, BlockPos pos, BlockPos otherPos) {
WaterloggableHelpers.updateShape(state, level, pos);
// Should never happen, but handle the case where we've no modem or cable.
if (!state.getValue(CABLE) && state.getValue(MODEM) == CableModemVariant.None) {
return getFluidState(state).createLegacyBlock();
}
return world instanceof Level level ? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, level, pos, side)) : state;
// Pop our modem if needed.
var dir = state.getValue(MODEM).getFacing();
if (dir != null && dir.equals(side) && !canSupportCenter(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();
// Otherwise remove the cable and drop the modem manually.
state = state.setValue(CableBlock.MODEM, CableModemVariant.None);
if (level instanceof Level actualLevel) {
Block.popResource(actualLevel, pos, new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
}
if (level.getBlockEntity(pos) instanceof CableBlockEntity cable) cable.scheduleConnectionsChanged();
}
var modem = state.getValue(MODEM);
if (modem.getFacing() == side && modem.isPeripheralOn() && level.getBlockEntity(pos) instanceof CableBlockEntity cable) {
cable.queueRefreshPeripheral();
}
return level instanceof Level actualLevel
? state.setValue(CONNECTIONS.get(side), doesConnectVisually(state, actualLevel, pos, side))
: state;
}
@Override
@ -230,6 +249,7 @@ public static BlockState correctConnections(Level world, BlockPos pos, BlockStat
@Override
@Deprecated
public final InteractionResult use(BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
return world.getBlockEntity(pos) instanceof CableBlockEntity modem ? modem.use(player) : InteractionResult.PASS;
}

View File

@ -7,7 +7,6 @@
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.network.wired.WiredNode;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.text.ChatHelpers;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.platform.ComponentAccess;
@ -20,21 +19,16 @@
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.Objects;
public class CableBlockEntity extends BlockEntity {
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
private final class CableElement extends WiredModemElement {
@Override
public Level getLevel() {
@ -57,34 +51,22 @@ protected void detachPeripheral(String name) {
}
}
private boolean invalidPeripheral;
private boolean peripheralAccessAllowed;
private boolean refreshPeripheral;
private final WiredModemLocalPeripheral peripheral = new WiredModemLocalPeripheral(PlatformHelper.get().createPeripheralAccess(this, x -> queueRefreshPeripheral()));
private @Nullable Runnable modemChanged;
private boolean connectionsFormed = false;
private boolean connectionsChanged = false;
private boolean refreshConnections = false;
private final WiredModemElement cable = new CableElement();
private final WiredNode node = cable.getNode();
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final WiredModemPeripheral modem = new WiredModemPeripheral(
new ModemState(() -> TickScheduler.schedule(tickToken)),
cable
new ModemState(() -> TickScheduler.schedule(tickToken)), cable, peripheral, this
) {
@Override
protected WiredModemLocalPeripheral getLocalPeripheral() {
return peripheral;
}
@Override
public Vec3 getPosition() {
return Vec3.atCenterOf(getBlockPos().relative(getDirection()));
}
@Override
public Object getTarget() {
return CableBlockEntity.this;
var dir = getModemDirection();
return Vec3.atCenterOf(dir == null ? getBlockPos() : getBlockPos().relative(dir));
}
};
@ -94,93 +76,61 @@ public CableBlockEntity(BlockEntityType<? extends CableBlockEntity> type, BlockP
super(type, pos, state);
}
private void onRemove() {
if (level == null || !level.isClientSide) {
node.remove();
connectionsFormed = false;
}
}
@Override
public void setRemoved() {
super.setRemoved();
modem.removed();
onRemove();
if (level == null || !level.isClientSide) node.remove();
}
@Override
public void clearRemoved() {
super.clearRemoved();
refreshConnections = refreshPeripheral = true;
TickScheduler.schedule(tickToken);
}
@Override
@Deprecated
public void setBlockState(BlockState state) {
var direction = getMaybeDirection();
var direction = getModemDirection();
var hasCable = hasCable();
super.setBlockState(state);
// We invalidate both the modem and element if the modem's direction is different.
if (getMaybeDirection() != direction && modemChanged != null) modemChanged.run();
// We invalidate both the modem and element if the modem direction or cable are different.
if (modemChanged != null && (hasCable() != hasCable || getModemDirection() != direction)) modemChanged.run();
}
@Nullable
private Direction getMaybeDirection() {
private Direction getModemDirection() {
return getBlockState().getValue(CableBlock.MODEM).getFacing();
}
private Direction getDirection() {
var direction = getMaybeDirection();
return direction == null ? Direction.NORTH : direction;
}
void neighborChanged(BlockPos neighbour) {
var dir = getDirection();
if (neighbour.equals(getBlockPos().relative(dir)) && hasModem() && !getBlockState().canSurvive(getLevel(), getBlockPos())) {
if (hasCable()) {
// Drop the modem and convert to cable
Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
getLevel().setBlockAndUpdate(getBlockPos(), getBlockState().setValue(CableBlock.MODEM, CableModemVariant.None));
modemChanged();
connectionsChanged();
} else {
// Drop everything and remove block
Block.popResource(getLevel(), getBlockPos(), new ItemStack(ModRegistry.Items.WIRED_MODEM.get()));
getLevel().removeBlock(getBlockPos(), false);
// This'll call #destroy(), so we don't need to reset the network here.
}
return;
}
if (!level.isClientSide && peripheralAccessAllowed) {
var facing = getDirection();
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral();
var dir = getModemDirection();
if (!level.isClientSide && dir != null && getBlockPos().relative(dir).equals(neighbour) && isPeripheralOn()) {
queueRefreshPeripheral();
}
}
private void queueRefreshPeripheral() {
if (invalidPeripheral) return;
invalidPeripheral = true;
void queueRefreshPeripheral() {
refreshPeripheral = true;
TickScheduler.schedule(tickToken);
}
private void refreshPeripheral() {
invalidPeripheral = false;
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), getDirection())) {
updateConnectedPeripherals();
}
}
InteractionResult use(Player player) {
if (player.isCrouching() || !player.mayBuild()) return InteractionResult.PASS;
if (!canAttachPeripheral()) return InteractionResult.FAIL;
if (getLevel().isClientSide) return InteractionResult.SUCCESS;
var oldName = peripheral.getConnectedName();
togglePeripheralAccess();
if (isPeripheralOn()) {
detachPeripheral();
} else {
attachPeripheral();
}
var newName = peripheral.getConnectedName();
if (!Objects.equals(newName, oldName)) {
if (oldName != null) {
player.displayClientMessage(Component.translatable("chat.computercraft.wired_modem.peripheral_disconnected",
@ -198,14 +148,11 @@ InteractionResult use(Player player) {
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
// Fallback to the previous (incorrect) key
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED) || nbt.getBoolean("PeirpheralAccess");
peripheral.read(nbt, "");
}
@Override
public void saveAdditional(CompoundTag nbt) {
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
peripheral.write(nbt, "");
super.saveAdditional(nbt);
}
@ -214,7 +161,7 @@ private void updateBlockState() {
var state = getBlockState();
var oldVariant = state.getValue(CableBlock.MODEM);
var newVariant = CableModemVariant
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheralAccessAllowed);
.from(oldVariant.getFacing(), modem.getModemState().isOpen(), peripheral.hasPeripheral());
if (oldVariant != newVariant) {
level.setBlockAndUpdate(getBlockPos(), state.setValue(CableBlock.MODEM, newVariant));
@ -224,31 +171,24 @@ private void updateBlockState() {
void blockTick() {
if (getLevel().isClientSide) return;
if (invalidPeripheral) refreshPeripheral();
if (refreshPeripheral) {
refreshPeripheral = false;
if (isPeripheralOn()) attachPeripheral();
}
if (modem.getModemState().pollChanged()) updateBlockState();
if (!connectionsFormed) {
connectionsFormed = true;
connectionsChanged();
if (peripheralAccessAllowed) {
peripheral.attach(level, worldPosition, getDirection());
updateConnectedPeripherals();
}
}
if (connectionsChanged) connectionsChanged();
if (refreshConnections) connectionsChanged();
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
void scheduleConnectionsChanged() {
refreshConnections = true;
TickScheduler.schedule(tickToken);
}
void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
refreshConnections = false;
var state = getBlockState();
var world = getLevel();
@ -269,51 +209,24 @@ void connectionsChanged() {
this.node.disconnectFrom(node);
}
}
// If we can no longer attach peripherals, then detach any which may have existed
if (!canAttachPeripheral()) detachPeripheral();
}
void modemChanged() {
// Tell anyone who cares that the connection state has changed
if (modemChanged != null) modemChanged.run();
if (getLevel().isClientSide) return;
// If we can no longer attach peripherals, then detach any
// which may have existed
if (!canAttachPeripheral() && peripheralAccessAllowed) {
peripheralAccessAllowed = false;
peripheral.detach();
node.updatePeripherals(Map.of());
setChanged();
updateBlockState();
}
private void attachPeripheral() {
var dir = Objects.requireNonNull(getModemDirection(), "Attaching without a modem");
if (peripheral.attach(getLevel(), getBlockPos(), dir)) updateConnectedPeripherals();
updateBlockState();
}
private void togglePeripheralAccess() {
if (!peripheralAccessAllowed) {
peripheral.attach(level, getBlockPos(), getDirection());
if (!peripheral.hasPeripheral()) return;
peripheralAccessAllowed = true;
node.updatePeripherals(peripheral.toMap());
} else {
peripheral.detach();
peripheralAccessAllowed = false;
node.updatePeripherals(Map.of());
}
private void detachPeripheral() {
if (peripheral.detach()) updateConnectedPeripherals();
updateBlockState();
}
private void updateConnectedPeripherals() {
var peripherals = peripheral.toMap();
if (peripherals.isEmpty()) {
// If there are no peripherals then disable access and update the display state.
peripheralAccessAllowed = false;
updateBlockState();
}
node.updatePeripherals(peripherals);
node.updatePeripherals(peripheral.toMap());
}
@Nullable
@ -323,7 +236,11 @@ public WiredElement getWiredElement(@Nullable Direction direction) {
@Nullable
public IPeripheral getPeripheral(@Nullable Direction direction) {
return direction == null || getMaybeDirection() == direction ? modem : null;
return direction == null || getModemDirection() == direction ? modem : null;
}
private boolean isPeripheralOn() {
return getBlockState().getValue(CableBlock.MODEM).isPeripheralOn();
}
public void onModemChanged(Runnable callback) {

View File

@ -35,10 +35,7 @@ boolean placeAt(Level world, BlockPos pos, BlockState state) {
world.playSound(null, pos, soundType.getPlaceSound(), SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F);
var tile = world.getBlockEntity(pos);
if (tile instanceof CableBlockEntity cable) {
cable.modemChanged();
cable.connectionsChanged();
}
if (tile instanceof CableBlockEntity cable) cable.connectionsChanged();
return true;
}

View File

@ -10,49 +10,57 @@
import javax.annotation.Nullable;
public enum CableModemVariant implements StringRepresentable {
None("none", null),
DownOff("down_off", Direction.DOWN),
UpOff("up_off", Direction.UP),
NorthOff("north_off", Direction.NORTH),
SouthOff("south_off", Direction.SOUTH),
WestOff("west_off", Direction.WEST),
EastOff("east_off", Direction.EAST),
DownOn("down_on", Direction.DOWN),
UpOn("up_on", Direction.UP),
NorthOn("north_on", Direction.NORTH),
SouthOn("south_on", Direction.SOUTH),
WestOn("west_on", Direction.WEST),
EastOn("east_on", Direction.EAST),
DownOffPeripheral("down_off_peripheral", Direction.DOWN),
UpOffPeripheral("up_off_peripheral", Direction.UP),
NorthOffPeripheral("north_off_peripheral", Direction.NORTH),
SouthOffPeripheral("south_off_peripheral", Direction.SOUTH),
WestOffPeripheral("west_off_peripheral", Direction.WEST),
EastOffPeripheral("east_off_peripheral", Direction.EAST),
DownOnPeripheral("down_on_peripheral", Direction.DOWN),
UpOnPeripheral("up_on_peripheral", Direction.UP),
NorthOnPeripheral("north_on_peripheral", Direction.NORTH),
SouthOnPeripheral("south_on_peripheral", Direction.SOUTH),
WestOnPeripheral("west_on_peripheral", Direction.WEST),
EastOnPeripheral("east_on_peripheral", Direction.EAST);
None("none", null, false, false),
DownOff("down_off", Direction.DOWN, false, false),
UpOff("up_off", Direction.UP, false, false),
NorthOff("north_off", Direction.NORTH, false, false),
SouthOff("south_off", Direction.SOUTH, false, false),
WestOff("west_off", Direction.WEST, false, false),
EastOff("east_off", Direction.EAST, false, false),
DownOn("down_on", Direction.DOWN, true, false),
UpOn("up_on", Direction.UP, true, false),
NorthOn("north_on", Direction.NORTH, true, false),
SouthOn("south_on", Direction.SOUTH, true, false),
WestOn("west_on", Direction.WEST, true, false),
EastOn("east_on", Direction.EAST, true, false),
DownOffPeripheral("down_off_peripheral", Direction.DOWN, false, true),
UpOffPeripheral("up_off_peripheral", Direction.UP, false, true),
NorthOffPeripheral("north_off_peripheral", Direction.NORTH, false, true),
SouthOffPeripheral("south_off_peripheral", Direction.SOUTH, false, true),
WestOffPeripheral("west_off_peripheral", Direction.WEST, false, true),
EastOffPeripheral("east_off_peripheral", Direction.EAST, false, true),
DownOnPeripheral("down_on_peripheral", Direction.DOWN, true, true),
UpOnPeripheral("up_on_peripheral", Direction.UP, true, true),
NorthOnPeripheral("north_on_peripheral", Direction.NORTH, true, true),
SouthOnPeripheral("south_on_peripheral", Direction.SOUTH, true, true),
WestOnPeripheral("west_on_peripheral", Direction.WEST, true, true),
EastOnPeripheral("east_on_peripheral", Direction.EAST, true, true);
private static final CableModemVariant[] VALUES = values();
private final String name;
private final @Nullable Direction facing;
private final boolean modemOn, peripheralOn;
CableModemVariant(String name, @Nullable Direction facing) {
CableModemVariant(String name, @Nullable Direction facing, boolean modemOn, boolean peripheralOn) {
this.name = name;
this.facing = facing;
this.modemOn = modemOn;
this.peripheralOn = peripheralOn;
if (ordinal() != getIndex(facing, modemOn, peripheralOn)) throw new IllegalStateException("Mismatched ordinal");
}
public static CableModemVariant from(Direction facing) {
return facing == null ? None : VALUES[1 + facing.get3DDataValue()];
return VALUES[1 + facing.get3DDataValue()];
}
private static int getIndex(@Nullable Direction facing, boolean modem, boolean peripheral) {
var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
return facing == null ? 0 : 1 + 6 * state + facing.get3DDataValue();
}
public static CableModemVariant from(@Nullable Direction facing, boolean modem, boolean peripheral) {
var state = (modem ? 1 : 0) + (peripheral ? 2 : 0);
return facing == null ? None : VALUES[1 + 6 * state + facing.get3DDataValue()];
return VALUES[getIndex(facing, modem, peripheral)];
}
@Override
@ -64,6 +72,14 @@ public String getSerializedName() {
return facing;
}
public boolean isModemOn() {
return modemOn;
}
public boolean isPeripheralOn() {
return peripheralOn;
}
@Override
public String toString() {
return name;

View File

@ -7,12 +7,14 @@
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.shared.ModRegistry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
@ -49,13 +51,27 @@ public final InteractionResult use(BlockState state, Level world, BlockPos pos,
@Override
@Deprecated
public final void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbourPos);
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) {
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
modem.queueRefreshPeripheral(direction);
}
return super.updateShape(state, direction, neighborState, level, pos, neighborPos);
}
@Override
@Deprecated
public final void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean isMoving) {
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
modem.neighborChanged(neighbourPos);
}
}
@ForgeOverride
public final void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbour) {
if (world.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) modem.neighborChanged(neighbour);
public final void onNeighborChange(BlockState state, LevelReader level, BlockPos pos, BlockPos neighbour) {
if (state.getValue(PERIPHERAL_ON) && level.getBlockEntity(pos) instanceof WiredModemFullBlockEntity modem) {
modem.neighborChanged(neighbour);
}
}
@Override

View File

@ -32,8 +32,6 @@
import static dan200.computercraft.shared.peripheral.modem.wired.WiredModemFullBlock.PERIPHERAL_ON;
public class WiredModemFullBlockEntity extends BlockEntity {
private static final String NBT_PERIPHERAL_ENABLED = "PeripheralAccess";
private static final class FullElement extends WiredModemElement {
private final WiredModemFullBlockEntity entity;
@ -70,11 +68,9 @@ public Vec3 getPosition() {
private final WiredModemPeripheral[] modems = new WiredModemPeripheral[6];
private boolean peripheralAccessAllowed = false;
private final WiredModemLocalPeripheral[] peripherals = new WiredModemLocalPeripheral[6];
private boolean connectionsFormed = false;
private boolean connectionsChanged = false;
private boolean refreshConnections = false;
private final TickScheduler.Token tickToken = new TickScheduler.Token(this);
private final ModemState modemState = new ModemState(() -> TickScheduler.schedule(tickToken));
@ -96,31 +92,30 @@ public WiredModemFullBlockEntity(BlockEntityType<WiredModemFullBlockEntity> type
@Override
public void setRemoved() {
super.setRemoved();
if (level == null || !level.isClientSide) {
node.remove();
connectionsFormed = false;
for (var modem : modems) {
if (modem != null) modem.removed();
}
if (level == null || !level.isClientSide) node.remove();
}
@Override
public void clearRemoved() {
super.clearRemoved();
refreshConnections = true;
invalidSides = DirectionUtil.ALL_SIDES;
TickScheduler.schedule(tickToken);
}
void neighborChanged(BlockPos neighbour) {
if (!level.isClientSide && peripheralAccessAllowed) {
for (var facing : DirectionUtil.FACINGS) {
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
for (var facing : DirectionUtil.FACINGS) {
if (getBlockPos().relative(facing).equals(neighbour)) queueRefreshPeripheral(facing);
}
}
private void queueRefreshPeripheral(Direction facing) {
if (invalidSides == 0) TickScheduler.schedule(tickToken);
void queueRefreshPeripheral(Direction facing) {
invalidSides |= 1 << facing.ordinal();
}
private void refreshPeripheral(Direction facing) {
invalidSides &= ~(1 << facing.ordinal());
var peripheral = peripherals[facing.ordinal()];
if (level != null && !isRemoved() && peripheral.attach(level, getBlockPos(), facing)) {
updateConnectedPeripherals();
}
TickScheduler.schedule(tickToken);
}
public InteractionResult use(Player player) {
@ -129,7 +124,11 @@ public InteractionResult use(Player player) {
// On server, we interacted if a peripheral was found
var oldPeriphNames = getConnectedPeripheralNames();
togglePeripheralAccess();
if (isPeripheralOn()) {
detachPeripherals();
} else {
attachPeripherals(DirectionUtil.ALL_SIDES);
}
var periphNames = getConnectedPeripheralNames();
if (!Objects.equals(periphNames, oldPeriphNames)) {
@ -158,65 +157,45 @@ private static void sendPeripheralChanges(Player player, String kind, Collection
@Override
public void load(CompoundTag nbt) {
super.load(nbt);
peripheralAccessAllowed = nbt.getBoolean(NBT_PERIPHERAL_ENABLED);
for (var i = 0; i < peripherals.length; i++) peripherals[i].read(nbt, Integer.toString(i));
}
@Override
public void saveAdditional(CompoundTag nbt) {
nbt.putBoolean(NBT_PERIPHERAL_ENABLED, peripheralAccessAllowed);
for (var i = 0; i < peripherals.length; i++) peripherals[i].write(nbt, Integer.toString(i));
super.saveAdditional(nbt);
}
private void updateBlockState() {
var state = getBlockState();
boolean modemOn = modemState.isOpen(), peripheralOn = peripheralAccessAllowed;
if (state.getValue(MODEM_ON) == modemOn && state.getValue(PERIPHERAL_ON) == peripheralOn) return;
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn).setValue(PERIPHERAL_ON, peripheralOn));
}
@Override
public void clearRemoved() {
super.clearRemoved();
TickScheduler.schedule(tickToken);
}
void blockTick() {
if (getLevel().isClientSide) return;
if (invalidSides != 0) {
for (var direction : DirectionUtil.FACINGS) {
if ((invalidSides & (1 << direction.ordinal())) != 0) refreshPeripheral(direction);
}
var oldInvalidSides = invalidSides;
invalidSides = 0;
if (isPeripheralOn()) attachPeripherals(oldInvalidSides);
}
if (modemState.pollChanged()) updateBlockState();
if (modemState.pollChanged()) updateModemBlockState();
if (!connectionsFormed) {
connectionsFormed = true;
if (refreshConnections) connectionsChanged();
}
connectionsChanged();
if (peripheralAccessAllowed) {
for (var facing : DirectionUtil.FACINGS) {
peripherals[facing.ordinal()].attach(level, getBlockPos(), facing);
}
updateConnectedPeripherals();
}
}
private void updateModemBlockState() {
var state = getBlockState();
var modemOn = modemState.isOpen();
if (state.getValue(MODEM_ON) == modemOn) return;
if (connectionsChanged) connectionsChanged();
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(MODEM_ON, modemOn));
}
private void scheduleConnectionsChanged() {
connectionsChanged = true;
refreshConnections = true;
TickScheduler.schedule(tickToken);
}
private void connectionsChanged() {
if (getLevel().isClientSide) return;
connectionsChanged = false;
refreshConnections = false;
var world = getLevel();
var current = getBlockPos();
@ -231,57 +210,48 @@ private void connectionsChanged() {
}
}
private void togglePeripheralAccess() {
if (!peripheralAccessAllowed) {
var hasAny = false;
for (var facing : DirectionUtil.FACINGS) {
var peripheral = peripherals[facing.ordinal()];
peripheral.attach(level, getBlockPos(), facing);
hasAny |= peripheral.hasPeripheral();
}
if (!hasAny) return;
peripheralAccessAllowed = true;
node.updatePeripherals(getConnectedPeripherals());
} else {
peripheralAccessAllowed = false;
for (var peripheral : peripherals) peripheral.detach();
node.updatePeripherals(Map.of());
}
updateBlockState();
}
private Set<String> getConnectedPeripheralNames() {
if (!peripheralAccessAllowed) return Set.of();
Set<String> peripherals = new HashSet<>(6);
private List<String> getConnectedPeripheralNames() {
List<String> peripherals = new ArrayList<>(6);
for (var peripheral : this.peripherals) {
var name = peripheral.getConnectedName();
if (name != null) peripherals.add(name);
}
peripherals.sort(String::compareTo);
return peripherals;
}
private Map<String, IPeripheral> getConnectedPeripherals() {
if (!peripheralAccessAllowed) return Map.of();
private void attachPeripherals(int sides) {
var anyChanged = false;
Map<String, IPeripheral> peripherals = new HashMap<>(6);
for (var peripheral : this.peripherals) peripheral.extendMap(peripherals);
return Collections.unmodifiableMap(peripherals);
}
Map<String, IPeripheral> attachedPeripherals = new HashMap<>(6);
private void updateConnectedPeripherals() {
var peripherals = getConnectedPeripherals();
if (peripherals.isEmpty()) {
// If there are no peripherals then disable access and update the display state.
peripheralAccessAllowed = false;
updateBlockState();
for (var facing : DirectionUtil.FACINGS) {
var peripheral = peripherals[facing.ordinal()];
if (DirectionUtil.isSet(sides, facing)) anyChanged |= peripheral.attach(getLevel(), getBlockPos(), facing);
peripheral.extendMap(attachedPeripherals);
}
node.updatePeripherals(peripherals);
if (anyChanged) node.updatePeripherals(attachedPeripherals);
updatePeripheralBlocKState(!attachedPeripherals.isEmpty());
}
private void detachPeripherals() {
var anyChanged = false;
for (var peripheral : peripherals) anyChanged |= peripheral.detach();
if (anyChanged) node.updatePeripherals(Map.of());
updatePeripheralBlocKState(false);
}
private void updatePeripheralBlocKState(boolean peripheralOn) {
var state = getBlockState();
if (state.getValue(PERIPHERAL_ON) == peripheralOn) return;
getLevel().setBlockAndUpdate(getBlockPos(), state.setValue(PERIPHERAL_ON, peripheralOn));
}
private boolean isPeripheralOn() {
return getBlockState().getValue(PERIPHERAL_ON);
}
public WiredElement getElement() {
@ -295,22 +265,11 @@ public WiredModemPeripheral getPeripheral(@Nullable Direction side) {
var peripheral = modems[side.ordinal()];
if (peripheral != null) return peripheral;
var localPeripheral = peripherals[side.ordinal()];
return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element) {
@Override
protected WiredModemLocalPeripheral getLocalPeripheral() {
return localPeripheral;
}
return modems[side.ordinal()] = new WiredModemPeripheral(modemState, element, peripherals[side.ordinal()], this) {
@Override
public Vec3 getPosition() {
return Vec3.atCenterOf(getBlockPos().relative(side));
}
@Override
public Object getTarget() {
return WiredModemFullBlockEntity.this;
}
};
}
}

View File

@ -6,6 +6,7 @@
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.platform.ComponentAccess;
import net.minecraft.core.BlockPos;
@ -65,7 +66,7 @@ public boolean attach(Level world, BlockPos origin, Direction direction) {
this.id = ServerContext.get(assertNonNull(world.getServer())).getNextId("peripheral." + type);
}
return oldPeripheral == null || !oldPeripheral.equals(peripheral);
return !PeripheralHelpers.equals(oldPeripheral, peripheral);
}
}

View File

@ -15,6 +15,7 @@
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.apis.PeripheralAPI;
import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.shared.computer.core.ServerContext;
@ -22,6 +23,7 @@
import dan200.computercraft.shared.peripheral.modem.ModemState;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,12 +36,21 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private static final Logger LOG = LoggerFactory.getLogger(WiredModemPeripheral.class);
private final WiredModemElement modem;
private final WiredModemLocalPeripheral localPeripheral;
private final BlockEntity target;
private final Map<IComputerAccess, ConcurrentMap<String, RemotePeripheralWrapper>> peripheralWrappers = new HashMap<>(1);
public WiredModemPeripheral(ModemState state, WiredModemElement modem) {
public WiredModemPeripheral(
ModemState state,
WiredModemElement modem,
WiredModemLocalPeripheral localPeripheral,
BlockEntity target
) {
super(state);
this.modem = modem;
this.localPeripheral = localPeripheral;
this.target = target;
}
//region IPacketSender implementation
@ -62,8 +73,6 @@ protected PacketNetwork getNetwork() {
public Level getLevel() {
return modem.getLevel();
}
protected abstract WiredModemLocalPeripheral getLocalPeripheral();
//endregion
@Override
@ -207,7 +216,7 @@ public final MethodResult callRemote(IComputerAccess computer, ILuaContext conte
*/
@LuaFunction
public final @Nullable Object[] getNameLocal() {
var local = getLocalPeripheral().getConnectedName();
var local = localPeripheral.getConnectedName();
return local == null ? null : new Object[]{ local };
}
@ -218,8 +227,7 @@ public void attach(IComputerAccess computer) {
ConcurrentMap<String, RemotePeripheralWrapper> wrappers;
synchronized (peripheralWrappers) {
wrappers = peripheralWrappers.get(computer);
if (wrappers == null) peripheralWrappers.put(computer, wrappers = new ConcurrentHashMap<>());
wrappers = peripheralWrappers.computeIfAbsent(computer, k -> new ConcurrentHashMap<>());
}
synchronized (modem.getRemotePeripherals()) {
@ -245,11 +253,13 @@ public void detach(IComputerAccess computer) {
}
@Override
public boolean equals(@Nullable IPeripheral other) {
if (other instanceof WiredModemPeripheral otherModem) {
return otherModem.modem == modem;
}
return false;
public final boolean equals(@Nullable IPeripheral other) {
return other instanceof WiredModemPeripheral otherModem && otherModem.modem == modem;
}
@Override
public final Object getTarget() {
return target;
}
//endregion
@ -272,12 +282,11 @@ public void detachPeripheral(String name) {
var wrapper = wrappers.remove(name);
if (wrapper != null) wrapper.detach();
}
}
}
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) {
if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
if (!peripherals.containsKey(periphName) && !periphName.equals(localPeripheral.getConnectedName())) {
var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
peripherals.put(periphName, wrapper);
@ -296,7 +305,7 @@ private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String
return wrappers == null ? null : wrappers.get(remoteName);
}
private static class RemotePeripheralWrapper implements IComputerAccess {
private static class RemotePeripheralWrapper implements IComputerAccess, GuardedLuaContext.Guard {
private final WiredModemElement element;
private final IPeripheral peripheral;
private final IComputerAccess computer;
@ -309,6 +318,8 @@ private static class RemotePeripheralWrapper implements IComputerAccess {
private volatile boolean attached;
private final Set<String> mounts = new HashSet<>();
private @Nullable GuardedLuaContext contextWrapper;
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
this.element = element;
this.peripheral = peripheral;
@ -356,7 +367,19 @@ public Collection<String> getMethodNames() {
public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
var method = methodMap.get(methodName);
if (method == null) throw new LuaException("No such method " + methodName);
return method.apply(peripheral, context, this, arguments);
// Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations.
var contextWrapper = this.contextWrapper;
if (contextWrapper == null || !contextWrapper.wraps(context)) {
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
}
return method.apply(peripheral, contextWrapper, this, arguments);
}
@Override
public boolean checkValid() {
return attached;
}
// IComputerAccess implementation

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

@ -37,7 +37,7 @@ class DfpwmState {
private boolean unplayed = true;
private long clientEndTime = PauseAwareTimer.getTime();
private float pendingVolume = 1.0f;
private @Nullable ByteBuffer pendingAudio;
private @Nullable EncodedAudio pendingAudio;
synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> volume) throws LuaException {
if (pendingAudio != null) return false;
@ -45,6 +45,10 @@ synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double>
var outSize = size / 8;
var buffer = ByteBuffer.allocate(outSize);
var initialCharge = charge;
var initialStrength = strength;
var initialPreviousBit = previousBit;
for (var i = 0; i < outSize; i++) {
var thisByte = 0;
for (var j = 1; j <= 8; j++) {
@ -80,7 +84,7 @@ synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double>
buffer.flip();
pendingAudio = buffer;
pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer);
pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume));
return true;
}
@ -89,12 +93,12 @@ boolean shouldSendPending(long now) {
return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER;
}
ByteBuffer pullPending(long now) {
EncodedAudio pullPending(long now) {
var audio = pendingAudio;
if (audio == null) throw new IllegalStateException("Should not pull pending audio yet");
pendingAudio = null;
// Compute when we should consider sending the next packet.
clientEndTime = Math.max(now, clientEndTime) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE);
clientEndTime = Math.max(now, clientEndTime) + (audio.audio().remaining() * SECOND * 8 / SAMPLE_RATE);
unplayed = false;
return audio;
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.speaker;
import net.minecraft.network.FriendlyByteBuf;
import java.nio.ByteBuffer;
/**
* A chunk of encoded audio, along with the state required for the decoder to reproduce the original audio samples.
*
* @param charge The DFPWM charge.
* @param strength The DFPWM strength.
* @param previousBit The previous bit.
* @param audio The block of encoded audio.
*/
public record EncodedAudio(int charge, int strength, boolean previousBit, ByteBuffer audio) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(charge());
buf.writeVarInt(strength());
buf.writeBoolean(previousBit());
buf.writeBytes(audio().duplicate());
}
public static EncodedAudio read(FriendlyByteBuf buf) {
var charge = buf.readVarInt();
var strength = buf.readVarInt();
var previousBit = buf.readBoolean();
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
return new EncodedAudio(charge, strength, previousBit, ByteBuffer.wrap(bytes));
}
}

View File

@ -18,16 +18,18 @@
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.network.server.ServerNetworking;
import dan200.computercraft.shared.platform.PlatformHelper;
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.Registries;
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 +254,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 = PlatformHelper.get().tryGetRegistryObject(Registries.SOUND_EVENT, 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

@ -389,4 +389,13 @@ default double getReachDistance(Player player) {
* @see ServerPlayerGameMode#useItemOn(ServerPlayer, Level, ItemStack, InteractionHand, BlockHitResult)
*/
InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitResult hit, Predicate<BlockState> canUseBlock);
/**
* Whether {@link net.minecraft.network.chat.ClickEvent.Action#RUN_COMMAND} can be used to run client commands.
*
* @return Whether client commands can be triggered from chat components.
*/
default boolean canClickRunClientCommand() {
return true;
}
}

View File

@ -190,6 +190,6 @@ protected void onTerminalChanged() {
@Override
protected void onRemoved() {
super.onRemoved();
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceID()), getLevel().getServer());
ServerNetworking.sendToAllPlayers(new PocketComputerDeletedClientMessage(getInstanceUUID()), getLevel().getServer());
}
}

View File

@ -43,6 +43,7 @@
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class PocketComputerItem extends Item implements IComputerItem, IMedia, IColouredItem {
private static final String NBT_UPGRADE = "Upgrade";
@ -188,10 +189,9 @@ public String getCreatorModId(ItemStack stack) {
}
public PocketServerComputer createServerComputer(ServerLevel level, Entity entity, @Nullable Container inventory, ItemStack stack) {
var sessionID = getSessionID(stack);
var registry = ServerContext.get(level.getServer()).registry();
var computer = (PocketServerComputer) registry.get(sessionID, getInstanceID(stack));
var computer = (PocketServerComputer) registry.get(getSessionID(stack), getInstanceID(stack));
if (computer == null) {
var computerID = getComputerID(stack);
if (computerID < 0) {
@ -201,8 +201,9 @@ public PocketServerComputer createServerComputer(ServerLevel level, Entity entit
computer = new PocketServerComputer(level, entity.blockPosition(), getComputerID(stack), getLabel(stack), getFamily());
setInstanceID(stack, computer.register());
setSessionID(stack, registry.getSessionID());
var tag = stack.getOrCreateTag();
tag.putInt(NBT_SESSION, registry.getSessionID());
tag.putUUID(NBT_INSTANCE, computer.register());
var upgrade = getUpgrade(stack);
@ -267,13 +268,9 @@ public boolean setLabel(ItemStack stack, @Nullable String label) {
return null;
}
public static int getInstanceID(ItemStack stack) {
public static @Nullable UUID getInstanceID(ItemStack stack) {
var nbt = stack.getTag();
return nbt != null && nbt.contains(NBT_INSTANCE) ? nbt.getInt(NBT_INSTANCE) : -1;
}
private static void setInstanceID(ItemStack stack, int instanceID) {
stack.getOrCreateTag().putInt(NBT_INSTANCE, instanceID);
return nbt != null && nbt.hasUUID(NBT_INSTANCE) ? nbt.getUUID(NBT_INSTANCE) : null;
}
private static int getSessionID(ItemStack stack) {
@ -281,10 +278,6 @@ private static int getSessionID(ItemStack stack) {
return nbt != null && nbt.contains(NBT_SESSION) ? nbt.getInt(NBT_SESSION) : -1;
}
private static void setSessionID(ItemStack stack, int sessionID) {
stack.getOrCreateTag().putInt(NBT_SESSION, sessionID);
}
private static boolean isMarkedOn(ItemStack stack) {
var nbt = stack.getTag();
return nbt != null && nbt.getBoolean(NBT_ON);

View File

@ -24,10 +24,9 @@
import net.minecraft.core.Direction;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
@ -136,17 +135,8 @@ public void loadServer(CompoundTag nbt) {
super.loadServer(nbt);
// Read inventory
var nbttaglist = nbt.getList("Items", Tag.TAG_COMPOUND);
inventory.clear();
inventorySnapshot.clear();
for (var i = 0; i < nbttaglist.size(); i++) {
var tag = nbttaglist.getCompound(i);
var slot = tag.getByte("Slot") & 0xff;
if (slot < getContainerSize()) {
inventory.set(slot, ItemStack.of(tag));
inventorySnapshot.set(slot, inventory.get(slot).copy());
}
}
ContainerHelper.loadAllItems(nbt, inventory);
for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());
// Read state
brain.readFromNBT(nbt);
@ -155,16 +145,7 @@ public void loadServer(CompoundTag nbt) {
@Override
public void saveAdditional(CompoundTag nbt) {
// Write inventory
var nbttaglist = new ListTag();
for (var i = 0; i < INVENTORY_SIZE; i++) {
if (!inventory.get(i).isEmpty()) {
var tag = new CompoundTag();
tag.putByte("Slot", (byte) i);
inventory.get(i).save(tag);
nbttaglist.add(tag);
}
}
nbt.put("Items", nbttaglist);
ContainerHelper.saveAllItems(nbt, inventory);
// Write brain
nbt = brain.writeToNBT(nbt);

View File

@ -14,6 +14,7 @@
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.util.PeripheralHelpers;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@ -589,7 +590,7 @@ private void updatePeripherals(ServerComputer serverComputer) {
}
var existing = peripherals.get(side);
if (existing == peripheral || (existing != null && peripheral != null && existing.equals(peripheral))) {
if (PeripheralHelpers.equals(existing, peripheral)) {
// If the peripheral is the same, just use that.
peripheral = existing;
} else {

View File

@ -54,7 +54,7 @@ public TurtleCommandResult execute(ITurtleAccess turtle) {
case ContainerTransfer.NO_SPACE:
return TurtleCommandResult.failure("No space for items");
case ContainerTransfer.NO_ITEMS:
return TurtleCommandResult.failure("No items to drop");
return TurtleCommandResult.failure("No items to take");
default:
turtle.playAnimation(TurtleAnimation.WAIT);
return TurtleCommandResult.success();

View File

@ -11,6 +11,11 @@ public final class DirectionUtil {
private DirectionUtil() {
}
/**
* A bitmask indicating all sides.
*/
public static final int ALL_SIDES = (1 << 6) - 1;
public static final Direction[] FACINGS = Direction.values();
public static ComputerSide toLocal(Direction front, Direction dir) {
@ -31,4 +36,15 @@ public static float toPitchAngle(Direction dir) {
default -> 0.0f;
};
}
/**
* Determine if a direction is in a bitmask.
*
* @param mask The bitmask to test
* @param direction The direction to check.
* @return Whether the direction is in a bitmask.
*/
public static boolean isSet(int mask, Direction direction) {
return (mask & (1 << direction.ordinal())) != 0;
}
}

View File

@ -7,7 +7,6 @@ accessWidener v1 named
# Additional access wideners for vanilla code. This is a effectively the subset of Fabric's transitive access wideners
# that we actually use
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ClampedItemPropertyFunction;)V
accessible method net/minecraft/client/renderer/blockentity/BlockEntityRenderers register (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/client/renderer/blockentity/BlockEntityRendererProvider;)V
accessible class net/minecraft/world/item/CreativeModeTab$Output
accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey;

View File

@ -4,6 +4,7 @@
package dan200.computercraft.client.sound;
import dan200.computercraft.shared.peripheral.speaker.EncodedAudio;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
@ -16,7 +17,7 @@ public void testDecodesBytes() {
var stream = new DfpwmStream();
var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 });
stream.push(input);
stream.push(new EncodedAudio(0, 0, false, input));
var buffer = stream.read(1024 + 1);
assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes");

View File

@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
@ -39,19 +40,19 @@ public void Parse_basic_inputs(String input, ComputerSelector expected) throws C
public static Arguments[] getArgumentTestCases() {
return new Arguments[]{
// Legacy selectors
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), OptionalInt.empty(), "some_label", null, null, null)),
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@some_label", new ComputerSelector("@some_label", OptionalInt.empty(), null, OptionalInt.empty(), "some_label", null, null, null)),
Arguments.of("~normal", new ComputerSelector("~normal", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("#123", new ComputerSelector("#123", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("123", new ComputerSelector("123", OptionalInt.of(123), null, OptionalInt.empty(), null, null, null, null)),
// New selectors
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[instance=123]", new ComputerSelector("@c[instance=123]", OptionalInt.of(123), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), OptionalInt.empty(), "foo", null, null, null)),
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("@c[]", new ComputerSelector("@c[]", OptionalInt.empty(), null, OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", new ComputerSelector("@c[instance=5e18f505-62f7-46f8-83f3-792f03224724]", OptionalInt.empty(), UUID.fromString("5e18f505-62f7-46f8-83f3-792f03224724"), OptionalInt.empty(), null, null, null, null)),
Arguments.of("@c[id=123]", new ComputerSelector("@c[id=123]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[label=\"foo\"]", new ComputerSelector("@c[label=\"foo\"]", OptionalInt.empty(), null, OptionalInt.empty(), "foo", null, null, null)),
Arguments.of("@c[family=normal]", new ComputerSelector("@c[family=normal]", OptionalInt.empty(), null, OptionalInt.empty(), null, ComputerFamily.NORMAL, null, null)),
// Complex selectors
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
Arguments.of("@c[ id = 123 , ]", new ComputerSelector("@c[ id = 123 , ]", OptionalInt.empty(), null, OptionalInt.of(123), null, null, null, null)),
Arguments.of("@c[id=123,family=normal]", new ComputerSelector("@c[id=123,family=normal]", OptionalInt.empty(), null, OptionalInt.of(123), null, ComputerFamily.NORMAL, null, null)),
};
}

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

@ -23,7 +23,7 @@ public void testEncoder() throws LuaException {
var state = new DfpwmState();
state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty());
var result = state.pullPending(0);
var result = state.pullPending(0).audio();
var contents = new byte[result.remaining()];
result.get(contents);

View File

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.test.core.ArbitraryByteBuffer;
import io.netty.buffer.Unpooled;
import net.jqwik.api.*;
import net.minecraft.network.FriendlyByteBuf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
class EncodedAudioTest {
/**
* Sends the audio on a roundtrip, ensuring that its contents are reassembled on the other end.
*
* @param audio The message to send.
*/
@Property
public void testRoundTrip(@ForAll("audio") EncodedAudio audio) {
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
audio.write(buffer);
var converted = EncodedAudio.read(buffer);
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");
assertThat("Messages are equal", converted, equalTo(converted));
}
@Provide
Arbitrary<EncodedAudio> audio() {
return Combinators.combine(
Arbitraries.integers(),
Arbitraries.integers(),
Arbitraries.of(true, false),
ArbitraryByteBuffer.bytes().ofMaxSize(1000)
).as(EncodedAudio::new);
}
}

View File

@ -11,11 +11,15 @@
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.*
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.InteractionHand
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.LeverBlock
import net.minecraft.world.level.block.RedstoneLampBlock
@ -101,6 +105,17 @@ fun Computer_peripheral(context: GameTestHelper) = context.sequence {
}
}
/**
* Check chest peripherals are reattached with a new size.
*/
@GameTest
fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
thenOnComputer { callPeripheral("right", "size").assertArrayEquals(27) }
thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
thenIdle(1)
thenOnComputer { callPeripheral("right", "size").assertArrayEquals(54) }
}
/**
* Check the client can open the computer UI and interact with it.
*/

View File

@ -7,18 +7,21 @@
import dan200.computercraft.api.lua.ObjectArguments
import dan200.computercraft.core.apis.PeripheralAPI
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.gametest.api.getBlockEntity
import dan200.computercraft.gametest.api.sequence
import dan200.computercraft.gametest.api.thenOnComputer
import dan200.computercraft.gametest.api.thenStartComputer
import dan200.computercraft.gametest.api.*
import dan200.computercraft.impl.network.wired.WiredNodeImpl
import dan200.computercraft.shared.ModRegistry
import dan200.computercraft.shared.peripheral.modem.wired.CableBlock
import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant
import dan200.computercraft.test.core.assertArrayEquals
import dan200.computercraft.test.core.computer.LuaTaskContext
import dan200.computercraft.test.core.computer.getApi
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.gametest.framework.GameTest
import net.minecraft.gametest.framework.GameTestHelper
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.block.Blocks
import org.junit.jupiter.api.Assertions.assertEquals
import kotlin.time.Duration.Companion.milliseconds
@ -83,7 +86,86 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
thenExecute {
val modem1 = helper.getBlockEntity(BlockPos(1, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
val modem2 = helper.getBlockEntity(BlockPos(3, 2, 1), ModRegistry.BlockEntities.WIRED_MODEM_FULL.get())
assertEquals(modem1.element.node.network, modem2.element.node.network, "On the same network")
assertEquals((modem1.element.node as WiredNodeImpl).network, (modem2.element.node as WiredNodeImpl).network, "On the same network")
}
}
/**
* Modems do not include the current peripheral when attached.
*/
@GameTest
fun Cable_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
// Modem does not report the computer as a peripheral.
thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
// However, if we connect the network, the other modem does.
thenExecute {
helper.setBlock(
BlockPos(1, 2, 3),
ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
)
}
thenIdle(2)
thenOnComputer { assertEquals(listOf("back", "computer_0", "right"), getPeripheralNames()) }
}
/**
* Modems do not include the current peripheral when attached.
*/
@GameTest
fun Full_block_modem_does_not_report_self(helper: GameTestHelper) = helper.sequence {
// Modem does not report the computer as a peripheral.
thenOnComputer { assertEquals(listOf("back", "right"), getPeripheralNames()) }
// However, if we connect the network, the other modem does.
thenExecute {
helper.setBlock(
BlockPos(1, 2, 3),
ModRegistry.Blocks.CABLE.get().defaultBlockState().setValue(CableBlock.CABLE, true),
)
}
thenIdle(2)
thenOnComputer { assertEquals(listOf("back", "computer_1", "right"), getPeripheralNames()) }
}
/**
* Test wired modems (without a cable) drop an item when the adjacent block is removed.
*/
@GameTest
fun Modem_drops_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
thenExecute {
helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
helper.assertBlockPresent(Blocks.AIR, BlockPos(2, 2, 2))
}
}
/**
* Test wired modems (with a cable) drop an item, but keep their cable when the adjacent block is removed.
*/
@GameTest
fun Modem_keeps_cable_when_neighbour_removed(helper: GameTestHelper) = helper.sequence {
thenExecute {
helper.setBlock(BlockPos(2, 3, 2), Blocks.AIR)
helper.assertItemEntityPresent(ModRegistry.Items.WIRED_MODEM.get(), BlockPos(2, 2, 2), 0.0)
helper.assertBlockIs(BlockPos(2, 2, 2)) {
it.block == ModRegistry.Blocks.CABLE.get() && it.getValue(CableBlock.MODEM) == CableModemVariant.None && it.getValue(CableBlock.CABLE)
}
}
}
/**
* Check chest peripherals are reattached with a new size.
*/
@GameTest
fun Chest_resizes_on_change(context: GameTestHelper) = context.sequence {
thenOnComputer {
callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(27)
}
thenExecute { context.placeItemAt(ItemStack(Items.CHEST), BlockPos(2, 2, 2), Direction.WEST) }
thenIdle(1)
thenOnComputer {
callRemotePeripheral("minecraft:chest_0", "size").assertArrayEquals(54)
}
}
}
@ -105,7 +187,7 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
if (!peripheral.isPresent(side)) continue
peripherals.add(side)
val hasType = peripheral.hasType(side, "modem")
val hasType = peripheral.hasType(side, "peripheral_hub")
if (hasType == null || hasType[0] != true) continue
val names = peripheral.call(context, ObjectArguments(side, "getNamesRemote")).await() ?: continue
@ -116,3 +198,22 @@ fun Full_modems_form_networks(helper: GameTestHelper) = helper.sequence {
peripherals.sort()
return peripherals
}
private suspend fun LuaTaskContext.callRemotePeripheral(name: String, method: String, vararg args: Any): Array<out Any?>? {
val peripheral = getApi<PeripheralAPI>()
if (peripheral.isPresent(name)) return peripheral.call(context, ObjectArguments(name, method, *args)).await()
for (side in ComputerSide.NAMES) {
if (!peripheral.isPresent(side)) continue
val hasType = peripheral.hasType(side, "peripheral_hub")
if (hasType == null || hasType[0] != true) continue
val isPresent = peripheral.call(context, ObjectArguments(side, "isPresentRemote", name)).await() ?: continue
if (isPresent[0] as Boolean) {
return peripheral.call(context, ObjectArguments(side, "callRemote", name, method, *args)).await()
}
}
throw IllegalArgumentException("No such peripheral $name")
}

View File

@ -38,10 +38,10 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
// And ensure its synced to the client.
thenIdle(4)
thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
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.
@ -54,10 +54,10 @@ fun Sync_state(context: GameTestHelper) = context.sequence {
// And ensure the new computer state and terminal are sent.
thenIdle(4)
thenOnClient {
val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem)
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

@ -329,8 +329,6 @@ fun Use_compostors(helper: GameTestHelper) = helper.sequence {
/**
* Checks turtles can be cleaned in cauldrons.
*
* Currently not required as turtles can no longer right-click cauldrons.
*/
@GameTest
fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence {
@ -643,7 +641,20 @@ fun Peripheral_change(helper: GameTestHelper) = helper.sequence {
}
}
// TODO: Turtle sucking from items
/**
* `turtle.suck` only pulls for the current side.
*/
@GameTest
fun Sided_suck(helper: GameTestHelper) = helper.sequence {
thenOnComputer {
turtle.suckUp(Optional.empty()).await().assertArrayEquals(true)
turtle.getItemDetail(context, Optional.empty(), Optional.empty()).await().assertArrayEquals(
mapOf("name" to "minecraft:iron_ingot", "count" to 8),
)
turtle.suckUp(Optional.empty()).await().assertArrayEquals(false, "No items to take")
}
}
/**
* Render turtles as an item.

View File

@ -19,15 +19,19 @@
import net.minecraft.gametest.framework.*
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.Container
import net.minecraft.world.InteractionHand
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EntityType
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.context.UseOnContext
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.entity.BarrelBlockEntity
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.properties.Property
import net.minecraft.world.phys.BlockHitResult
import net.minecraft.world.phys.Vec3
import org.hamcrest.Matchers
import org.hamcrest.StringDescription
@ -304,3 +308,29 @@ private fun getName(type: BlockEntityType<*>): ResourceLocation = RegistryWrappe
container.setItem(slot, item)
container.setChanged()
}
/**
* An alternative version ot [GameTestHelper.placeAt], which sets the player's held item first.
*
* This is required for compatibility with Forge, which uses the in-hand stack, rather than the stack requested.
*/
fun GameTestHelper.placeItemAt(stack: ItemStack, pos: BlockPos, direction: Direction) {
val player = makeMockPlayer()
player.setItemInHand(InteractionHand.MAIN_HAND, stack)
val absolutePos = absolutePos(pos.relative(direction))
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

@ -86,6 +86,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: "minecraft:air"},
{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: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
{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: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
{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",
"minecraft:chest{facing:north,type:single,waterlogged:false}",
"computercraft:computer_normal{facing:north,state:on}"
]
}

View File

@ -0,0 +1,139 @@
{
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:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "computer", id: "computercraft:cable"}},
{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: "modem_test.cable_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{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:cable{cable:true,down:false,east:true,modem:east_off_peripheral,north:false,south:false,up:false,waterlogged:false,west:false}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:false}"
]
}

View File

@ -0,0 +1,139 @@
{
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: "minecraft:air"},
{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: "minecraft:chest{facing:north,type:single,waterlogged:false}", nbt: {Items: [], id: "minecraft:chest"}},
{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: "computercraft:computer_normal{facing:north,state:on}", nbt: {ComputerId: 1, Label: "modem_test.chest_resizes_on_change", On: 1b, id: "computercraft:computer_normal"}},
{pos: [3, 1, 2], state: "computercraft:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralId2: 2, PeripheralId4: 0, PeripheralType2: "computer", PeripheralType4: "minecraft:chest", id: "computercraft:wired_modem_full"}},
{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",
"minecraft:chest{facing:north,type:single,waterlogged:false}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:wired_modem_full{modem:false,peripheral:true}"
]
}

View File

@ -0,0 +1,139 @@
{
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:wired_modem_full{modem:false,peripheral:true}", nbt: {PeripheralAccess: 1b, PeripheralId5: 1, PeripheralType5: "computer", id: "computercraft:wired_modem_full"}},
{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: "modem_test.full_block_modem_does_not_report_self", On: 1b, id: "computercraft:computer_normal"}},
{pos: [2, 1, 3], state: "computercraft:wired_modem_full{modem:false,peripheral:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:wired_modem_full"}},
{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:wired_modem_full{modem:false,peripheral:true}",
"computercraft:computer_normal{facing:north,state:on}",
"computercraft:wired_modem_full{modem:false,peripheral:false}"
]
}

View File

@ -28,7 +28,7 @@
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: " ", term_text_1: " ", term_text_10: " ", term_text_11: " ", term_text_12: " ", term_text_13: " ", term_text_14: " ", term_text_15: " ", term_text_16: " ", term_text_17: " ", term_text_18: " ", term_text_19: " ", term_text_2: " ", term_text_20: " ", term_text_3: " ", term_text_4: " ", term_text_5: " ", term_text_6: " ", term_text_7: " ", term_text_8: " ", term_text_9: " "}},
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "printer", id: "computercraft:cable"}},
{pos: [0, 1, 2], state: "minecraft:air"},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
@ -36,7 +36,7 @@
{pos: [1, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 1, PeripheralType: "monitor", id: "computercraft:cable"}},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "minecraft:air"},
@ -133,11 +133,11 @@
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:printer{bottom:false,facing:north,top:false}",
"computercraft:cable{cable:true,down:false,east:true,modem:north_on,north:true,south:false,up:false,waterlogged:false,west:false}",
"computercraft:cable{cable:true,down:false,east:true,modem:north_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:false}",
"computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
"computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:false}",
"computercraft:computer_advanced{facing:north,state:blinking}",
"computercraft:cable{cable:true,down:false,east:false,modem:north_off,north:true,south:false,up:false,waterlogged:false,west:true}"

View File

@ -28,7 +28,7 @@
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
{pos: [0, 1, 0], state: "computercraft:printer{bottom:false,facing:north,top:false}", nbt: {Items: [], PageTitle: "", Printing: 0b, id: "computercraft:printer", term_bgColour: 15, term_cursorBlink: 0b, term_cursorX: 0, term_cursorY: 0, term_palette: [I; 1118481, 13388876, 5744206, 8349260, 3368652, 11691749, 5020082, 10066329, 5000268, 15905484, 8375321, 14605932, 10072818, 15040472, 15905331, 15790320], term_textBgColour_0: "fffffffffffffffffffffffff", term_textBgColour_1: "fffffffffffffffffffffffff", term_textBgColour_10: "fffffffffffffffffffffffff", term_textBgColour_11: "fffffffffffffffffffffffff", term_textBgColour_12: "fffffffffffffffffffffffff", term_textBgColour_13: "fffffffffffffffffffffffff", term_textBgColour_14: "fffffffffffffffffffffffff", term_textBgColour_15: "fffffffffffffffffffffffff", term_textBgColour_16: "fffffffffffffffffffffffff", term_textBgColour_17: "fffffffffffffffffffffffff", term_textBgColour_18: "fffffffffffffffffffffffff", term_textBgColour_19: "fffffffffffffffffffffffff", term_textBgColour_2: "fffffffffffffffffffffffff", term_textBgColour_20: "fffffffffffffffffffffffff", term_textBgColour_3: "fffffffffffffffffffffffff", term_textBgColour_4: "fffffffffffffffffffffffff", term_textBgColour_5: "fffffffffffffffffffffffff", term_textBgColour_6: "fffffffffffffffffffffffff", term_textBgColour_7: "fffffffffffffffffffffffff", term_textBgColour_8: "fffffffffffffffffffffffff", term_textBgColour_9: "fffffffffffffffffffffffff", term_textColour: 0, term_textColour_0: "0000000000000000000000000", term_textColour_1: "0000000000000000000000000", term_textColour_10: "0000000000000000000000000", term_textColour_11: "0000000000000000000000000", term_textColour_12: "0000000000000000000000000", term_textColour_13: "0000000000000000000000000", term_textColour_14: "0000000000000000000000000", term_textColour_15: "0000000000000000000000000", term_textColour_16: "0000000000000000000000000", term_textColour_17: "0000000000000000000000000", term_textColour_18: "0000000000000000000000000", term_textColour_19: "0000000000000000000000000", term_textColour_2: "0000000000000000000000000", term_textColour_20: "0000000000000000000000000", term_textColour_3: "0000000000000000000000000", term_textColour_4: "0000000000000000000000000", term_textColour_5: "0000000000000000000000000", term_textColour_6: "0000000000000000000000000", term_textColour_7: "0000000000000000000000000", term_textColour_8: "0000000000000000000000000", term_textColour_9: "0000000000000000000000000", term_text_0: " ", term_text_1: " ", term_text_10: " ", term_text_11: " ", term_text_12: " ", term_text_13: " ", term_text_14: " ", term_text_15: " ", term_text_16: " ", term_text_17: " ", term_text_18: " ", term_text_19: " ", term_text_2: " ", term_text_20: " ", term_text_3: " ", term_text_4: " ", term_text_5: " ", term_text_6: " ", term_text_7: " ", term_text_8: " ", term_text_9: " "}},
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
{pos: [0, 1, 1], state: "computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "printer", id: "computercraft:cable"}},
{pos: [0, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [0, 1, 3], state: "minecraft:air"},
{pos: [0, 1, 4], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:none}", nbt: {Height: 1, Width: 1, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
@ -36,7 +36,7 @@
{pos: [1, 1, 1], state: "minecraft:air"},
{pos: [1, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [1, 1, 3], state: "computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
{pos: [1, 1, 4], state: "computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 1b, PeripheralId: 0, PeripheralType: "monitor", id: "computercraft:cable"}},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:air"},
{pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}", nbt: {PeripheralAccess: 0b, id: "computercraft:cable"}},
@ -133,12 +133,12 @@
"minecraft:polished_andesite",
"minecraft:air",
"computercraft:printer{bottom:false,facing:north,top:false}",
"computercraft:cable{cable:true,down:false,east:false,modem:north_on,north:true,south:true,up:false,waterlogged:false,west:false}",
"computercraft:cable{cable:true,down:false,east:false,modem:north_off_peripheral,north:true,south:true,up:false,waterlogged:false,west:false}",
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:true,south:false,up:false,waterlogged:false,west:false}",
"computercraft:monitor_advanced{facing:north,orientation:north,state:none}",
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:true,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:false,modem:none,north:true,south:true,up:false,waterlogged:false,west:false}",
"computercraft:cable{cable:true,down:false,east:false,modem:west_on,north:true,south:false,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:false,modem:west_off_peripheral,north:true,south:false,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:true,modem:none,north:false,south:false,up:false,waterlogged:false,west:true}",
"computercraft:cable{cable:true,down:false,east:true,modem:east_off,north:false,south:false,up:false,waterlogged:false,west:true}",
"computercraft:computer_advanced{facing:north,state:blinking}"

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:light_gray_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
{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:light_gray_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
{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:light_gray_stained_glass",
"minecraft:air",
"computercraft:cable{cable:false,down:false,east:false,modem:up_off,north:false,south:false,up:false,waterlogged:false,west:false}"
]
}

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:light_gray_stained_glass"},
{pos: [1, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 1, 4], state: "minecraft:air"},
{pos: [2, 1, 0], state: "minecraft:air"},
{pos: [2, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 2], state: "computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}", nbt: {id: "computercraft:cable"}},
{pos: [2, 1, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 1, 4], state: "minecraft:air"},
{pos: [3, 1, 0], state: "minecraft:air"},
{pos: [3, 1, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 1, 3], state: "minecraft:light_gray_stained_glass"},
{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:light_gray_stained_glass"},
{pos: [1, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [1, 2, 4], state: "minecraft:air"},
{pos: [2, 2, 0], state: "minecraft:air"},
{pos: [2, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 3], state: "minecraft:light_gray_stained_glass"},
{pos: [2, 2, 4], state: "minecraft:air"},
{pos: [3, 2, 0], state: "minecraft:air"},
{pos: [3, 2, 1], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 2], state: "minecraft:light_gray_stained_glass"},
{pos: [3, 2, 3], state: "minecraft:light_gray_stained_glass"},
{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:light_gray_stained_glass",
"minecraft:air",
"computercraft:cable{cable:true,down:false,east:false,modem:up_off,north:false,south:false,up:true,waterlogged:false,west:false}"
]
}

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

@ -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: "minecraft:air"},
{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:turtle_normal{facing:north,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.sided_suck", On: 1b, Slot: 0, id: "computercraft:turtle_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:furnace{facing:north,lit:false}", nbt: {BurnTime: 0s, CookTime: 0s, CookTimeTotal: 200s, Items: [{Count: 64b, Slot: 1b, id: "minecraft:coal"}, {Count: 8b, Slot: 2b, id: "minecraft:iron_ingot"}], RecipesUsed: {"minecraft:iron_ingot_from_smelting_raw_iron": 8}, id: "minecraft:furnace"}},
{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:turtle_normal{facing:north,waterlogged:false}",
"minecraft:furnace{facing:north,lit:false}"
]
}

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

@ -11,6 +11,7 @@
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.computer.GuardedLuaContext;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.metrics.Metrics;
@ -26,7 +27,7 @@
* @hidden
*/
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
private class PeripheralWrapper extends ComputerAccess {
private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard {
private final String side;
private final IPeripheral peripheral;
@ -35,6 +36,8 @@ private class PeripheralWrapper extends ComputerAccess {
private final Map<String, PeripheralMethod> methodMap;
private boolean attached = false;
private @Nullable GuardedLuaContext contextWrapper;
PeripheralWrapper(IPeripheral peripheral, String side) {
super(environment);
this.side = side;
@ -91,9 +94,21 @@ public MethodResult call(ILuaContext context, String methodName, IArguments argu
if (method == null) throw new LuaException("No such method " + methodName);
try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
return method.apply(peripheral, context, this, arguments);
// Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this
// should be pretty common as ILuaMachine uses a constant context.
var contextWrapper = this.contextWrapper;
if (contextWrapper == null || !contextWrapper.wraps(context)) {
contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
}
try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) {
return method.apply(peripheral, contextWrapper, this, arguments);
}
}
@Override
public boolean checkValid() {
return isAttached();
}
// IComputerAccess implementation

View File

@ -11,6 +11,7 @@
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.metrics.MetricsObserver;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.PeripheralHelpers;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@ -268,9 +269,7 @@ public void setPeripheral(ComputerSide side, @Nullable IPeripheral peripheral) {
synchronized (peripherals) {
var index = side.ordinal();
var existing = peripherals[index];
if ((existing == null && peripheral != null) ||
(existing != null && peripheral == null) ||
(existing != null && !existing.equals(peripheral))) {
if (!PeripheralHelpers.equals(existing, peripheral)) {
peripherals[index] = peripheral;
if (peripheralListener != null) peripheralListener.onPeripheralChanged(side, peripheral);
}

View File

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.computer;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaTask;
/**
* A {@link ILuaContext} which checks if context is valid when before executing
* {@linkplain #issueMainThreadTask(LuaTask) main-thread tasks}.
*/
public final class GuardedLuaContext implements ILuaContext {
private final ILuaContext original;
private final Guard guard;
public GuardedLuaContext(ILuaContext original, Guard guard) {
this.original = original;
this.guard = guard;
}
/**
* Determine if this {@link GuardedLuaContext} wraps another context.
* <p>
* This may be used to avoid constructing new guarded contexts, in a pattern something like:
*
* <pre>{@code
* var contextWrapper = this.contextWrapper;
* if(contextWrapper == null || !contextWrapper.wraps(context)) {
* contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
* }
* }</pre>
*
* @param context The original context.
* @return Whether {@code this} wraps {@code context}.
*/
public boolean wraps(ILuaContext context) {
return original == context;
}
@Override
public long issueMainThreadTask(LuaTask task) throws LuaException {
return original.issueMainThreadTask(() -> guard.checkValid() ? task.execute() : null);
}
/**
* The function which checks if the context is still valid.
*/
@FunctionalInterface
public interface Guard {
boolean checkValid();
}
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.util;
import dan200.computercraft.api.peripheral.IPeripheral;
import javax.annotation.Nullable;
/**
* Utilities for working with {@linkplain IPeripheral peripherals}.
*/
public final class PeripheralHelpers {
private PeripheralHelpers() {
}
/**
* Determine if two peripherals are equal. This is equivalent to {@link java.util.Objects#equals(Object, Object)},
* but using {@link IPeripheral#equals(IPeripheral)} instead.
*
* @param a The first peripheral.
* @param b The second peripheral.
* @return If the two peripherals are equal.
*/
public static boolean equals(@Nullable IPeripheral a, @Nullable IPeripheral b) {
return a == b || (a != null && b != null && a.equals(b));
}
}

View File

@ -1,3 +1,24 @@
# 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.
* Remove custom breaking progress of modems on Forge.
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`.
# New features in CC: Tweaked 1.109.7
* Improve performance of removing and unloading wired cables/modems.

View File

@ -1,11 +1,7 @@
New features in CC: Tweaked 1.109.7
* Improve performance of removing and unloading wired cables/modems.
New features in CC: Tweaked 1.110.1
Several bug fixes:
* Fix monitors sometimes not updating on the client when chunks are unloaded and reloaded.
* `colour.toBlit` correctly errors on out-of-bounds values.
* Round non-standard colours in `window`, like `term.native()` does.
* Fix the client monitor rendering both the current and outdated contents.
* Fix computers not turning on after they're unloaded/not-ticked for a while.
* Fix networking cables sometimes not connecting on Forge.
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]
if not file then
@ -54,7 +58,7 @@ elseif cmd == "play" then
print("Downloading...")
handle, err = http.get(file)
else
handle, err = fs.open(file, "r")
handle, err = fs.open(shell.resolve(file), "r")
end
if not handle then

View File

@ -27,6 +27,7 @@
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
@ -45,7 +46,7 @@ public static void init() {
ClientRegistry.register();
ClientRegistry.registerTurtleModellers(FabricComputerCraftAPIClient::registerTurtleUpgradeModeller);
ClientRegistry.registerItemColours(ColorProviderRegistry.ITEM::register);
ClientRegistry.registerMainThread();
ClientRegistry.registerMainThread(ItemProperties::register);
PreparableModelLoadingPlugin.register(CustomModelLoader::prepare, (state, context) -> {
ClientRegistry.registerExtraModels(context::addModels);

View File

@ -13,6 +13,8 @@
import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage;
import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant;
import net.fabricmc.fabric.api.transfer.v1.storage.SlottedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedSlottedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.CombinedStorage;
import net.fabricmc.fabric.api.transfer.v1.storage.base.SingleSlotStorage;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@ -42,6 +44,34 @@ public final class InventoryMethods extends AbstractInventoryMethods<InventoryMe
* @param storage The underlying storage
*/
public record StorageWrapper(SlottedStorage<ItemVariant> storage) {
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof StorageWrapper other)) return false;
var otherStorage = other.storage;
/*
Equality for inventory storage isn't really defined, and most of the time falls back to reference
equality.
- Vanilla inventories are exposed via InventoryStorage - the creation of this is cached, so will be
the same object.
- Double chests are combined into a CombinedSlottedStorage. We check the parts are equal.
*/
if (
storage instanceof CombinedSlottedStorage<?, ?> cs && storage.getClass() == otherStorage.getClass()
&& cs.parts.equals(((CombinedStorage<?, ?>) otherStorage).parts)
) {
return true;
}
return storage.equals(otherStorage);
}
@Override
public int hashCode() {
return storage instanceof CombinedSlottedStorage<?, ?> cs ? cs.parts.hashCode() : storage.hashCode();
}
}
@Override

View File

@ -9,12 +9,11 @@
import net.fabricmc.fabric.api.transfer.v1.storage.Storage;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageUtil;
import net.fabricmc.fabric.api.transfer.v1.storage.StorageView;
import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction;
import net.fabricmc.fabric.api.transfer.v1.transaction.TransactionContext;
import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Predicate;
@SuppressWarnings("UnstableApiUsage")
public class FabricContainerTransfer implements ContainerTransfer {
@ -34,37 +33,31 @@ public static ContainerTransfer.Slotted of(SlottedStorage<ItemVariant> storage)
@Override
public int moveTo(ContainerTransfer destination, int maxAmount) {
var predicate = new GatePredicate<ItemVariant>();
var hasItem = false;
var moved = StorageUtil.move(storage, ((FabricContainerTransfer) destination).storage, predicate, maxAmount, null);
if (moved > 0) return moved > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) moved;
var destStorage = ((FabricContainerTransfer) destination).storage;
for (var slot : storage.nonEmptyViews()) {
var resource = slot.getResource();
// Nasty hack here to check if move() actually found an item in the original inventory. Saves having to
// iterate over the source twice.
return predicate.hasItem() ? NO_SPACE : NO_ITEMS;
}
try (var transaction = Transaction.openOuter()) {
// Check how much can be extracted and inserted.
var maxExtracted = StorageUtil.simulateExtract(slot, resource, maxAmount, transaction);
if (maxExtracted == 0) continue;
/**
* A predicate which accepts the first value it sees, and then only those matching that value.
*
* @param <T> The type of the object to accept.
*/
private static final class GatePredicate<T> implements Predicate<T> {
private @Nullable T instance = null;
hasItem = true;
@Override
public boolean test(T o) {
if (instance == null) {
instance = o;
return true;
var accepted = destStorage.insert(resource, maxExtracted, transaction);
if (accepted == 0) continue;
// Extract or rollback.
if (slot.extract(resource, accepted, transaction) == accepted) {
transaction.commit();
return (int) accepted;
}
}
return instance.equals(o);
}
boolean hasItem() {
return instance != null;
}
return hasItem ? NO_SPACE : NO_ITEMS;
}
private static final class SlottedImpl extends FabricContainerTransfer implements ContainerTransfer.Slotted {

View File

@ -8,7 +8,6 @@ import net.minecraftforge.gradle.common.util.RunConfig
plugins {
id("cc-tweaked.forge")
id("cc-tweaked.gametest")
alias(libs.plugins.mixinGradle)
id("cc-tweaked.mod-publishing")
}
@ -103,12 +102,6 @@ minecraft {
}
}
mixin {
add(sourceSets.client.get(), "client-computercraft.refmap.json")
config("computercraft-client.forge.mixins.json")
}
configurations {
minecraftLibrary { extendsFrom(minecraftEmbed.get()) }
@ -121,9 +114,6 @@ configurations {
}
dependencies {
annotationProcessor("org.spongepowered:mixin:0.8.5-SQUID:processor")
clientAnnotationProcessor("org.spongepowered:mixin:0.8.5-SQUID:processor")
compileOnly(libs.jetbrainsAnnotations)
annotationProcessorEverywhere(libs.autoService)
@ -246,12 +236,6 @@ modPublishing {
output.set(tasks.jarJar)
}
// Make sure configureReobfTaskForReobfJarJar runs after compilation
// see - https://github.com/SpongePowered/MixinGradle/pull/51
tasks.configureEach {
if (name == "configureReobfTaskForReobfJarJar") mustRunAfter(tasks.jarJar)
}
// Don't publish the slim jar
for (cfg in listOf(configurations.apiElements, configurations.runtimeElements)) {
cfg.configure { artifacts.removeIf { it.classifier == "slim" } }

View File

@ -8,6 +8,7 @@
import dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent;
import dan200.computercraft.client.model.turtle.TurtleModelLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
@ -82,6 +83,6 @@ public static void registerReloadListeners(RegisterClientReloadListenersEvent ev
@SubscribeEvent
public static void setupClient(FMLClientSetupEvent event) {
ClientRegistry.register();
event.enqueueWork(ClientRegistry::registerMainThread);
event.enqueueWork(() -> ClientRegistry.registerMainThread(ItemProperties::register));
}
}

View File

@ -1,66 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.ClientHooks;
import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
import net.minecraft.client.renderer.block.ModelBlockRenderer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.BlockAndTintGetter;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.client.model.data.ModelData;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Provides custom block breaking progress for modems, so it only applies to the current part.
*
* @see BlockRenderDispatcher#renderBreakingTexture(BlockState, BlockPos, BlockAndTintGetter, PoseStack, VertexConsumer, ModelData)
*/
@Mixin(BlockRenderDispatcher.class)
public class BlockRenderDispatcherMixin {
@Shadow
@Final
private RandomSource random;
@Shadow
@Final
private BlockModelShaper blockModelShaper;
@Shadow
@Final
private ModelBlockRenderer modelRenderer;
@Inject(
method = "name=/^renderBreakingTexture/ desc=/ModelData;\\)V$/",
at = @At("HEAD"),
cancellable = true,
require = 0 // This isn't critical functionality, so don't worry if we can't apply it.
)
public void renderBlockDamage(
BlockState state, BlockPos pos, BlockAndTintGetter world, PoseStack pose, VertexConsumer buffers, ModelData modelData,
CallbackInfo info
) {
var newState = ClientHooks.getBlockBreakingState(state, pos);
if (newState != null) {
info.cancel();
var model = blockModelShaper.getBlockModel(newState);
modelRenderer.tesselateBlock(
world, model, newState, pos, pose, buffers, true, random, newState.getSeed(pos),
OverlayTexture.NO_OVERLAY, modelData, null
);
}
}
}

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.client;
import dan200.computercraft.shared.command.CommandComputerCraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraftforge.client.ClientCommandHandler;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Allows triggering ComputerCraft's client commands from chat components events.
*/
@Mixin(ClientPacketListener.class)
class ClientPacketListenerMixin {
@Inject(method = "sendUnsignedCommand", at = @At("HEAD"), cancellable = true)
void commandUnsigned(String command, CallbackInfoReturnable<Boolean> ci) {
if (command.startsWith(CommandComputerCraft.CLIENT_OPEN_FOLDER) && ClientCommandHandler.runCommand(command)) {
ci.setReturnValue(true);
}
}
}

View File

@ -1,14 +0,0 @@
{
"required": true,
"package": "dan200.computercraft.mixin.client",
"minVersion": "0.8",
"compatibilityLevel": "JAVA_17",
"injectors": {
"defaultRequire": 1
},
"client": [
"BlockRenderDispatcherMixin",
"ClientPacketListenerMixin"
],
"refmap": "client-computercraft.refmap.json"
}

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.RegistryWrappers;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
@ -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,11 +92,14 @@ 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 && blockEntity.isRemoved()) return null;
if (object instanceof ICapabilityProvider provider) {
var cap = provider.getCapability(ForgeCapabilities.FLUID_HANDLER);
var cap = CapabilityUtil.getCapability(provider, ForgeCapabilities.FLUID_HANDLER, direction);
if (cap.isPresent()) return cap.orElseThrow(NullPointerException::new);
}

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.world.Container;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
@ -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,11 +110,14 @@ 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 && blockEntity.isRemoved()) return null;
if (object instanceof ICapabilityProvider provider) {
var cap = provider.getCapability(ForgeCapabilities.ITEM_HANDLER);
var cap = CapabilityUtil.getCapability(provider, ForgeCapabilities.ITEM_HANDLER, direction);
if (cap.isPresent()) return cap.orElseThrow(NullPointerException::new);
}

View File

@ -345,6 +345,11 @@ public InteractionResult useOn(ServerPlayer player, ItemStack stack, BlockHitRes
return event.getUseItem() == Event.Result.DENY ? InteractionResult.PASS : stack.useOn(context);
}
@Override
public boolean canClickRunClientCommand() {
return false;
}
private record RegistryWrapperImpl<T>(
ResourceLocation name, ForgeRegistry<T> registry
) implements RegistryWrappers.RegistryWrapper<T> {

View File

@ -50,8 +50,8 @@ public static <T> T unwrap(LazyOptional<T> p, InvalidateCallback invalidate) {
* @param <T> The type of the underlying capability.
* @return The extracted capability, if present.
*/
public static <T> LazyOptional<T> getCapability(ICapabilityProvider provider, Capability<T> capability, Direction side) {
public static <T> LazyOptional<T> getCapability(ICapabilityProvider provider, Capability<T> capability, @Nullable Direction side) {
var cap = provider.getCapability(capability);
return cap.isPresent() ? cap : provider.getCapability(capability, side);
return !cap.isPresent() && side != null ? provider.getCapability(capability, side) : cap;
}
}

View File

@ -105,7 +105,6 @@ private fun fromAnnotationStream(annotations: Stream<out AnnotationMirror>) =
companion object {
private val notClientPackages = listOf(
// Ugly! But we do what we must.
"net.fabricmc.fabric.api.client.itemgroup",
"dan200.computercraft.shared.network.client",
)

View File

@ -78,11 +78,11 @@ public class JavascriptConv {
* @return The wrapped array.
*/
public static byte[] asByteArray(ArrayBuffer view) {
return asByteArray(Int8Array.create(view));
return asByteArray(new Int8Array(view));
}
public static Int8Array toArray(ByteBuffer buffer) {
var array = Int8Array.create(buffer.remaining());
var array = new Int8Array(buffer.remaining());
for (var i = 0; i < array.getLength(); i++) array.set(i, buffer.get(i));
return array;
}

View File

@ -53,11 +53,13 @@ public void tick() {
}
@LuaFunction
@SuppressWarnings("DoNotCallSuggester")
public final boolean playNote(String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA) throws LuaException {
throw new LuaException("Cannot play notes outside of Minecraft");
}
@LuaFunction
@SuppressWarnings("DoNotCallSuggester")
public final boolean playSound(String name, Optional<Double> volumeA, Optional<Double> pitchA) throws LuaException {
throw new LuaException("Cannot play sounds outside of Minecraft");
}
@ -70,7 +72,7 @@ public final boolean playAudio(LuaTable<?, ?> audio, Optional<Double> volume) th
if (length <= 0) throw new LuaException("Cannot play empty audio");
if (length > 128 * 1024) throw new LuaException("Audio data is too large");
if (audioContext == null) audioContext = AudioContext.create();
if (audioContext == null) audioContext = new AudioContext();
if (state == null || !state.isPlaying()) state = new AudioState(audioContext);
return state.pushBuffer(audio, length, volume);

View File

@ -92,7 +92,7 @@ public void request(URI uri, HttpMethod method) {
if (isClosed()) return;
try {
var request = XMLHttpRequest.create();
var request = new XMLHttpRequest();
request.setOnReadyStateChange(() -> onResponseStateChange(request));
request.setResponseType("arraybuffer");
var address = uri.toASCIIString();

Some files were not shown because too many files have changed in this diff Show More