mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-11-05 09:36:19 +00:00
Atomic update of disk drive item stacks
Disk drives have had a long-standing issue with mutating their contents on the computer thread, potentially leading to all sorts of odd bugs. We tried to fix this by moving setDiskLabel and the mounting code to run on the main thread. Unfortunately, this means there is a slight delay to mounts being attached, breaking disk startup. This commit implements an alternative solution - we now do mounting on the computer thread again. If the disk's stack is modified, we update it in the peripheral-facing item, but not the actual inventory. The next time the disk drive is ticked, we then sync the two items. This does mean that there is a fraction of a tick where the two will be out-of-sync. This isn't ideal - it would potentially be possible to cycle through disk ids - but I don't really think that's avoidable without significantly complicating the IMedia API. Fixes #1649, fixes #1686.
This commit is contained in:
parent
bc03090ca4
commit
f695f22d8a
@ -7,6 +7,7 @@ package dan200.computercraft.shared.peripheral.diskdrive;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.media.IMedia;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.shared.common.AbstractContainerBlockEntity;
|
||||
@ -32,6 +33,28 @@ import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* The underlying block entity for disk drives. This holds the main logic for the {@linkplain DiskDrivePeripheral disk
|
||||
* drive peripheral}, such as handling mounts and {@linkplain DiskDrivePeripheral#playAudio() playing audio}.
|
||||
* <p>
|
||||
* Most disk drive peripheral methods execute on the computer thread (largely due to historic reasons). This causes some
|
||||
* problems, as the disk item could be read by both the computer thread (via peripheral calls) and main thread (via
|
||||
* Minecraft inventory interaction).
|
||||
* <p>
|
||||
* To solve this, we use an immutable {@link MediaStack}, which holds an immutable version of the current
|
||||
* {@link ItemStack} (and its corresponding {@link IMedia}). When the {@linkplain #setChanged() inventory is changed},
|
||||
* we {@linkplain #updateMedia() update the media stack} and recompute mounts.
|
||||
* <p>
|
||||
* This is somewhat complicated by {@link #attach(IComputerAccess)}. As that can happen on the computer thread and
|
||||
* may mutate the stack (when {@link IMedia#createDataMount(ItemStack, ServerLevel)} assigns an ID for the first time),
|
||||
* we need a way to safely update the inventory. To solve this, all internal non-inventory interactions with disk drives
|
||||
* treat the media stack as the "primary" stack. This allows us to atomically update it, and then sync it back to the
|
||||
* main inventory ({@link #updateMediaStack(ItemStack, boolean)}) either directly ({@link #updateDiskFromMedia()}) or
|
||||
* on the next block tick ({@link #stackDirty}). This does mean there's a one-tick delay where the inventory may be
|
||||
* out-of-date, but that should happen very rarely.
|
||||
*
|
||||
* @see DiskDrivePeripheral
|
||||
*/
|
||||
public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
private static final String NBT_ITEM = "Item";
|
||||
|
||||
@ -42,11 +65,13 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
|
||||
private final DiskDrivePeripheral peripheral = new DiskDrivePeripheral(this);
|
||||
|
||||
private final @GuardedBy("this") Map<IComputerAccess, MountInfo> computers = new HashMap<>();
|
||||
|
||||
private final NonNullList<ItemStack> inventory = NonNullList.withSize(1, ItemStack.EMPTY);
|
||||
|
||||
@GuardedBy("this")
|
||||
private final Map<IComputerAccess, MountInfo> computers = new HashMap<>();
|
||||
@GuardedBy("this")
|
||||
private MediaStack media = MediaStack.EMPTY;
|
||||
@GuardedBy("this")
|
||||
private @Nullable Mount mount;
|
||||
|
||||
private boolean recordPlaying = false;
|
||||
@ -54,7 +79,12 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
// then read them when ticking.
|
||||
private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null);
|
||||
private final AtomicBoolean ejectQueued = new AtomicBoolean(false);
|
||||
private final AtomicBoolean mountQueued = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Whether the stack in {@link #media} has been modified on the computer thread, and needs to be written back to the
|
||||
* inventory on the main thread.
|
||||
*/
|
||||
private final AtomicBoolean stackDirty = new AtomicBoolean(false);
|
||||
|
||||
public DiskDriveBlockEntity(BlockEntityType<DiskDriveBlockEntity> type, BlockPos pos, BlockState state) {
|
||||
super(type, pos, state);
|
||||
@ -66,7 +96,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
|
||||
@Override
|
||||
public void clearRemoved() {
|
||||
updateItem();
|
||||
updateMedia();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,12 +123,14 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
}
|
||||
|
||||
void serverTick() {
|
||||
if (stackDirty.getAndSet(false)) updateDiskFromMedia();
|
||||
if (ejectQueued.getAndSet(false)) ejectContents();
|
||||
|
||||
var recordQueued = this.recordQueued.getAndSet(null);
|
||||
if (recordQueued != null) {
|
||||
switch (recordQueued) {
|
||||
case PLAY -> {
|
||||
var media = getMedia();
|
||||
var record = media.getAudio();
|
||||
if (record != null) {
|
||||
recordPlaying = true;
|
||||
@ -112,12 +144,6 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mountQueued.get()) {
|
||||
synchronized (this) {
|
||||
mountAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -127,38 +153,46 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
|
||||
@Override
|
||||
public void setChanged() {
|
||||
if (level != null && !level.isClientSide) updateItem();
|
||||
if (level != null && !level.isClientSide) updateMedia();
|
||||
super.setChanged();
|
||||
}
|
||||
|
||||
private void updateItem() {
|
||||
var newDisk = getDiskStack();
|
||||
if (ItemStack.isSameItemSameTags(newDisk, media.stack)) return;
|
||||
/**
|
||||
* Called on the server after the item has changed. This unmounts the old media and mounts the new one.
|
||||
*/
|
||||
private synchronized void updateMedia() {
|
||||
var newStack = getDiskStack();
|
||||
if (ItemStack.isSameItemSameTags(newStack, media.stack())) return;
|
||||
|
||||
var media = MediaStack.of(newDisk);
|
||||
var newMedia = MediaStack.of(newStack);
|
||||
|
||||
if (newDisk.isEmpty()) {
|
||||
if (newStack.isEmpty()) {
|
||||
updateBlockState(DiskDriveState.EMPTY);
|
||||
} else {
|
||||
updateBlockState(media.media != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
|
||||
updateBlockState(newMedia.media() != null ? DiskDriveState.FULL : DiskDriveState.INVALID);
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
// Unmount old disk
|
||||
if (!this.media.stack.isEmpty()) {
|
||||
for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
|
||||
// Unmount old disk
|
||||
if (!media.stack().isEmpty()) {
|
||||
for (var computer : computers.entrySet()) unmountDisk(computer.getKey(), computer.getValue());
|
||||
}
|
||||
|
||||
// Stop music
|
||||
if (recordPlaying) {
|
||||
stopRecord();
|
||||
recordPlaying = false;
|
||||
}
|
||||
|
||||
// Use our new media, and (if needed) mount the new disk.
|
||||
mount = null;
|
||||
media = newMedia;
|
||||
stackDirty.set(false);
|
||||
|
||||
if (!newStack.isEmpty() && !computers.isEmpty()) {
|
||||
var mount = getOrCreateMount(true);
|
||||
for (var entry : computers.entrySet()) {
|
||||
mountDisk(entry.getKey(), entry.getValue(), mount);
|
||||
}
|
||||
|
||||
// Stop music
|
||||
if (recordPlaying) {
|
||||
stopRecord();
|
||||
recordPlaying = false;
|
||||
}
|
||||
|
||||
mount = null;
|
||||
this.media = media;
|
||||
|
||||
mountAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +200,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
return getItem(0);
|
||||
}
|
||||
|
||||
MediaStack getMedia() {
|
||||
synchronized MediaStack getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
@ -181,16 +215,31 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current disk stack, assuming the underlying item does not change. Unlike
|
||||
* {@link #setDiskStack(ItemStack)} this will not change any mounts.
|
||||
*
|
||||
* @param stack The new disk stack.
|
||||
* Update the inventory's disk stack from the media stack. Unlike {@link #setDiskStack(ItemStack)} this will not
|
||||
* change any mounts.
|
||||
*/
|
||||
void updateDiskStack(ItemStack stack) {
|
||||
setItem(0, stack);
|
||||
if (!ItemStack.isSameItemSameTags(stack, media.stack)) {
|
||||
media = MediaStack.of(stack);
|
||||
super.setChanged();
|
||||
private synchronized void updateDiskFromMedia() {
|
||||
// Write back the item to the main inventory, and then mark it as dirty.
|
||||
setItem(0, media.stack().copy());
|
||||
super.setChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically update {@link #media}'s stack, then sync it back to the main inventory.
|
||||
*
|
||||
* @param stack The original stack.
|
||||
* @param immediate Whether to do this immediately (when called from the main thread) or asynchronously (when called
|
||||
* from the computer thread).
|
||||
*/
|
||||
@GuardedBy("this")
|
||||
private void updateMediaStack(ItemStack stack, boolean immediate) {
|
||||
if (ItemStack.isSameItemSameTags(media.stack(), stack)) return;
|
||||
media = new MediaStack(stack, media.media());
|
||||
|
||||
if (immediate) {
|
||||
updateDiskFromMedia();
|
||||
} else {
|
||||
stackDirty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,7 +261,9 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
synchronized (this) {
|
||||
var info = new MountInfo();
|
||||
computers.put(computer, info);
|
||||
mountQueued.set(true);
|
||||
if (!media.stack().isEmpty()) {
|
||||
mountDisk(computer, info, getOrCreateMount(level instanceof ServerLevel l && l.getServer().isSameThread()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,53 +285,50 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
ejectQueued.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add our mount to all computers.
|
||||
*/
|
||||
@GuardedBy("this")
|
||||
private void mountAll() {
|
||||
doMountAll();
|
||||
mountQueued.set(false);
|
||||
synchronized MountResult setDiskLabel(@Nullable String label) {
|
||||
if (media.media() == null) return MountResult.NO_MEDIA;
|
||||
|
||||
// Set the label, and write it back to the media stack.
|
||||
var stack = media.stack().copy();
|
||||
if (!media.media().setLabel(stack, label)) return MountResult.NOT_ALLOWED;
|
||||
updateMediaStack(stack, true);
|
||||
|
||||
return MountResult.CHANGED;
|
||||
}
|
||||
|
||||
/**
|
||||
* The worker for {@link #mountAll()}. This is responsible for creating the mount and placing it on all computers.
|
||||
*/
|
||||
@GuardedBy("this")
|
||||
private void doMountAll() {
|
||||
if (computers.isEmpty() || media.media == null) return;
|
||||
private @Nullable Mount getOrCreateMount(boolean immediate) {
|
||||
if (media.media() == null) return null;
|
||||
if (mount != null) return mount;
|
||||
|
||||
if (mount == null) {
|
||||
var stack = getDiskStack();
|
||||
mount = media.media.createDataMount(stack, (ServerLevel) level);
|
||||
setDiskStack(stack);
|
||||
}
|
||||
// Set the id (if needed) and write it back to the media stack.
|
||||
var stack = media.stack().copy();
|
||||
mount = media.media().createDataMount(stack, (ServerLevel) level);
|
||||
updateMediaStack(stack, immediate);
|
||||
|
||||
if (mount == null) return;
|
||||
return mount;
|
||||
}
|
||||
|
||||
for (var entry : computers.entrySet()) {
|
||||
var computer = entry.getKey();
|
||||
var info = entry.getValue();
|
||||
if (info.mountPath != null) continue;
|
||||
|
||||
if (mount instanceof WritableMount writable) {
|
||||
// Try mounting at the lowest numbered "disk" name we can
|
||||
var n = 1;
|
||||
while (info.mountPath == null) {
|
||||
info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
|
||||
n++;
|
||||
}
|
||||
} else {
|
||||
// Try mounting at the lowest numbered "disk" name we can
|
||||
var n = 1;
|
||||
while (info.mountPath == null) {
|
||||
info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
|
||||
n++;
|
||||
}
|
||||
private static void mountDisk(IComputerAccess computer, MountInfo info, @Nullable Mount mount) {
|
||||
if (mount instanceof WritableMount writable) {
|
||||
// Try mounting at the lowest numbered "disk" name we can
|
||||
var n = 1;
|
||||
while (info.mountPath == null) {
|
||||
info.mountPath = computer.mountWritable(n == 1 ? "disk" : "disk" + n, writable);
|
||||
n++;
|
||||
}
|
||||
|
||||
computer.queueEvent("disk", computer.getAttachmentName());
|
||||
} else if (mount != null) {
|
||||
// Try mounting at the lowest numbered "disk" name we can
|
||||
var n = 1;
|
||||
while (info.mountPath == null) {
|
||||
info.mountPath = computer.mount(n == 1 ? "disk" : "disk" + n, mount);
|
||||
n++;
|
||||
}
|
||||
} else {
|
||||
assert info.mountPath == null : "Mount path should be null";
|
||||
}
|
||||
|
||||
computer.queueEvent("disk", computer.getAttachmentName());
|
||||
}
|
||||
|
||||
private static void unmountDisk(IComputerAccess computer, MountInfo info) {
|
||||
@ -327,4 +375,10 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity {
|
||||
PLAY,
|
||||
STOP,
|
||||
}
|
||||
|
||||
enum MountResult {
|
||||
NO_MEDIA,
|
||||
NOT_ALLOWED,
|
||||
CHANGED,
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ public class DiskDrivePeripheral implements IPeripheral {
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean isDiskPresent() {
|
||||
return !diskDrive.getMedia().stack.isEmpty();
|
||||
return !diskDrive.getMedia().stack().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,7 +64,7 @@ public class DiskDrivePeripheral implements IPeripheral {
|
||||
@LuaFunction
|
||||
public final Object[] getDiskLabel() {
|
||||
var media = diskDrive.getMedia();
|
||||
return media.media == null ? null : new Object[]{ media.media.getLabel(media.stack) };
|
||||
return media.media() == null ? null : new Object[]{ media.media().getLabel(media.stack()) };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,15 +80,11 @@ public class DiskDrivePeripheral implements IPeripheral {
|
||||
*/
|
||||
@LuaFunction(mainThread = true)
|
||||
public final void setDiskLabel(Optional<String> label) throws LuaException {
|
||||
var media = diskDrive.getMedia();
|
||||
if (media.media == null) return;
|
||||
|
||||
// We're on the main thread so the stack and media should be in sync.
|
||||
var stack = diskDrive.getDiskStack();
|
||||
if (!media.media.setLabel(stack, label.map(StringUtil::normaliseLabel).orElse(null))) {
|
||||
throw new LuaException("Disk label cannot be changed");
|
||||
switch (diskDrive.setDiskLabel(label.map(StringUtil::normaliseLabel).orElse(null))) {
|
||||
case NOT_ALLOWED -> throw new LuaException("Disk label cannot be changed");
|
||||
case CHANGED, NO_MEDIA -> {
|
||||
}
|
||||
}
|
||||
diskDrive.updateDiskStack(stack);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,7 +168,7 @@ public class DiskDrivePeripheral implements IPeripheral {
|
||||
@Nullable
|
||||
@LuaFunction
|
||||
public final Object[] getDiskID() {
|
||||
var disk = diskDrive.getMedia().stack;
|
||||
var disk = diskDrive.getMedia().stack();
|
||||
return disk.getItem() instanceof DiskItem ? new Object[]{ DiskItem.getDiskID(disk) } : null;
|
||||
}
|
||||
|
||||
|
@ -13,19 +13,14 @@ import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An immutable snapshot of the current disk. This allows us to read the stack in a thread-safe manner.
|
||||
*
|
||||
* @param stack An immutable {@link ItemStack}.
|
||||
* @param media The associated {@link IMedia} instance for this stack.
|
||||
*/
|
||||
final class MediaStack {
|
||||
record MediaStack(ItemStack stack, @Nullable IMedia media) {
|
||||
static final MediaStack EMPTY = new MediaStack(ItemStack.EMPTY, null);
|
||||
|
||||
final ItemStack stack;
|
||||
final @Nullable IMedia media;
|
||||
|
||||
private MediaStack(ItemStack stack, @Nullable IMedia media) {
|
||||
this.stack = stack;
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public static MediaStack of(ItemStack stack) {
|
||||
static MediaStack of(ItemStack stack) {
|
||||
if (stack.isEmpty()) return EMPTY;
|
||||
|
||||
var freshStack = stack.copy();
|
||||
|
@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.core.apis.FSAPI
|
||||
import dan200.computercraft.core.util.Colour
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.media.items.DiskItem
|
||||
@ -19,6 +20,9 @@ import net.minecraft.network.chat.Component
|
||||
import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.block.RedStoneWireBlock
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.array
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
class Disk_Drive_Test {
|
||||
@ -45,14 +49,48 @@ class Disk_Drive_Test {
|
||||
thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A mount is initially attached, and then removed when the disk is ejected.
|
||||
*/
|
||||
@GameTest
|
||||
fun Queues_event(helper: GameTestHelper) = helper.sequence {
|
||||
val pos = BlockPos(1, 2, 2)
|
||||
|
||||
var started = false
|
||||
var disk = false
|
||||
var ejected = false
|
||||
thenStartComputer {
|
||||
// thenOnComputer discards events, so instead we need to track our state transitions.
|
||||
started = true
|
||||
|
||||
val diskEvent = pullEvent("disk")
|
||||
assertThat(diskEvent, array(equalTo("disk"), equalTo("right")))
|
||||
|
||||
disk = true
|
||||
|
||||
val ejectEvent = pullEvent("disk_eject")
|
||||
assertThat(ejectEvent, array(equalTo("disk_eject"), equalTo("right")))
|
||||
|
||||
ejected = true
|
||||
}
|
||||
|
||||
thenWaitUntil { helper.assertTrue(started, "Computer not started") }
|
||||
thenExecute { helper.setContainerItem(pos, 0, ItemStack(Items.DIRT)) }
|
||||
thenWaitUntil { helper.assertTrue(disk, "disk not inserted") }
|
||||
thenExecute { helper.setContainerItem(pos, 0, ItemStack.EMPTY) }
|
||||
thenWaitUntil { helper.assertTrue(ejected, "disk not ejected") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A mount is initially attached, and then removed when the disk is ejected.
|
||||
*/
|
||||
@GameTest
|
||||
fun Adds_removes_mount(helper: GameTestHelper) = helper.sequence {
|
||||
thenOnComputer { } // Wait for the computer to start up
|
||||
thenIdle(2) // Let the disk drive tick once to create the mount
|
||||
thenOnComputer { // Then actually assert things!
|
||||
thenExecute {
|
||||
helper.setContainerItem(BlockPos(1, 2, 2), 0, DiskItem.createFromIDAndColour(1, null, Colour.BLACK.hex))
|
||||
}
|
||||
thenOnComputer {
|
||||
getApi<FSAPI>().getDrive("disk").assertArrayEquals("right")
|
||||
callPeripheral("right", "ejectDisk")
|
||||
}
|
||||
|
@ -168,6 +168,16 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a [Container] at a given position.
|
||||
*/
|
||||
fun GameTestHelper.getContainerAt(pos: BlockPos): Container =
|
||||
when (val container = getBlockEntity(pos)) {
|
||||
is Container -> container
|
||||
null -> failVerbose("Expected a container at $pos, found nothing", pos)
|
||||
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a container contains exactly these items and no more.
|
||||
*
|
||||
@ -176,10 +186,7 @@ fun <T : Comparable<T>> GameTestHelper.assertBlockHas(pos: BlockPos, property: P
|
||||
* first `n` slots - the remaining are required to be empty.
|
||||
*/
|
||||
fun GameTestHelper.assertContainerExactly(pos: BlockPos, items: List<ItemStack>) =
|
||||
when (val container = getBlockEntity(pos) ?: failVerbose("Expected a container at $pos, found nothing", pos)) {
|
||||
is Container -> assertContainerExactlyImpl(pos, container, items)
|
||||
else -> failVerbose("Expected a container at $pos, found ${getName(container.type)}", pos)
|
||||
}
|
||||
assertContainerExactlyImpl(pos, getContainerAt(pos), items)
|
||||
|
||||
/**
|
||||
* Assert an container contains exactly these items and no more.
|
||||
@ -287,3 +294,13 @@ fun GameTestHelper.setBlock(pos: BlockPos, state: BlockInput) = state.place(leve
|
||||
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
|
||||
setBlock(pos, modify(getBlockState(pos)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update items in the container at [pos], setting the item in the specified [slot] to [item], and then marking it
|
||||
* changed.
|
||||
*/
|
||||
fun GameTestHelper.setContainerItem(pos: BlockPos, slot: Int, item: ItemStack) {
|
||||
val container = getContainerAt(pos)
|
||||
container.setItem(slot, item)
|
||||
container.setChanged()
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
{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:disk_drive{facing:north,state:full}", nbt: {Item: {Count: 1b, id: "computercraft:disk", tag: {Color: 1118481, DiskId: 0}}, id: "computercraft:disk_drive"}},
|
||||
{pos: [1, 1, 2], state: "computercraft:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
|
138
projects/common/src/testMod/resources/data/cctest/structures/disk_drive_test.queues_event.snbt
generated
Normal file
138
projects/common/src/testMod/resources/data/cctest/structures/disk_drive_test.queues_event.snbt
generated
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
DataVersion: 2975,
|
||||
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:disk_drive{facing:north,state:full}", nbt: {id: "computercraft:disk_drive"}},
|
||||
{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_advanced{facing:north,state:blinking}", nbt: {ComputerId: 1, Label: "disk_drive_test.queues_event", On: 1b, id: "computercraft:computer_advanced"}},
|
||||
{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:disk_drive{facing:north,state:full}",
|
||||
"computercraft:computer_advanced{facing:north,state:blinking}"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user