/** * Copyright (c) 2011-2015, SpaceToad and the BuildCraft Team * http://www.mod-buildcraft.com * <p/> * BuildCraft is distributed under the terms of the Minecraft Mod Public * License 1.0, or MMPL. Please check the contents of the license located in * http://www.mod-buildcraft.com/MMPL-1.0.txt */ package buildcraft.builders; import java.util.LinkedList; import java.util.List; import java.util.Set; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import io.netty.buffer.ByteBuf; import net.minecraft.block.Block; import net.minecraft.block.BlockLiquid; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.ISidedInventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.MathHelper; import net.minecraft.world.ChunkCoordIntPair; import net.minecraftforge.common.ForgeChunkManager; import net.minecraftforge.common.ForgeChunkManager.Ticket; import net.minecraftforge.common.ForgeChunkManager.Type; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.ForgeDirection; import net.minecraftforge.fluids.IFluidBlock; import buildcraft.BuildCraftBuilders; import buildcraft.BuildCraftCore; import buildcraft.api.blueprints.BuilderAPI; import buildcraft.api.core.BuildCraftAPI; import buildcraft.api.core.IAreaProvider; import buildcraft.api.core.SafeTimeTracker; import buildcraft.api.filler.FillerManager; import buildcraft.api.statements.IStatementParameter; import buildcraft.api.tiles.IControllable; import buildcraft.api.tiles.IHasWork; import buildcraft.api.transport.IPipeConnection; import buildcraft.api.transport.IPipeTile; import buildcraft.core.Box; import buildcraft.core.Box.Kind; import buildcraft.core.CoreConstants; import buildcraft.core.DefaultAreaProvider; import buildcraft.core.DefaultProps; import buildcraft.core.blueprints.Blueprint; import buildcraft.core.blueprints.BptBuilderBase; import buildcraft.core.blueprints.BptBuilderBlueprint; import buildcraft.core.builders.TileAbstractBuilder; import buildcraft.core.builders.patterns.FillerPattern; import buildcraft.core.internal.IDropControlInventory; import buildcraft.core.internal.ILEDProvider; import buildcraft.core.lib.RFBattery; import buildcraft.core.lib.utils.BlockMiner; import buildcraft.core.lib.utils.BlockUtils; import buildcraft.core.lib.utils.Utils; import buildcraft.core.proxy.CoreProxy; public class TileQuarry extends TileAbstractBuilder implements IHasWork, ISidedInventory, IDropControlInventory, IPipeConnection, IControllable, ILEDProvider { private enum Stage { BUILDING, DIGGING, MOVING, IDLE, DONE } public EntityMechanicalArm arm; public EntityPlayer placedBy; protected Box box = new Box(); private int targetX, targetY, targetZ; private double headPosX, headPosY, headPosZ; private double speed = 0.03; private Stage stage = Stage.BUILDING; private boolean movingHorizontally; private boolean movingVertically; private float headTrajectory; private SafeTimeTracker updateTracker = new SafeTimeTracker(BuildCraftCore.updateFactor); private BptBuilderBase builder; private final LinkedList<int[]> visitList = Lists.newLinkedList(); private boolean loadDefaultBoundaries = false; private Ticket chunkTicket; private boolean frameProducer = true; private NBTTagCompound initNBT = null; private BlockMiner miner; private int ledState; public TileQuarry() { box.kind = Kind.STRIPES; this.setBattery(new RFBattery((int) (2 * 64 * BuilderAPI.BREAK_ENERGY * BuildCraftCore.miningMultiplier), (int) (1000 * BuildCraftCore.miningMultiplier), 0)); } public void createUtilsIfNeeded() { if (!worldObj.isRemote) { if (builder == null) { if (!box.isInitialized()) { setBoundaries(loadDefaultBoundaries); } initializeBlueprintBuilder(); } } if (stage != Stage.BUILDING) { box.isVisible = false; if (arm == null) { createArm(); } if (findTarget(false)) { if ((headPosX < box.xMin || headPosX > box.xMax) || (headPosZ < box.zMin || headPosZ > box.zMax)) { setHead(box.xMin + 1, yCoord + 2, box.zMin + 1); } } } else { box.isVisible = true; } } private void createArm() { worldObj.spawnEntityInWorld (new EntityMechanicalArm(worldObj, box.xMin + CoreConstants.PIPE_MAX_POS, yCoord + box.sizeY() - 1 + CoreConstants.PIPE_MIN_POS, box.zMin + CoreConstants.PIPE_MAX_POS, box.sizeX() - 2 + CoreConstants.PIPE_MIN_POS * 2, box.sizeZ() - 2 + CoreConstants.PIPE_MIN_POS * 2, this)); } // Callback from the arm once it's created public void setArm(EntityMechanicalArm arm) { this.arm = arm; } public boolean areChunksLoaded() { if (BuildCraftBuilders.quarryLoadsChunks) { // Small optimization return true; } // Each chunk covers the full height, so we only check one of them per height. return worldObj.blockExists(box.xMin, box.yMax, box.zMin) && worldObj.blockExists(box.xMax, box.yMax, box.zMin) && worldObj.blockExists(box.xMin, box.yMax, box.zMax) && worldObj.blockExists(box.xMax, box.yMax, box.zMax); } @Override public void updateEntity() { super.updateEntity(); if (worldObj.isRemote) { if (stage != Stage.DONE) { moveHead(speed); } return; } if (stage == Stage.DONE) { if (mode == Mode.Loop) { stage = Stage.IDLE; } else { return; } } if (!areChunksLoaded()) { return; } if (mode == Mode.Off && stage != Stage.MOVING) { return; } createUtilsIfNeeded(); if (stage == Stage.BUILDING) { if (builder != null && !builder.isDone(this)) { builder.buildNextSlot(worldObj, this, xCoord, yCoord, zCoord); } else { stage = Stage.IDLE; } } else if (stage == Stage.DIGGING) { dig(); } else if (stage == Stage.IDLE) { idling(); // We are sending a network packet update ONLY below. // In this case, since idling() does it anyway, we should return. return; } else if (stage == Stage.MOVING) { int energyUsed = this.getBattery().useEnergy(20, (int) Math.ceil(20D + (double) getBattery().getEnergyStored() / 10), false); if (energyUsed >= 20) { speed = 0.1 + energyUsed / 2000F; // If it's raining or snowing above the head, slow down. if (worldObj.isRaining()) { int headBPX = (int) headPosX; int headBPY = (int) headPosY; int headBPZ = (int) headPosZ; if (worldObj.getHeightValue(headBPX, headBPZ) < headBPY) { speed *= 0.7; } } moveHead(speed); } else { speed = 0; } } if (updateTracker.markTimeIfDelay(worldObj)) { sendNetworkUpdate(); } } protected void dig() { if (worldObj.isRemote) { return; } if (miner == null) { // Hmm. Probably shouldn't be mining if there's no miner. stage = Stage.IDLE; return; } int rfTaken = miner.acceptEnergy(getBattery().getEnergyStored()); getBattery().useEnergy(rfTaken, rfTaken, false); if (miner.hasMined()) { // Collect any lost items laying around. double[] head = getHead(); AxisAlignedBB axis = AxisAlignedBB.getBoundingBox(head[0] - 2, head[1] - 2, head[2] - 2, head[0] + 3, head[1] + 3, head[2] + 3); List<EntityItem> result = worldObj.getEntitiesWithinAABB(EntityItem.class, axis); for (EntityItem entity : result) { if (entity.isDead) { continue; } ItemStack mineable = entity.getEntityItem(); if (mineable.stackSize <= 0) { continue; } CoreProxy.proxy.removeEntity(entity); miner.mineStack(mineable); } } if (miner.hasMined() || miner.hasFailed()) { miner = null; if (!findFrame()) { initializeBlueprintBuilder(); stage = Stage.BUILDING; } else { stage = Stage.IDLE; } } } protected boolean findFrame() { for (int i = 2; i < 6; i++) { ForgeDirection o = ForgeDirection.getOrientation(i); if (box.contains(xCoord + o.offsetX, yCoord + o.offsetY, zCoord + o.offsetZ)) { return worldObj.getBlock(xCoord + o.offsetX, yCoord + o.offsetY, zCoord + o.offsetZ) == BuildCraftBuilders.frameBlock; } } // Could not find any location in box - this is strange, so obviously // we're going to ignore it! return true; } protected void idling() { if (!findTarget(true)) { // I believe the issue is box going null becuase of bad chunkloader positioning if (arm != null && box != null) { setTarget(box.xMin + 1, yCoord + 2, box.zMin + 1); } stage = Stage.DONE; } else { stage = Stage.MOVING; } movingHorizontally = true; movingVertically = true; double[] head = getHead(); int[] target = getTarget(); headTrajectory = (float) Math.atan2(target[2] - head[2], target[0] - head[0]); sendNetworkUpdate(); } public boolean findTarget(boolean doSet) { if (worldObj.isRemote) { return false; } boolean columnVisitListIsUpdated = false; if (visitList.isEmpty()) { createColumnVisitList(); columnVisitListIsUpdated = true; } if (!doSet) { return !visitList.isEmpty(); } if (visitList.isEmpty()) { return false; } int[] nextTarget = visitList.removeFirst(); if (!columnVisitListIsUpdated) { // nextTarget may not be accurate, at least search the target column for changes for (int y = nextTarget[1] + 1; y < yCoord + 3; y++) { if (isQuarriableBlock(nextTarget[0], y, nextTarget[2])) { createColumnVisitList(); columnVisitListIsUpdated = true; nextTarget = null; break; } } } if (columnVisitListIsUpdated && nextTarget == null && !visitList.isEmpty()) { nextTarget = visitList.removeFirst(); } else if (columnVisitListIsUpdated && nextTarget == null) { return false; } setTarget(nextTarget[0], nextTarget[1] + 1, nextTarget[2]); return true; } /** * Make the column visit list: called once per layer */ private void createColumnVisitList() { visitList.clear(); boolean[][] blockedColumns = new boolean[builder.blueprint.sizeX - 2][builder.blueprint.sizeZ - 2]; for (int searchY = yCoord + 3; searchY >= 1; --searchY) { int startX, endX, incX; if (searchY % 2 == 0) { startX = 0; endX = builder.blueprint.sizeX - 2; incX = 1; } else { startX = builder.blueprint.sizeX - 3; endX = -1; incX = -1; } for (int searchX = startX; searchX != endX; searchX += incX) { int startZ, endZ, incZ; if (searchX % 2 == searchY % 2) { startZ = 0; endZ = builder.blueprint.sizeZ - 2; incZ = 1; } else { startZ = builder.blueprint.sizeZ - 3; endZ = -1; incZ = -1; } for (int searchZ = startZ; searchZ != endZ; searchZ += incZ) { if (!blockedColumns[searchX][searchZ]) { int bx = box.xMin + searchX + 1, by = searchY, bz = box.zMin + searchZ + 1; Block block = worldObj.getBlock(bx, by, bz); if (!BlockUtils.canChangeBlock(block, worldObj, bx, by, bz)) { blockedColumns[searchX][searchZ] = true; } else if (!BuildCraftAPI.isSoftBlock(worldObj, bx, by, bz) && !(block instanceof BlockLiquid) && !(block instanceof IFluidBlock)) { visitList.add(new int[]{bx, by, bz}); } // Stop at two planes - generally any obstructions will have been found and will force a recompute prior to this if (visitList.size() > builder.blueprint.sizeZ * builder.blueprint.sizeX * 2) { return; } } } } } } @Override public void readFromNBT(NBTTagCompound nbttagcompound) { super.readFromNBT(nbttagcompound); if (nbttagcompound.hasKey("box")) { box.initialize(nbttagcompound.getCompoundTag("box")); loadDefaultBoundaries = false; } else if (nbttagcompound.hasKey("xSize")) { // This is a legacy save, get old data int xMin = nbttagcompound.getInteger("xMin"); int zMin = nbttagcompound.getInteger("zMin"); int xSize = nbttagcompound.getInteger("xSize"); int ySize = nbttagcompound.getInteger("ySize"); int zSize = nbttagcompound.getInteger("zSize"); box.initialize(xMin, yCoord, zMin, xMin + xSize - 1, yCoord + ySize - 1, zMin + zSize - 1); loadDefaultBoundaries = false; } else { // This is a legacy save, compute boundaries loadDefaultBoundaries = true; } targetX = nbttagcompound.getInteger("targetX"); targetY = nbttagcompound.getInteger("targetY"); targetZ = nbttagcompound.getInteger("targetZ"); headPosX = nbttagcompound.getDouble("headPosX"); headPosY = nbttagcompound.getDouble("headPosY"); headPosZ = nbttagcompound.getDouble("headPosZ"); // The rest of load has to be done upon initialize. initNBT = (NBTTagCompound) nbttagcompound.getCompoundTag("bpt").copy(); } @Override public void writeToNBT(NBTTagCompound nbttagcompound) { super.writeToNBT(nbttagcompound); nbttagcompound.setInteger("targetX", targetX); nbttagcompound.setInteger("targetY", targetY); nbttagcompound.setInteger("targetZ", targetZ); nbttagcompound.setDouble("headPosX", headPosX); nbttagcompound.setDouble("headPosY", headPosY); nbttagcompound.setDouble("headPosZ", headPosZ); NBTTagCompound boxTag = new NBTTagCompound(); box.writeToNBT(boxTag); nbttagcompound.setTag("box", boxTag); NBTTagCompound bptNBT = new NBTTagCompound(); if (builder != null) { NBTTagCompound builderCpt = new NBTTagCompound(); builder.saveBuildStateToNBT(builderCpt, this); bptNBT.setTag("builderState", builderCpt); } nbttagcompound.setTag("bpt", bptNBT); } @SuppressWarnings("rawtypes") public void positionReached() { if (worldObj.isRemote) { return; } if (isQuarriableBlock(targetX, targetY - 1, targetZ)) { miner = new BlockMiner(worldObj, this, targetX, targetY - 1, targetZ); stage = Stage.DIGGING; } else { stage = Stage.IDLE; } } private boolean isQuarriableBlock(int bx, int by, int bz) { Block block = worldObj.getBlock(bx, by, bz); return BlockUtils.canChangeBlock(block, worldObj, bx, by, bz) && !BuildCraftAPI.isSoftBlock(worldObj, bx, by, bz) && !(block instanceof BlockLiquid) && !(block instanceof IFluidBlock); } @Override protected int getNetworkUpdateRange() { return DefaultProps.NETWORK_UPDATE_RANGE + (int) Math.ceil(Math.sqrt(yCoord * yCoord + box.sizeX() * box.sizeX() + box.sizeZ() * box.sizeZ())); } @Override public void invalidate() { if (chunkTicket != null) { ForgeChunkManager.releaseTicket(chunkTicket); } super.invalidate(); destroy(); } @Override public void onChunkUnload() { destroy(); } @Override public void destroy() { if (arm != null) { arm.setDead(); } arm = null; frameProducer = false; if (miner != null) { miner.invalidate(); } } @Override public boolean hasWork() { return stage != Stage.DONE; } private void setBoundaries(boolean useDefaultI) { boolean useDefault = useDefaultI; if (BuildCraftBuilders.quarryLoadsChunks && chunkTicket == null) { chunkTicket = ForgeChunkManager.requestTicket(BuildCraftBuilders.instance, worldObj, Type.NORMAL); } if (chunkTicket != null) { chunkTicket.getModData().setInteger("quarryX", xCoord); chunkTicket.getModData().setInteger("quarryY", yCoord); chunkTicket.getModData().setInteger("quarryZ", zCoord); } IAreaProvider a = null; if (!useDefault) { a = Utils.getNearbyAreaProvider(worldObj, xCoord, yCoord, zCoord); } if (a == null) { a = new DefaultAreaProvider(xCoord, yCoord, zCoord, xCoord + 10, yCoord + 4, zCoord + 10); useDefault = true; } int xSize = a.xMax() - a.xMin() + 1; int zSize = a.zMax() - a.zMin() + 1; if (xSize < 3 || zSize < 3 || (chunkTicket != null && ((xSize * zSize) >> 8) >= chunkTicket.getMaxChunkListDepth())) { if (placedBy != null) { placedBy.addChatMessage(new ChatComponentTranslation("chat.buildcraft.quarry.tooSmall", xSize, zSize, chunkTicket != null ? chunkTicket.getMaxChunkListDepth() : 0)); } a = new DefaultAreaProvider(xCoord, yCoord, zCoord, xCoord + 10, yCoord + 4, zCoord + 10); useDefault = true; } xSize = a.xMax() - a.xMin() + 1; int ySize = a.yMax() - a.yMin() + 1; zSize = a.zMax() - a.zMin() + 1; box.initialize(a); if (ySize < 5) { ySize = 5; box.yMax = box.yMin + ySize - 1; } if (useDefault) { int xMin, zMin; int dir = worldObj.getBlockMetadata(xCoord, yCoord, zCoord); ForgeDirection o = ForgeDirection.getOrientation(dir > 6 ? 6 : dir).getOpposite(); switch (o) { case EAST: xMin = xCoord + 1; zMin = zCoord - 4 - 1; break; case WEST: xMin = xCoord - 9 - 2; zMin = zCoord - 4 - 1; break; case SOUTH: xMin = xCoord - 4 - 1; zMin = zCoord + 1; break; case NORTH: default: xMin = xCoord - 4 - 1; zMin = zCoord - 9 - 2; break; } box.initialize(xMin, yCoord, zMin, xMin + xSize - 1, yCoord + ySize - 1, zMin + zSize - 1); } a.removeFromWorld(); if (chunkTicket != null) { forceChunkLoading(chunkTicket); } sendNetworkUpdate(); } private void initializeBlueprintBuilder() { Blueprint bpt = ((FillerPattern) FillerManager.registry.getPattern("buildcraft:frame")) .getBlueprint(box, worldObj, new IStatementParameter[0], BuildCraftBuilders.frameBlock, 0); if (bpt != null) { builder = new BptBuilderBlueprint(bpt, worldObj, box.xMin, yCoord, box.zMin); speed = 0; stage = Stage.BUILDING; sendNetworkUpdate(); } } @Override public void writeData(ByteBuf stream) { super.writeData(stream); box.writeData(stream); stream.writeInt(targetX); stream.writeShort(targetY); stream.writeInt(targetZ); stream.writeDouble(headPosX); stream.writeDouble(headPosY); stream.writeDouble(headPosZ); stream.writeFloat((float) speed); stream.writeFloat(headTrajectory); int flags = stage.ordinal(); flags |= movingHorizontally ? 0x10 : 0; flags |= movingVertically ? 0x20 : 0; stream.writeByte(flags); ledState = (hasWork() && mode != Mode.Off && getTicksSinceEnergyReceived() < 12 ? 16 : 0) | (getBattery().getEnergyStored() * 15 / getBattery().getMaxEnergyStored()); stream.writeByte(ledState); } @Override public void readData(ByteBuf stream) { super.readData(stream); box.readData(stream); targetX = stream.readInt(); targetY = stream.readUnsignedShort(); targetZ = stream.readInt(); headPosX = stream.readDouble(); headPosY = stream.readDouble(); headPosZ = stream.readDouble(); speed = stream.readFloat(); headTrajectory = stream.readFloat(); int flags = stream.readUnsignedByte(); stage = Stage.values()[flags & 0x07]; movingHorizontally = (flags & 0x10) != 0; movingVertically = (flags & 0x20) != 0; int newLedState = stream.readUnsignedByte(); if (newLedState != ledState) { ledState = newLedState; worldObj.markBlockRangeForRenderUpdate(xCoord, yCoord, zCoord, xCoord, yCoord, zCoord); } createUtilsIfNeeded(); if (arm != null) { arm.setHead(headPosX, headPosY, headPosZ); arm.updatePosition(); } } @Override public void initialize() { super.initialize(); if (!this.getWorldObj().isRemote && !box.initialized) { setBoundaries(false); } createUtilsIfNeeded(); if (initNBT != null && builder != null) { builder.loadBuildStateToNBT( initNBT.getCompoundTag("builderState"), this); } initNBT = null; sendNetworkUpdate(); } public void reinitalize() { initializeBlueprintBuilder(); } @Override public int getSizeInventory() { return 1; } @Override public ItemStack getStackInSlot(int i) { if (frameProducer) { return new ItemStack(BuildCraftBuilders.frameBlock); } else { return null; } } @Override public ItemStack decrStackSize(int i, int j) { if (frameProducer) { return new ItemStack(BuildCraftBuilders.frameBlock, j); } else { return null; } } @Override public void setInventorySlotContents(int i, ItemStack itemstack) { } @Override public ItemStack getStackInSlotOnClosing(int slot) { return null; } @Override public String getInventoryName() { return ""; } @Override public int getInventoryStackLimit() { return 0; } @Override public boolean isItemValidForSlot(int i, ItemStack itemstack) { return false; } @Override public boolean isUseableByPlayer(EntityPlayer entityplayer) { return false; } @Override public void openInventory() { } @Override public void closeInventory() { } @Override public boolean isBuildingMaterialSlot(int i) { return true; } public void moveHead(double instantSpeed) { int[] target = getTarget(); double[] head = getHead(); if (movingHorizontally) { if (Math.abs(target[0] - head[0]) < instantSpeed * 2 && Math.abs(target[2] - head[2]) < instantSpeed * 2) { head[0] = target[0]; head[2] = target[2]; movingHorizontally = false; if (!movingVertically) { positionReached(); head[1] = target[1]; } } else { head[0] += MathHelper.cos(headTrajectory) * instantSpeed; head[2] += MathHelper.sin(headTrajectory) * instantSpeed; } setHead(head[0], head[1], head[2]); } if (movingVertically) { if (Math.abs(target[1] - head[1]) < instantSpeed * 2) { head[1] = target[1]; movingVertically = false; if (!movingHorizontally) { positionReached(); head[0] = target[0]; head[2] = target[2]; } } else { if (target[1] > head[1]) { head[1] += instantSpeed; } else { head[1] -= instantSpeed; } } setHead(head[0], head[1], head[2]); } updatePosition(); } private void updatePosition() { if (arm != null && worldObj.isRemote) { arm.setHead(headPosX, headPosY, headPosZ); arm.updatePosition(); } } private void setHead(double x, double y, double z) { this.headPosX = x; this.headPosY = y; this.headPosZ = z; } private double[] getHead() { return new double[]{headPosX, headPosY, headPosZ}; } private int[] getTarget() { return new int[]{targetX, targetY, targetZ}; } private void setTarget(int x, int y, int z) { this.targetX = x; this.targetY = y; this.targetZ = z; } public void forceChunkLoading(Ticket ticket) { if (chunkTicket == null) { chunkTicket = ticket; } Set<ChunkCoordIntPair> chunks = Sets.newHashSet(); ChunkCoordIntPair quarryChunk = new ChunkCoordIntPair(xCoord >> 4, zCoord >> 4); chunks.add(quarryChunk); ForgeChunkManager.forceChunk(ticket, quarryChunk); for (int chunkX = box.xMin >> 4; chunkX <= box.xMax >> 4; chunkX++) { for (int chunkZ = box.zMin >> 4; chunkZ <= box.zMax >> 4; chunkZ++) { ChunkCoordIntPair chunk = new ChunkCoordIntPair(chunkX, chunkZ); ForgeChunkManager.forceChunk(ticket, chunk); chunks.add(chunk); } } if (placedBy != null && !(placedBy instanceof FakePlayer)) { placedBy.addChatMessage(new ChatComponentTranslation( "chat.buildcraft.quarry.chunkloadInfo", xCoord, yCoord, zCoord, chunks.size())); } } @Override public boolean hasCustomInventoryName() { return false; } @Override public AxisAlignedBB getRenderBoundingBox() { return new Box(this).extendToEncompass(box).expand(50).getBoundingBox(); } @Override public Box getBox() { return box; } @Override public int[] getAccessibleSlotsFromSide(int side) { return new int[]{}; } @Override public boolean canInsertItem(int p1, ItemStack p2, int p3) { return false; } @Override public boolean canExtractItem(int p1, ItemStack p2, int p3) { return false; } @Override public boolean acceptsControlMode(Mode mode) { return mode == Mode.Off || mode == Mode.On || mode == Mode.Loop; } @Override public boolean doDrop() { return false; } @Override public ConnectOverride overridePipeConnection(IPipeTile.PipeType type, ForgeDirection with) { return type == IPipeTile.PipeType.ITEM ? ConnectOverride.CONNECT : ConnectOverride.DEFAULT; } @Override public int getLEDLevel(int led) { if (led == 0) { // Red LED return ledState & 15; } else { // Green LED return (ledState >> 4) > 0 ? 15 : 0; } } }