package li.cil.oc2.common.entity; import li.cil.oc2.api.bus.DeviceBusElement; import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.DeviceTypes; import li.cil.oc2.api.bus.device.object.Callback; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.object.Parameter; import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery; import li.cil.oc2.api.capabilities.TerminalUserProvider; import li.cil.oc2.common.Config; import li.cil.oc2.common.bus.AbstractDeviceBusElement; import li.cil.oc2.common.bus.CommonDeviceBusController; import li.cil.oc2.common.bus.device.util.Devices; import li.cil.oc2.common.capabilities.Capabilities; import li.cil.oc2.common.container.DeviceItemStackHandler; import li.cil.oc2.common.container.FixedSizeItemStackHandler; import li.cil.oc2.common.container.RobotInventoryContainer; import li.cil.oc2.common.container.RobotTerminalContainer; import li.cil.oc2.common.energy.FixedEnergyStorage; import li.cil.oc2.common.entity.robot.*; import li.cil.oc2.common.integration.Wrenches; import li.cil.oc2.common.item.Items; import li.cil.oc2.common.network.Network; import li.cil.oc2.common.network.message.*; import li.cil.oc2.common.serialization.NBTSerialization; import li.cil.oc2.common.util.LevelUtils; import li.cil.oc2.common.util.NBTTagIds; import li.cil.oc2.common.util.NBTUtils; import li.cil.oc2.common.util.TerminalUtils; import li.cil.oc2.common.vm.*; import net.minecraft.core.BlockPos; import net.minecraft.core.Cursor3D; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.Packet; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.loot.LootContext; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.items.ItemStackHandler; import net.minecraftforge.network.NetworkHooks; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.nio.ByteBuffer; import java.util.*; import java.util.function.Consumer; import static java.util.Collections.singleton; import static li.cil.oc2.common.Constants.*; public final class Robot extends Entity implements li.cil.oc2.api.capabilities.Robot, TerminalUserProvider { public static final EntityDataAccessor TARGET_POSITION = SynchedEntityData.defineId(Robot.class, EntityDataSerializers.BLOCK_POS); public static final EntityDataAccessor TARGET_DIRECTION = SynchedEntityData.defineId(Robot.class, EntityDataSerializers.DIRECTION); public static final EntityDataAccessor SELECTED_SLOT = SynchedEntityData.defineId(Robot.class, EntityDataSerializers.BYTE); private static final String TERMINAL_TAG_NAME = "terminal"; private static final String STATE_TAG_NAME = "state"; private static final String BUS_ELEMENT_TAG_NAME = "bus_element"; private static final String DEVICES_TAG_NAME = "devices"; private static final String COMMAND_PROCESSOR_TAG_NAME = "commands"; private static final String INVENTORY_TAG_NAME = "inventory"; private static final String SELECTED_SLOT_TAG_NAME = "selected_slot"; private static final int MAX_QUEUED_ACTIONS = 16; private static final int MAX_QUEUED_RESULTS = 16; private static final int MEMORY_SLOTS = 4; private static final int HARD_DRIVE_SLOTS = 2; private static final int FLASH_MEMORY_SLOTS = 1; private static final int MODULE_SLOTS = 4; private static final int INVENTORY_SIZE = 12; /////////////////////////////////////////////////////////////////// private final Consumer chunkUnloadListener = this::handleChunkUnload; private final Consumer worldUnloadListener = this::handleWorldUnload; private final BlockPos.MutableBlockPos mutablePosition = new BlockPos.MutableBlockPos(); private final AnimationState animationState = new AnimationState(); private final RobotActionProcessor actionProcessor = new RobotActionProcessor(); private final Terminal terminal = new Terminal(); private final RobotVirtualMachine virtualMachine; private final RobotBusElement busElement = new RobotBusElement(); private final RobotItemStackHandlers deviceItems = new RobotItemStackHandlers(); private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.robotEnergyStorage); private final ItemStackHandler inventory = new FixedSizeItemStackHandler(INVENTORY_SIZE); private final Set terminalUsers = Collections.newSetFromMap(new WeakHashMap<>()); private long lastPistonMovement; /////////////////////////////////////////////////////////////////// public Robot(final EntityType type, final Level world) { super(type, world); this.blocksBuilding = true; setNoGravity(true); final CommonDeviceBusController busController = new CommonDeviceBusController(busElement, Config.robotEnergyPerTick); virtualMachine = new RobotVirtualMachine(busController); virtualMachine.state.builtinDevices.rtcMinecraft.setLevel(world); } /////////////////////////////////////////////////////////////////// @OnlyIn(Dist.CLIENT) public AnimationState getAnimationState() { return animationState; } public Terminal getTerminal() { return terminal; } public VirtualMachine getVirtualMachine() { return virtualMachine; } public VMItemStackHandlers getItemStackHandlers() { return deviceItems; } @Override public ItemStackHandler getInventory() { return inventory; } @Override public int getSelectedSlot() { return getEntityData().get(SELECTED_SLOT); } @Override public void setSelectedSlot(final int value) { getEntityData().set(SELECTED_SLOT, (byte) Mth.clamp(value, 0, INVENTORY_SIZE - 1)); } @Nonnull @Override public LazyOptional getCapability(final Capability capability, @Nullable final Direction side) { if (capability == Capabilities.ITEM_HANDLER) { return LazyOptional.of(() -> inventory).cast(); } if (capability == Capabilities.ENERGY_STORAGE && Config.robotsUseEnergy()) { return LazyOptional.of(() -> energy).cast(); } if (capability == Capabilities.ROBOT) { return LazyOptional.of(() -> this).cast(); } final LazyOptional optional = super.getCapability(capability, side); if (optional.isPresent()) { return optional; } for (final Device device : virtualMachine.busController.getDevices()) { if (device instanceof final ICapabilityProvider capabilityProvider) { final LazyOptional value = capabilityProvider.getCapability(capability, side); if (value.isPresent()) { return value; } } } return LazyOptional.empty(); } public long getLastPistonMovement() { return lastPistonMovement; } public void start() { if (!level.isClientSide()) { virtualMachine.start(); } } public void stop() { if (!level.isClientSide()) { virtualMachine.stop(); } } public void openTerminalScreen(final ServerPlayer player) { RobotTerminalContainer.createServer(this, energy, virtualMachine.busController, player); } public void openInventoryScreen(final ServerPlayer player) { RobotInventoryContainer.createServer(this, energy, virtualMachine.busController, player); } public void addTerminalUser(final Player player) { terminalUsers.add(player); } public void removeTerminalUser(final Player player) { terminalUsers.remove(player); } @Override public Iterable getTerminalUsers() { return terminalUsers; } public void dropSelf() { if (!isAlive()) { return; } final ItemStack stack = new ItemStack(Items.ROBOT.get()); exportToItemStack(stack); spawnAtLocation(stack); discard(); LevelUtils.playSound(level, blockPosition(), SoundType.METAL, SoundType::getBreakSound); } @Override public void tick() { final boolean isClient = level.isClientSide(); if (firstTick) { if (isClient) { requestInitialState(); } else { registerListeners(); RobotActions.initializeData(this); if (actionProcessor.action != null) { actionProcessor.action.initialize(this); } } } super.tick(); if (isClient) { terminal.clientTick(); } if (!isClient) { virtualMachine.tick(); } actionProcessor.tick(); if (!isClient && level instanceof final ServerLevel serverLevel) { final VoxelShape shape = Shapes.create(getBoundingBox()); final Cursor3D iterator = getBlockPosIterator(); while (iterator.advance()) { final int x = iterator.nextX(); final int y = iterator.nextY(); final int z = iterator.nextZ(); mutablePosition.set(x, y, z); final BlockState blockState = serverLevel.getBlockState(mutablePosition); if (blockState.isAir() || blockState.is(Blocks.MOVING_PISTON) || blockState.is(Blocks.PISTON_HEAD)) { continue; } final VoxelShape blockShape = blockState.getCollisionShape(serverLevel, mutablePosition); if (Shapes.joinIsNotEmpty(shape, blockShape.move(x, y, z), BooleanOp.AND)) { final BlockEntity blockEntity = serverLevel.getBlockEntity(mutablePosition); final LootContext.Builder builder = new LootContext.Builder(serverLevel) .withRandom(serverLevel.random) .withParameter(LootContextParams.THIS_ENTITY, this) .withParameter(LootContextParams.ORIGIN, position()) .withParameter(LootContextParams.TOOL, ItemStack.EMPTY) .withParameter(LootContextParams.BLOCK_STATE, blockState) .withOptionalParameter(LootContextParams.BLOCK_ENTITY, blockEntity); final List drops = blockState.getDrops(builder); serverLevel.setBlockAndUpdate(mutablePosition, Blocks.AIR.defaultBlockState()); for (final ItemStack drop : drops) { Block.popResource(serverLevel, mutablePosition, drop); } } } } } @Override public InteractionResult interact(final Player player, final InteractionHand hand) { final ItemStack stack = player.getItemInHand(hand); if (!level.isClientSide()) { if (Wrenches.isWrench(stack)) { if (player.isShiftKeyDown()) { dropSelf(); } else if (player instanceof final ServerPlayer serverPlayer) { openInventoryScreen(serverPlayer); } } else { if (player.isShiftKeyDown()) { start(); } else if (player instanceof final ServerPlayer serverPlayer) { openTerminalScreen(serverPlayer); } } } return InteractionResult.sidedSuccess(level.isClientSide()); } @Override public Packet getAddEntityPacket() { return NetworkHooks.getEntitySpawningPacket(this); } @Override public void setRemoved(final RemovalReason reason) { super.setRemoved(reason); if (!level.isClientSide()) { // Full unload to release out-of-nbt persisted runtime-only data such as ram. virtualMachine.stop(); virtualMachine.dispose(); } } @Override public boolean isPickable() { return true; } @Override public boolean canCollideWith(final Entity entity) { return entity != this; } @Override public void push(final Entity entity) { } @Override public boolean canBeCollidedWith() { return true; } @Override public boolean canSpawnSprintParticle() { return false; } public void exportToItemStack(final ItemStack stack) { final CompoundTag itemsTag = NBTUtils.getOrCreateChildTag(stack.getOrCreateTag(), MOD_TAG_NAME, ITEMS_TAG_NAME); deviceItems.saveItems(itemsTag); // Puts one tag per device type, as expected by TooltipUtils. itemsTag.put(INVENTORY_TAG_NAME, inventory.serializeNBT()); // Won't show up in tooltip. NBTUtils.getOrCreateChildTag(stack.getOrCreateTag(), MOD_TAG_NAME) .put(ENERGY_TAG_NAME, energy.serializeNBT()); } public void importFromItemStack(final ItemStack stack) { final CompoundTag itemsTag = NBTUtils.getChildTag(stack.getTag(), MOD_TAG_NAME, ITEMS_TAG_NAME); deviceItems.loadItems(itemsTag); inventory.deserializeNBT(itemsTag.getCompound(INVENTORY_TAG_NAME)); energy.deserializeNBT(NBTUtils.getChildTag(stack.getTag(), MOD_TAG_NAME, ENERGY_TAG_NAME)); } /////////////////////////////////////////////////////////////////// @Override protected void defineSynchedData() { final SynchedEntityData dataManager = getEntityData(); dataManager.define(TARGET_POSITION, BlockPos.ZERO); dataManager.define(TARGET_DIRECTION, Direction.NORTH); dataManager.define(SELECTED_SLOT, (byte) 0); } @Override protected void addAdditionalSaveData(final CompoundTag tag) { if (virtualMachine.getRunState() != VMRunState.STOPPED) { tag.put(STATE_TAG_NAME, virtualMachine.serialize()); tag.put(TERMINAL_TAG_NAME, NBTSerialization.serialize(terminal)); } tag.put(COMMAND_PROCESSOR_TAG_NAME, actionProcessor.serialize()); tag.put(BUS_ELEMENT_TAG_NAME, busElement.serialize()); tag.put(ITEMS_TAG_NAME, deviceItems.saveItems()); tag.put(DEVICES_TAG_NAME, deviceItems.saveDevices()); tag.put(ENERGY_TAG_NAME, energy.serializeNBT()); tag.put(INVENTORY_TAG_NAME, inventory.serializeNBT()); tag.putByte(SELECTED_SLOT_TAG_NAME, getEntityData().get(SELECTED_SLOT)); } @Override protected void readAdditionalSaveData(final CompoundTag tag) { virtualMachine.deserialize(tag.getCompound(STATE_TAG_NAME)); NBTSerialization.deserialize(tag.getCompound(TERMINAL_TAG_NAME), terminal); actionProcessor.deserialize(tag.getCompound(COMMAND_PROCESSOR_TAG_NAME)); busElement.deserialize(tag.getCompound(BUS_ELEMENT_TAG_NAME)); deviceItems.loadItems(tag.getCompound(ITEMS_TAG_NAME)); deviceItems.loadDevices(tag.getCompound(DEVICES_TAG_NAME)); energy.deserializeNBT(tag.getCompound(ENERGY_TAG_NAME)); inventory.deserializeNBT(tag.getCompound(INVENTORY_TAG_NAME)); setSelectedSlot(tag.getByte(SELECTED_SLOT_TAG_NAME)); } @Override protected Entity.MovementEmission getMovementEmission() { return Entity.MovementEmission.NONE; } @Override protected void checkInsideBlocks() { } @Override protected Vec3 limitPistonMovement(final Vec3 pos) { lastPistonMovement = level.getGameTime(); return super.limitPistonMovement(pos); } /////////////////////////////////////////////////////////////////// @OnlyIn(Dist.CLIENT) private void requestInitialState() { Network.sendToServer(new RobotInitializationRequestMessage(this)); } private void registerListeners() { MinecraftForge.EVENT_BUS.addListener(chunkUnloadListener); MinecraftForge.EVENT_BUS.addListener(worldUnloadListener); } private void unregisterListeners() { MinecraftForge.EVENT_BUS.unregister(chunkUnloadListener); MinecraftForge.EVENT_BUS.unregister(worldUnloadListener); } private void handleChunkUnload(final ChunkEvent.Unload event) { if (event.getWorld() != level) { return; } final ChunkPos chunkPos = new ChunkPos(blockPosition()); if (!Objects.equals(chunkPos, event.getChunk().getPos())) { return; } unregisterListeners(); virtualMachine.suspend(); virtualMachine.dispose(); } private void handleWorldUnload(final WorldEvent.Unload event) { if (event.getWorld() != level) { return; } unregisterListeners(); virtualMachine.suspend(); virtualMachine.dispose(); } private Cursor3D getBlockPosIterator() { final AABB bounds = getBoundingBox(); return new Cursor3D( Mth.floor(bounds.minX), Mth.floor(bounds.minY), Mth.floor(bounds.minZ), Mth.floor(bounds.maxX), Mth.floor(bounds.maxY), Mth.floor(bounds.maxZ) ); } private static float lerpClamped(final float from, final float to, final float delta) { if (from < to) { return Math.min(from + delta, to); } else if (from > to) { return Math.max(from - delta, to); } else { return from; } } private static float remapFrom01To(final float x, final float a1, final float b1) { if (a1 == b1) { return a1; } else { return x * (b1 - a1) + a1; } } /////////////////////////////////////////////////////////////////// public final class AnimationState { private static final float TOP_IDLE_Y = -2f / 16f; private static final float BASE_IDLE_Y = -1f / 16f; private static final float TRANSLATION_SPEED = 0.005f; private static final float ROTATION_SPEED = 1f; private static final float MAX_ROTATION = 5f; private static final float MIN_ROTATION_SPEED = 0.055f; private static final float MAX_ROTATION_SPEED = 0.060f; private static final float HOVER_ANIMATION_SPEED = 0.01f; public float topRenderOffsetY = TOP_IDLE_Y; public float baseRenderOffsetY = BASE_IDLE_Y; public float topRenderRotationY; public float topRenderTargetRotationY; public float topRenderRotationSpeed; public float topRenderHover = -(hashCode() & 0xFFFF); // init to "random" to avoid synchronous hovering public void update(final float deltaTime, final Random random) { if (getVirtualMachine().isRunning() || actionProcessor.hasQueuedActions()) { topRenderHover = topRenderHover + deltaTime * HOVER_ANIMATION_SPEED; final float topOffsetY = Mth.sin(topRenderHover) / 32f; topRenderOffsetY = lerpClamped(topRenderOffsetY, topOffsetY, deltaTime * TRANSLATION_SPEED); baseRenderOffsetY = lerpClamped(baseRenderOffsetY, topOffsetY, deltaTime * TRANSLATION_SPEED); topRenderRotationY = lerpClamped(topRenderRotationY, topRenderTargetRotationY, deltaTime * topRenderRotationSpeed); if (topRenderRotationY == topRenderTargetRotationY) { topRenderTargetRotationY = remapFrom01To(random.nextFloat(), -MAX_ROTATION, MAX_ROTATION); topRenderRotationSpeed = remapFrom01To(random.nextFloat(), MIN_ROTATION_SPEED, MAX_ROTATION_SPEED); } } else { topRenderOffsetY = lerpClamped(topRenderOffsetY, TOP_IDLE_Y, deltaTime * TRANSLATION_SPEED * 2); baseRenderOffsetY = lerpClamped(baseRenderOffsetY, BASE_IDLE_Y, deltaTime * TRANSLATION_SPEED); topRenderRotationY = lerpClamped(topRenderRotationY, 0, deltaTime * ROTATION_SPEED); } } } private static final class RobotActionProcessorResult { private static final String ACTION_ID_TAG_NAME = "action_id"; private static final String RESULT_TAG_NAME = "result"; public int actionId; public RobotActionResult result; public RobotActionProcessorResult(final int actionId, final RobotActionResult result) { this.actionId = actionId; this.result = result; } public RobotActionProcessorResult(final CompoundTag tag) { deserialize(tag); } public CompoundTag serialize() { final CompoundTag tag = new CompoundTag(); tag.putInt(ACTION_ID_TAG_NAME, actionId); NBTUtils.putEnum(tag, RESULT_TAG_NAME, result); return tag; } public void deserialize(final CompoundTag tag) { actionId = tag.getInt(ACTION_ID_TAG_NAME); result = NBTUtils.getEnum(tag, RESULT_TAG_NAME, RobotActionResult.class); } } private final class RobotActionProcessor { private static final String QUEUE_TAG_NAME = "queue"; private static final String ACTION_TAG_NAME = "action"; private static final String RESULTS_TAG_NAME = "results"; private static final String LAST_ACTION_ID_TAG_NAME = "last_action_id"; private final Queue queue = new ArrayDeque<>(MAX_QUEUED_ACTIONS - 1); @Nullable private AbstractRobotAction action; private final Queue results = new ArrayDeque<>(MAX_QUEUED_RESULTS); private int lastActionId; public boolean hasQueuedActions() { return action != null || !queue.isEmpty(); } public int getQueuedActionCount() { return (action != null ? 1 : 0) + queue.size(); } public boolean move(final MovementDirection direction) { return addAction(new RobotMovementAction(direction)); } public boolean rotate(final RotationDirection direction) { return addAction(new RobotRotationAction(direction)); } public void tick() { if (level.isClientSide()) { RobotActions.performClient(Robot.this); } else { if (action != null) { final RobotActionResult result = action.perform(Robot.this); if (result != RobotActionResult.INCOMPLETE) { synchronized (results) { if (results.size() == MAX_QUEUED_RESULTS) { results.remove(); } results.add(new RobotActionProcessorResult(action.getId(), result)); } action = null; } } if (action == null) { action = queue.poll(); if (action != null) { action.initialize(Robot.this); } } RobotActions.performServer(Robot.this, action); } } public void clear() { queue.clear(); results.clear(); lastActionId = 0; } public CompoundTag serialize() { final CompoundTag tag = new CompoundTag(); final ListTag queueTag = new ListTag(); for (final AbstractRobotAction action : queue) { queueTag.add(RobotActions.serialize(action)); } tag.put(QUEUE_TAG_NAME, queueTag); if (action != null) { tag.put(ACTION_TAG_NAME, RobotActions.serialize(action)); } final ListTag resultsTag = new ListTag(); for (final RobotActionProcessorResult result : results) { resultsTag.add(result.serialize()); } tag.put(RESULTS_TAG_NAME, resultsTag); tag.putInt(LAST_ACTION_ID_TAG_NAME, lastActionId); return tag; } public void deserialize(final CompoundTag tag) { queue.clear(); results.clear(); final ListTag queueTag = tag.getList(QUEUE_TAG_NAME, NBTTagIds.TAG_COMPOUND); for (int i = 0; i < Math.min(queueTag.size(), MAX_QUEUED_ACTIONS - 1); i++) { final AbstractRobotAction action = RobotActions.deserialize(queueTag.getCompound(i)); if (action != null) { queue.add(action); } } action = RobotActions.deserialize(tag.getCompound(ACTION_TAG_NAME)); final ListTag resultsTag = tag.getList(RESULTS_TAG_NAME, NBTTagIds.TAG_COMPOUND); for (int i = 0; i < Math.min(resultsTag.size(), MAX_QUEUED_RESULTS); i++) { final RobotActionProcessorResult result = new RobotActionProcessorResult(resultsTag.getCompound(i)); if (result.actionId != 0) { results.add(result); } } lastActionId = tag.getInt(LAST_ACTION_ID_TAG_NAME); } private boolean addAction(final AbstractRobotAction action) { if (level.isClientSide()) { return false; } if (!getVirtualMachine().isRunning()) { return false; } if (queue.size() < MAX_QUEUED_ACTIONS - 1) { // -1 for current action lastActionId = (lastActionId + 1) & 0x7FFFFFFF; // only positive ids; unlikely to ever wrap, but eh. action.setId(lastActionId); synchronized (queue) { queue.add(action); } return true; } else { return false; } } } private final class RobotItemStackHandlers extends AbstractVMItemStackHandlers { public RobotItemStackHandlers() { super( new GroupDefinition(DeviceTypes.MEMORY, MEMORY_SLOTS), new GroupDefinition(DeviceTypes.HARD_DRIVE, HARD_DRIVE_SLOTS), new GroupDefinition(DeviceTypes.FLASH_MEMORY, FLASH_MEMORY_SLOTS), new GroupDefinition(DeviceTypes.ROBOT_MODULE, MODULE_SLOTS) ); } @Override protected ItemDeviceQuery getDeviceQuery(final ItemStack stack) { return Devices.makeQuery(Robot.this, stack); } @Override protected void onContentsChanged(final DeviceItemStackHandler itemHandler, final int slot) { if (!level.isClientSide()) { virtualMachine.busController.scheduleBusScan(); } } } private final class RobotBusElement extends AbstractDeviceBusElement { private static final String DEVICE_ID_TAG_NAME = "device_id"; private final Device device = new ObjectDevice(new RobotDevice(), "robot"); private UUID deviceId = UUID.randomUUID(); @Override public Optional>> getNeighbors() { return Optional.of(singleton(LazyOptional.of(() -> deviceItems.busElement))); } @Override public Collection getLocalDevices() { return singleton(device); } @Override public Optional getDeviceIdentifier(final Device device) { if (device == this.device) { return Optional.of(deviceId); } return super.getDeviceIdentifier(device); } public CompoundTag serialize() { final CompoundTag tag = new CompoundTag(); tag.putUUID(DEVICE_ID_TAG_NAME, deviceId); return tag; } public void deserialize(final CompoundTag tag) { if (tag.hasUUID(DEVICE_ID_TAG_NAME)) { deviceId = tag.getUUID(DEVICE_ID_TAG_NAME); } } } private final class RobotVMRunner extends AbstractTerminalVMRunner { public RobotVMRunner(final AbstractVirtualMachine virtualMachine, final Terminal terminal) { super(virtualMachine, terminal); } @Override protected void sendTerminalUpdateToClient(final ByteBuffer output) { Network.sendToClientsTrackingEntity(new RobotTerminalOutputMessage(Robot.this, output), Robot.this); } } private final class RobotVirtualMachine extends AbstractVirtualMachine { private RobotVirtualMachine(final CommonDeviceBusController busController) { super(busController); state.vmAdapter.setBaseAddressProvider(deviceItems::getDeviceAddressBase); } @Override protected boolean consumeEnergy(final int amount, final boolean simulate) { if (!Config.robotsUseEnergy()) { return true; } if (amount > energy.getEnergyStored()) { return false; } energy.extractEnergy(amount, simulate); return true; } @Override protected void stopRunnerAndReset() { super.stopRunnerAndReset(); TerminalUtils.resetTerminal(terminal, output -> Network.sendToClientsTrackingEntity( new RobotTerminalOutputMessage(Robot.this, output), Robot.this)); actionProcessor.clear(); } @Override protected AbstractTerminalVMRunner createRunner() { return new RobotVMRunner(this, terminal); } @Override protected void handleBusStateChanged(final CommonDeviceBusController.BusState value) { Network.sendToClientsTrackingEntity(new RobotBusStateMessage(Robot.this, value), Robot.this); } @Override protected void handleRunStateChanged(final VMRunState value) { Network.sendToClientsTrackingEntity(new RobotRunStateMessage(Robot.this, value), Robot.this); } @Override protected void handleBootErrorChanged(@Nullable final Component value) { Network.sendToClientsTrackingEntity(new RobotBootErrorMessage(Robot.this, value), Robot.this); } } public final class RobotDevice { @Callback(synchronize = false) public int getEnergyStored() { return energy.getEnergyStored(); } @Callback(synchronize = false) public int getEnergyCapacity() { return energy.getMaxEnergyStored(); } @Callback(synchronize = false) public int getSelectedSlot() { return Robot.this.getSelectedSlot(); } @Callback(synchronize = false) public void setSelectedSlot(@Parameter("slot") final int slot) { Robot.this.setSelectedSlot(slot); } @Callback public ItemStack getStackInSlot(@Parameter("slot") final int slot) { return inventory.getStackInSlot(slot); } @Callback(synchronize = false) public boolean move(@Parameter("direction") @Nullable final MovementDirection direction) { if (direction == null) throw new IllegalArgumentException(); return actionProcessor.move(direction); } @Callback(synchronize = false) public boolean turn(@Parameter("direction") @Nullable final RotationDirection direction) { if (direction == null) throw new IllegalArgumentException(); return actionProcessor.rotate(direction); } @Callback(synchronize = false) public int getLastActionId() { return actionProcessor.lastActionId; } @Callback(synchronize = false) public int getQueuedActionCount() { return actionProcessor.getQueuedActionCount(); } @Nullable @Callback(synchronize = false) public RobotActionResult getActionResult(@Parameter("actionId") final int actionId) { final AbstractRobotAction currentAction = actionProcessor.action; if (currentAction != null && currentAction.getId() == actionId) { return RobotActionResult.INCOMPLETE; } synchronized (actionProcessor.queue) { for (final AbstractRobotAction action : actionProcessor.queue) { if (action.getId() == actionId) { return RobotActionResult.INCOMPLETE; } } } synchronized (actionProcessor.results) { for (final RobotActionProcessorResult result : actionProcessor.results) { if (result.actionId == actionId) { return result.result; } } } return null; } private RobotDevice() { } } }