diff --git a/src/main/java/li/cil/oc2/client/ClientSetup.java b/src/main/java/li/cil/oc2/client/ClientSetup.java index be774840..93ac260c 100644 --- a/src/main/java/li/cil/oc2/client/ClientSetup.java +++ b/src/main/java/li/cil/oc2/client/ClientSetup.java @@ -7,10 +7,7 @@ import li.cil.oc2.client.item.CustomItemModelProperties; import li.cil.oc2.client.model.BusCableModelLoader; import li.cil.oc2.client.renderer.BusInterfaceNameRenderer; import li.cil.oc2.client.renderer.NetworkCableRenderer; -import li.cil.oc2.client.renderer.blockentity.ChargerRenderer; -import li.cil.oc2.client.renderer.blockentity.ComputerRenderer; -import li.cil.oc2.client.renderer.blockentity.DiskDriveRenderer; -import li.cil.oc2.client.renderer.blockentity.NetworkConnectorRenderer; +import li.cil.oc2.client.renderer.blockentity.*; import li.cil.oc2.client.renderer.color.BusCableBlockColor; import li.cil.oc2.client.renderer.entity.RobotRenderer; import li.cil.oc2.client.renderer.entity.model.RobotModel; @@ -43,6 +40,7 @@ public final class ClientSetup { BlockEntityRenderers.register(BlockEntities.NETWORK_CONNECTOR.get(), NetworkConnectorRenderer::new); BlockEntityRenderers.register(BlockEntities.DISK_DRIVE.get(), DiskDriveRenderer::new); BlockEntityRenderers.register(BlockEntities.CHARGER.get(), ChargerRenderer::new); + BlockEntityRenderers.register(BlockEntities.PROJECTOR.get(), ProjectorRenderer::new); event.enqueueWork(() -> { CustomItemModelProperties.initialize(); diff --git a/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java b/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java index b2850306..ce34c030 100644 --- a/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java +++ b/src/main/java/li/cil/oc2/client/renderer/ModRenderType.java @@ -1,30 +1,48 @@ package li.cil.oc2.client.renderer; +import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.VertexFormat; import li.cil.oc2.api.API; -import net.minecraft.client.renderer.RenderStateShard; import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; @OnlyIn(Dist.CLIENT) public abstract class ModRenderType extends RenderType { private static final RenderType NETWORK_CABLE = create( - API.MOD_ID + "/network_cable", - DefaultVertexFormat.POSITION_COLOR_LIGHTMAP, - VertexFormat.Mode.QUADS, - 256, - false, - false, - CompositeState.builder() - .setShaderState(POSITION_COLOR_LIGHTMAP_SHADER) - .setTextureState(NO_TEXTURE) - .setTransparencyState(NO_TRANSPARENCY) - .setCullState(NO_CULL) - .setLightmapState(LIGHTMAP) - .createCompositeState(false)); + API.MOD_ID + "/network_cable", + DefaultVertexFormat.POSITION_COLOR_LIGHTMAP, + VertexFormat.Mode.QUADS, + 256, + false, + false, + CompositeState.builder() + .setShaderState(POSITION_COLOR_LIGHTMAP_SHADER) + .setTextureState(NO_TEXTURE) + .setTransparencyState(NO_TRANSPARENCY) + .setCullState(NO_CULL) + .setLightmapState(LIGHTMAP) + .createCompositeState(false)); + + private static final RenderType PROJECTOR_LIGHT = create( + API.MOD_ID + "/projector_light", + DefaultVertexFormat.POSITION_COLOR, + VertexFormat.Mode.QUADS, + 256, + false, + true, + CompositeState.builder() + .setShaderState(RENDERTYPE_LIGHTNING_SHADER) + .setOutputState(WEATHER_TARGET) + .setTransparencyState(LIGHTNING_TRANSPARENCY) + .setWriteMaskState(COLOR_WRITE) + .setCullState(NO_CULL) + .createCompositeState(false)); /////////////////////////////////////////////////////////////////// @@ -32,6 +50,10 @@ public abstract class ModRenderType extends RenderType { return NETWORK_CABLE; } + public static RenderType getProjectorLight() { + return PROJECTOR_LIGHT; + } + public static RenderType getUnlitBlock(final ResourceLocation location) { final TextureStateShard texture = new TextureStateShard(location, false, true); final RenderType.CompositeState state = RenderType.CompositeState.builder() @@ -69,8 +91,40 @@ public abstract class ModRenderType extends RenderType { state); } + public static RenderType getProjector(final DynamicTexture texture) { + final RenderType.CompositeState state = RenderType.CompositeState.builder() + .setShaderState(POSITION_TEX_SHADER) + .setTextureState(new DynamicTextureStateShard(texture)) + .setOutputState(TRANSLUCENT_TARGET) + .setTransparencyState(ADDITIVE_TRANSPARENCY) + .setCullState(NO_CULL) + .createCompositeState(false); + return create( + API.MOD_ID + "/projector", + DefaultVertexFormat.POSITION_TEX, + VertexFormat.Mode.QUADS, + 256, + false, + true, + state); + } + /////////////////////////////////////////////////////////////////// + private static final class DynamicTextureStateShard extends EmptyTextureStateShard { + public DynamicTextureStateShard(final DynamicTexture texture) { + super(() -> { + RenderSystem.enableTexture(); + RenderSystem.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL13.GL_CLAMP_TO_BORDER); + RenderSystem.texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL13.GL_CLAMP_TO_BORDER); + // TODO client side setting +// texture.setFilter(true, false); + RenderSystem.setShaderTexture(0, texture.getId()); + }, () -> { + }); + } + } + private ModRenderType(final String name, final VertexFormat format, final VertexFormat.Mode drawMode, final int bufferSize, final boolean useDelegate, final boolean needsSorting, final Runnable setupTask, final Runnable clearTask) { super(name, format, drawMode, bufferSize, useDelegate, needsSorting, setupTask, clearTask); } diff --git a/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java b/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java new file mode 100644 index 00000000..d80e7467 --- /dev/null +++ b/src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java @@ -0,0 +1,256 @@ +package li.cil.oc2.client.renderer.blockentity; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.RemovalNotification; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Matrix4f; +import com.mojang.math.Quaternion; +import com.mojang.math.Vector3f; +import li.cil.oc2.client.renderer.ModRenderType; +import li.cil.oc2.common.block.ProjectorBlock; +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; + +import java.time.Duration; +import java.util.BitSet; +import java.util.concurrent.ExecutionException; + +public final class ProjectorRenderer implements BlockEntityRenderer { + private static final int LIGHT_COLOR_NEAR = 0x22FFFFFF; + private static final int LIGHT_COLOR_FAR = 0x00FFFFFF; + private static final int LENS_COLOR = 0xDDFFFFFF; + private static final int LED_COLOR = 0xCC6688DD; + + private static final float LENS_RIGHT = 0 + 4 / 16f; + private static final float LENS_LEFT = 1 - 4 / 16f; + private static final float LENS_BOTTOM = 0 + 4 / 16f; + private static final float LENS_TOP = 1 - 4 / 16f; + + private static final Cache textures = CacheBuilder.newBuilder() + .expireAfterAccess(Duration.ofSeconds(5)) + .removalListener(ProjectorRenderer::handleNoLongerRendering) + .build(); + + /////////////////////////////////////////////////////////////////// + + public ProjectorRenderer(final BlockEntityRendererProvider.Context ignored) { + MinecraftForge.EVENT_BUS.addListener(ProjectorRenderer::updateCache); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public boolean shouldRender(final ProjectorBlockEntity projector, final Vec3 position) { + return projector.isProjecting() && BlockEntityRenderer.super.shouldRender(projector, position); + } + + @Override + public void render(final ProjectorBlockEntity projector, final float partialTicks, final PoseStack stack, final MultiBufferSource bufferSource, final int light, final int overlay) { + stack.pushPose(); + + // Align with front face of block. + final Direction blockFacing = projector.getBlockState().getValue(ProjectorBlock.FACING); + final Quaternion rotation = new Quaternion(Vector3f.YN, blockFacing.toYRot(), true); + stack.translate(0.5f, 0, 0.5f); + stack.mulPose(rotation); + + renderProjections(projector, stack, bufferSource); + + renderProjectorLight(stack, bufferSource); + + stack.popPose(); + } + + /////////////////////////////////////////////////////////////////// + + private void renderProjections(final ProjectorBlockEntity projector, final PoseStack stack, final MultiBufferSource bufferSource) { + final ProjectorBlockEntity.VisibilityData visibilityData = projector.getVisibilityData(); + final BitSet[] visibilities = visibilityData.visibilities(); + if (hasNoVisibleTiles(visibilities)) { + return; + } + + final BlockPos projectorPos = projector.getBlockPos(); + final Frustum frustum = new Frustum(stack.last().pose(), RenderSystem.getProjectionMatrix()); + frustum.prepare(projectorPos.getX(), projectorPos.getY(), projectorPos.getZ()); + if (!frustum.isVisible(visibilityData.visibilityBounds())) { + return; + } + + stack.pushPose(); + + stack.translate(0, 0, 0.49); + + final RenderType renderType = getUpdatedRenderType(projector); + final VertexConsumer consumer = bufferSource.getBuffer(renderType); + for (int distance = 0; distance < visibilities.length; distance++) { + final BitSet visibility = visibilities[distance]; + if (!visibility.isEmpty()) { + stack.pushPose(); + + stack.translate(0, 0, distance + 1); + + renderProjection(stack, consumer, ProjectorBlockEntity.getLayerSize(distance), visibility); + + stack.popPose(); + } + } + + stack.popPose(); + } + + private void renderProjection(final PoseStack stack, final VertexConsumer consumer, final ProjectorBlockEntity.LayerSize layerSize, final BitSet visibility) { + final float width = layerSize.width(); + final float height = layerSize.height(); + final int discreteWidth = layerSize.discreteWidth(); + final int discreteHeight = layerSize.discreteHeight(); + final float uOffset = (discreteHeight / height - 1) / 2f; + final float vOffset = (discreteWidth / width - 1) / 2f; + + stack.translate(-width / 2.0, (discreteHeight - height) / 2.0, 0); + stack.scale(width, height, 1); + final Matrix4f matrix = stack.last().pose(); + + for (int index = visibility.nextSetBit(0); index >= 0; index = visibility.nextSetBit(index + 1)) { + final int x = index % discreteWidth; + final int y = index / discreteWidth; + + final float u0 = x / width - vOffset; + final float u1 = (x + 1) / width - vOffset; + final float v0 = y / height - uOffset; + final float v1 = (y + 1) / height - uOffset; + + consumer.vertex(matrix, u0, v0, 0).uv(1 - u0, 1 - v0).endVertex(); + consumer.vertex(matrix, u1, v0, 0).uv(1 - u1, 1 - v0).endVertex(); + consumer.vertex(matrix, u1, v1, 0).uv(1 - u1, 1 - v1).endVertex(); + consumer.vertex(matrix, u0, v1, 0).uv(1 - u0, 1 - v1).endVertex(); + } + } + + private void renderProjectorLight(final PoseStack stack, final MultiBufferSource bufferSource) { + stack.pushPose(); + + stack.translate(-0.5, 0, 0.5); + final VertexConsumer consumer = bufferSource.getBuffer(ModRenderType.getProjectorLight()); + final Matrix4f matrix = stack.last().pose(); + + final float leftFar = 1.25f; + final float rightFar = -0.25f; + final float topFar = 1.5f; + final float bottomFar = 0 + 1 / 16f; + + // Top. + consumer.vertex(matrix, leftFar, topFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // top left far + consumer.vertex(matrix, LENS_LEFT, LENS_TOP, 0).color(LIGHT_COLOR_NEAR).endVertex(); // top left near + consumer.vertex(matrix, LENS_RIGHT, LENS_TOP, 0).color(LIGHT_COLOR_NEAR).endVertex(); // top right near + consumer.vertex(matrix, rightFar, topFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // top right far + + // Bottom. + consumer.vertex(matrix, leftFar, bottomFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // bottom left far + consumer.vertex(matrix, LENS_LEFT, LENS_BOTTOM, 0).color(LIGHT_COLOR_NEAR).endVertex(); // bottom left near + consumer.vertex(matrix, LENS_RIGHT, LENS_BOTTOM, 0).color(LIGHT_COLOR_NEAR).endVertex(); // bottom right near + consumer.vertex(matrix, rightFar, bottomFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // bottom right far + + // Left. + consumer.vertex(matrix, leftFar, topFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // top left far + consumer.vertex(matrix, leftFar, bottomFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // bottom left far + consumer.vertex(matrix, LENS_LEFT, LENS_BOTTOM, 0).color(LIGHT_COLOR_NEAR).endVertex(); // bottom left near + consumer.vertex(matrix, LENS_LEFT, LENS_TOP, 0).color(LIGHT_COLOR_NEAR).endVertex(); // top left near + + // Right. + consumer.vertex(matrix, rightFar, topFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // top right far + consumer.vertex(matrix, LENS_RIGHT, LENS_TOP, 0).color(LIGHT_COLOR_NEAR).endVertex(); // top right near + consumer.vertex(matrix, LENS_RIGHT, LENS_BOTTOM, 0).color(LIGHT_COLOR_NEAR).endVertex(); // bottom right near + consumer.vertex(matrix, rightFar, bottomFar, 1).color(LIGHT_COLOR_FAR).endVertex(); // bottom right far + + renderLens(matrix, consumer); + renderLed(matrix, consumer); + + stack.popPose(); + } + + private void renderLens(final Matrix4f matrix, final VertexConsumer consumer) { + final float lensDepth = -1 / 16f; + consumer.vertex(matrix, LENS_RIGHT, LENS_BOTTOM, lensDepth).color(LENS_COLOR).endVertex(); + consumer.vertex(matrix, LENS_LEFT, LENS_BOTTOM, lensDepth).color(LENS_COLOR).endVertex(); + consumer.vertex(matrix, LENS_LEFT, LENS_TOP, lensDepth).color(LENS_COLOR).endVertex(); + consumer.vertex(matrix, LENS_RIGHT, LENS_TOP, lensDepth).color(LENS_COLOR).endVertex(); + } + + private void renderLed(final Matrix4f matrix, final VertexConsumer consumer) { + final float ledRight = 0 + 7 / 16f; + final float ledLeft = 0 + 9 / 16f; + final float ledBottom = 0 + 3 / 16f; + final float ledTop = 0 + 4 / 16f; + final float ledDepth = -0.75f / 16f; + + consumer.vertex(matrix, ledRight, ledBottom, ledDepth).color(LED_COLOR).endVertex(); + consumer.vertex(matrix, ledLeft, ledBottom, ledDepth).color(LED_COLOR).endVertex(); + consumer.vertex(matrix, ledLeft, ledTop, ledDepth).color(LED_COLOR).endVertex(); + consumer.vertex(matrix, ledRight, ledTop, ledDepth).color(LED_COLOR).endVertex(); + } + + private static boolean hasNoVisibleTiles(final BitSet[] visibilities) { + for (final BitSet visibility : visibilities) { + if (!visibility.isEmpty()) { + return false; + } + } + return true; + } + + private static RenderType getUpdatedRenderType(final ProjectorBlockEntity projector) { + final RenderInfo renderInfo = getRenderInfo(projector); + + final NativeImage image = renderInfo.texture().getPixels(); + assert image != null; + if (projector.applyFramebufferChanges(image::setPixelRGBA)) { + renderInfo.texture().upload(); + } + + return renderInfo.renderType(); + } + + private static RenderInfo getRenderInfo(final ProjectorBlockEntity projector) { + try { + return ProjectorRenderer.textures.get(projector, () -> { + final DynamicTexture texture = new DynamicTexture( + ProjectorVMDevice.WIDTH, + ProjectorVMDevice.HEIGHT, + false + ); + return new RenderInfo(texture, ModRenderType.getProjector(texture)); + } + ); + } catch (final ExecutionException e) { + throw new RuntimeException(e); + } + } + + private static void updateCache(final TickEvent.ClientTickEvent event) { + textures.cleanUp(); + } + + private static void handleNoLongerRendering(final RemovalNotification notification) { + final RenderInfo renderInfo = notification.getValue(); + assert renderInfo != null; + renderInfo.texture().close(); + } + + private record RenderInfo(DynamicTexture texture, RenderType renderType) { } +} diff --git a/src/main/java/li/cil/oc2/common/Config.java b/src/main/java/li/cil/oc2/common/Config.java index 6c3ae36a..e30ea3ad 100644 --- a/src/main/java/li/cil/oc2/common/Config.java +++ b/src/main/java/li/cil/oc2/common/Config.java @@ -20,6 +20,8 @@ public final class Config { @Path("energy.blocks") public static int computerEnergyStorage = 2000; @Path("energy.blocks") public static int chargerEnergyPerTick = 2500; @Path("energy.blocks") public static int chargerEnergyStorage = 10000; + @Path("energy.blocks") public static int projectorEnergyPerTick = 20; + @Path("energy.blocks") public static int projectorEnergyStorage = 2000; @Path("energy.entities") public static int robotEnergyPerTick = 5; @Path("energy.entities") public static int robotEnergyStorage = 750000; @@ -37,10 +39,7 @@ public final class Config { @Path("gameplay") public static ResourceLocation blockOperationsModuleToolTier = TierSortingRegistry.getName(Tiers.DIAMOND); @Path("admin") public static UUID fakePlayerUUID = UUID.fromString("e39dd9a7-514f-4a2d-aa5e-b6030621416d"); - - public static boolean computersUseEnergy() { - return computerEnergyPerTick > 0 && computerEnergyStorage > 0; - } + @Path("admin.blocks") public static int projectorMaxBytesPerTick = 8192; public static boolean robotsUseEnergy() { return robotEnergyPerTick > 0 && robotEnergyStorage > 0; diff --git a/src/main/java/li/cil/oc2/common/Main.java b/src/main/java/li/cil/oc2/common/Main.java index 3dfb4c60..f8a21cd5 100644 --- a/src/main/java/li/cil/oc2/common/Main.java +++ b/src/main/java/li/cil/oc2/common/Main.java @@ -19,6 +19,7 @@ import li.cil.oc2.common.tags.BlockTags; import li.cil.oc2.common.tags.ItemTags; import li.cil.oc2.common.util.RegistryUtils; import li.cil.oc2.common.util.SoundEvents; +import li.cil.oc2.common.vm.provider.DeviceTreeProviders; import li.cil.sedna.Sedna; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.DistExecutor; @@ -30,6 +31,7 @@ public final class Main { public Main() { Ceres.initialize(); Sedna.initialize(); + DeviceTreeProviders.initialize(); Serializers.initialize(); ConfigManager.add(Config::new); diff --git a/src/main/java/li/cil/oc2/common/block/Blocks.java b/src/main/java/li/cil/oc2/common/block/Blocks.java index f8290d6d..1dc33fad 100644 --- a/src/main/java/li/cil/oc2/common/block/Blocks.java +++ b/src/main/java/li/cil/oc2/common/block/Blocks.java @@ -19,6 +19,7 @@ public final class Blocks { public static final RegistryObject NETWORK_CONNECTOR = BLOCKS.register("network_connector", NetworkConnectorBlock::new); public static final RegistryObject NETWORK_HUB = BLOCKS.register("network_hub", NetworkHubBlock::new); public static final RegistryObject REDSTONE_INTERFACE = BLOCKS.register("redstone_interface", RedstoneInterfaceBlock::new); + public static final RegistryObject PROJECTOR = BLOCKS.register("projector", ProjectorBlock::new); /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java b/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java new file mode 100644 index 00000000..60a05c41 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/block/ProjectorBlock.java @@ -0,0 +1,60 @@ +package li.cil.oc2.common.block; + +import li.cil.oc2.common.blockentity.BlockEntities; +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.util.BlockEntityUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.material.Material; + +import javax.annotation.Nullable; + +public final class ProjectorBlock extends HorizontalDirectionalBlock implements EntityBlock { + public ProjectorBlock() { + super(Properties + .of(Material.METAL) + .sound(SoundType.METAL) + .strength(1.5f, 6.0f)); + registerDefaultState(getStateDefinition().any() + .setValue(FACING, Direction.NORTH)); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public BlockEntity newBlockEntity(final BlockPos pos, final BlockState state) { + return BlockEntities.PROJECTOR.get().create(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(final Level level, final BlockState state, final BlockEntityType type) { + if (!level.isClientSide()) { + return BlockEntityUtils.createTicker(type, BlockEntities.PROJECTOR.get(), ProjectorBlockEntity::serverTick); + } else { + return null; + } + } + + @Override + public BlockState getStateForPlacement(final BlockPlaceContext context) { + return super.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + /////////////////////////////////////////////////////////////////// + + protected void createBlockStateDefinition(final StateDefinition.Builder builder) { + builder.add(FACING); + } +} diff --git a/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java b/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java index 65040347..7686406e 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java +++ b/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java @@ -22,6 +22,7 @@ public final class BlockEntities { public static final RegistryObject> DISK_DRIVE = register(Blocks.DISK_DRIVE, DiskDriveBlockEntity::new); public static final RegistryObject> CHARGER = register(Blocks.CHARGER, ChargerBlockEntity::new); public static final RegistryObject> CREATIVE_ENERGY = register(Blocks.CREATIVE_ENERGY, CreativeEnergyBlockEntity::new); + public static final RegistryObject> PROJECTOR = register(Blocks.PROJECTOR, ProjectorBlockEntity::new); /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java index 9ee369e3..b8bff2f2 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java @@ -255,7 +255,7 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina collector.offer(Capabilities.DEVICE_BUS_ELEMENT, busElement); collector.offer(Capabilities.TERMINAL_USER_PROVIDER, this); - if (Config.computersUseEnergy()) { + if (computersUseEnergy()) { collector.offer(Capabilities.ENERGY_STORAGE, energy); } } @@ -305,6 +305,12 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina /////////////////////////////////////////////////////////////////// + public static boolean computersUseEnergy() { + return Config.computerEnergyPerTick > 0 && Config.computerEnergyStorage > 0; + } + + /////////////////////////////////////////////////////////////////// + private final class ComputerItemStackHandlers extends AbstractVMItemStackHandlers { public ComputerItemStackHandlers() { 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.CARD, CARD_SLOTS)); @@ -424,7 +430,7 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina @Override protected boolean consumeEnergy(final int amount, final boolean simulate) { - if (!Config.computersUseEnergy()) { + if (!computersUseEnergy()) { return true; } diff --git a/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java new file mode 100644 index 00000000..67b9006e --- /dev/null +++ b/src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java @@ -0,0 +1,337 @@ +package li.cil.oc2.common.blockentity; + +import li.cil.oc2.common.Config; +import li.cil.oc2.common.block.ProjectorBlock; +import li.cil.oc2.common.bus.device.vm.ProjectorVMDevice; +import li.cil.oc2.common.capabilities.Capabilities; +import li.cil.oc2.common.energy.FixedEnergyStorage; +import li.cil.oc2.common.network.Network; +import li.cil.oc2.common.network.message.ProjectorFrameBufferTileMessage; +import li.cil.oc2.common.network.message.ProjectorStateMessage; +import li.cil.oc2.common.vm.device.SimpleFramebufferDevice; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.BitSet; +import java.util.Optional; + +public final class ProjectorBlockEntity extends ModBlockEntity { + private static final int MAX_RENDER_DISTANCE = 12; + private static final int MAX_WIDTH = MAX_RENDER_DISTANCE + 1; // +1 To make it odd, so we can center. + private static final int MAX_HEIGHT = (MAX_RENDER_DISTANCE * ProjectorVMDevice.HEIGHT / ProjectorVMDevice.WIDTH) + 1; // + 1 To match horizontal margin. + private static final LayerSize[] LAYER_SIZES = computeLayerSizes(); + + private static final String ENERGY_TAG_NAME = "energy"; + private static final String IS_PROJECTING_TAG_NAME = "projecting"; + + /////////////////////////////////////////////////////////////// + + private final ProjectorVMDevice projectorDevice = new ProjectorVMDevice(this); + private boolean isProjecting, hasEnergy; + private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.projectorEnergyStorage); + + // Client only data. + private final BitSet dirtyLines = new BitSet(ProjectorVMDevice.HEIGHT); + @Nullable private ByteBuffer buffer; + private final BitSet[] visibilities = new BitSet[MAX_RENDER_DISTANCE]; + private AABB visibilityBounds; + private AABB renderBounds; + + /////////////////////////////////////////////////////////////// + + public ProjectorBlockEntity(final BlockPos pos, final BlockState state) { + super(BlockEntities.PROJECTOR.get(), pos, state); + + for (int i = 0; i < visibilities.length; i++) { + visibilities[i] = new BitSet(MAX_WIDTH * MAX_HEIGHT); + } + visibilityBounds = super.getRenderBoundingBox(); + + updateRenderBounds(); + } + + /////////////////////////////////////////////////////////////// + + public static void serverTick(final Level ignoredLevel, final BlockPos ignoredPos, final BlockState ignoredState, final ProjectorBlockEntity projector) { + projector.serverTick(); + } + + public ProjectorVMDevice getProjectorDevice() { + return projectorDevice; + } + + public boolean isProjecting() { + return isProjecting; + } + + public void setProjecting(final boolean value) { + isProjecting = value; + + if (!isProjecting) { + buffer = null; + dirtyLines.set(0, ProjectorVMDevice.HEIGHT); + } + + sendRunningState(); + } + + @Override + public CompoundTag getUpdateTag() { + final CompoundTag tag = super.getUpdateTag(); + + tag.putBoolean(IS_PROJECTING_TAG_NAME, isProjecting); + projectorDevice.setAllDirty(); // todo is this good enough to be notified of new client observers? + + return tag; + } + + @Override + public void handleUpdateTag(final CompoundTag tag) { + super.handleUpdateTag(tag); + + setProjecting(tag.getBoolean(IS_PROJECTING_TAG_NAME)); + } + + @Override + protected void saveAdditional(final CompoundTag tag) { + super.saveAdditional(tag); + + tag.put(ENERGY_TAG_NAME, energy.serializeNBT()); + } + + @Override + public void load(final CompoundTag tag) { + super.load(tag); + + energy.deserializeNBT(tag.getCompound(ENERGY_TAG_NAME)); + } + + @Override + public AABB getRenderBoundingBox() { + return renderBounds; + } + + @SuppressWarnings("deprecation") + @Override + public void setBlockState(final BlockState state) { + super.setBlockState(state); + + updateRenderBounds(); + } + + public record LayerSize(float width, float height, int discreteWidth, int discreteHeight) { } + + public static LayerSize getLayerSize(final int distance) { + return LAYER_SIZES[distance]; + } + + public record VisibilityData(AABB visibilityBounds, BitSet[] visibilities) { } + + public VisibilityData getVisibilityData() { + updateVisibilities(); + return new VisibilityData(visibilityBounds, visibilities); + } + + @FunctionalInterface + public interface FramebufferPixelSetter { + void set(final int x, final int y, final int rgba); + } + + public boolean applyFramebufferChanges(final FramebufferPixelSetter setter) { + if (dirtyLines.isEmpty()) { + return false; + } + + final ByteBuffer buffer = getOrCreateBuffer(); + for (int y = dirtyLines.nextSetBit(0); y >= 0; y = dirtyLines.nextSetBit(y + 1)) { + for (int x = 0; x < ProjectorVMDevice.WIDTH; x++) { + final int index = (x + y * ProjectorVMDevice.WIDTH) * Short.BYTES; + final int r5g6b5 = buffer.getShort(index) & 0xFFFF; + setter.set(x, y, ProjectorVMDevice.toRGBA(r5g6b5)); + } + } + + dirtyLines.clear(); + return true; + } + + public void applyFramebufferTile(final SimpleFramebufferDevice.Tile tile) { + tile.apply(ProjectorVMDevice.WIDTH, getOrCreateBuffer()); + dirtyLines.set(tile.startPixelY(), tile.startPixelY() + SimpleFramebufferDevice.TILE_WIDTH); + } + + /////////////////////////////////////////////////////////////// + + @Override + protected void collectCapabilities(final CapabilityCollector collector, @org.jetbrains.annotations.Nullable final Direction direction) { + if (projectorsUseEnergy()) { + collector.offer(Capabilities.ENERGY_STORAGE, energy); + } + } + + /////////////////////////////////////////////////////////////// + + private static boolean projectorsUseEnergy() { + return Config.projectorEnergyStorage > 0 && Config.projectorEnergyPerTick > 0; + } + + private void serverTick() { + if (!isProjecting()) { + return; + } + + if (energy.extractEnergy(Config.projectorEnergyPerTick, true) < Config.projectorEnergyPerTick) { + if (hasEnergy) { + hasEnergy = false; + sendRunningState(); + } + return; + } else if (!hasEnergy) { + hasEnergy = true; + sendRunningState(); + } + + int byteBudget = Config.projectorMaxBytesPerTick; + Optional tile; + while (byteBudget > 0 && (tile = projectorDevice.getNextDirtyTile()).isPresent()) { + final ProjectorFrameBufferTileMessage message = new ProjectorFrameBufferTileMessage(this, tile.get()); + Network.sendToClientsTrackingBlockEntity(message, this); + byteBudget -= SimpleFramebufferDevice.TILE_SIZE_IN_BYTES; + } + } + + private void sendRunningState() { + if (level != null && !level.isClientSide()) { + Network.sendToClientsTrackingBlockEntity(new ProjectorStateMessage(this, isProjecting && hasEnergy), this); + } + } + + private void updateRenderBounds() { + final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING); + final Direction screenUp = Direction.UP; + final Direction screenLeft = blockFacing.getCounterClockWise(); + + final BlockPos projectorPos = getBlockPos(); + final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE); + final BlockPos screenMinPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2); + final BlockPos screenMaxPos = screenBasePos + .relative(screenLeft, MAX_WIDTH / 2) + // -1 for the MAX_HEIGHT padding, -1 for auto-expansion of AABB constructor + .relative(screenUp, MAX_HEIGHT - 2); + + renderBounds = new AABB(getBlockPos()).minmax(new AABB(screenMinPos)).minmax(new AABB(screenMaxPos)); + } + + /** + * Rebuild "stencil buffer" of blocks hit in projection direction, per block layer, up to max distance. + */ + private void updateVisibilities() { + final Direction blockFacing = getBlockState().getValue(ProjectorBlock.FACING); + final Direction screenUp = Direction.UP; + final Direction screenLeft = blockFacing.getCounterClockWise(); + + final BlockPos projectorPos = getBlockPos(); + final BlockPos screenBasePos = projectorPos.relative(blockFacing, MAX_RENDER_DISTANCE + 1); + final BlockPos screenOriginPos = screenBasePos.relative(screenLeft.getOpposite(), MAX_WIDTH / 2); + + final Vec3 toFaceCenter = new Vec3(blockFacing.getOpposite().step()).scale(0.45); + final Vec3 clipStartPos = Vec3.atCenterOf(projectorPos.relative(blockFacing)).subtract(toFaceCenter); + final Vec3 stepOrigin = Vec3.atCenterOf(screenOriginPos).add(toFaceCenter); + final Vec3 upStep = new Vec3(screenUp.step()); + final Vec3 leftStep = new Vec3(screenLeft.step()); + + for (final BitSet bitSet : visibilities) { + bitSet.clear(); + } + + AABB bounds = new AABB(getBlockPos()).minmax(new AABB(getBlockPos().relative(blockFacing))); + + final Level level = getLevel(); + if (level == null) { + return; + } + + for (int y = 0; y < MAX_HEIGHT + 1; y++) { + for (int x = 0; x < MAX_WIDTH; x++) { + final Vec3 clipEndPos = stepOrigin.add(upStep.scale(y)).add(leftStep.scale(x)); + final ClipContext context = new ClipContext(clipStartPos, clipEndPos, ClipContext.Block.VISUAL, ClipContext.Fluid.NONE, null); + final BlockHitResult hit = level.clip(context); + if (hit.getType() == HitResult.Type.MISS) { + continue; + } + + if (hit.getDirection() != blockFacing.getOpposite()) { + continue; + } + + final BlockState blockState = level.getBlockState(hit.getBlockPos()); + if (!blockState.isFaceSturdy(level, hit.getBlockPos(), blockFacing.getOpposite())) { + continue; + } + + final BlockPos delta = hit.getBlockPos().subtract(projectorPos); + final int distance = Math.abs(delta.get(blockFacing.getAxis())) - 2; + if (distance >= visibilities.length) { + continue; + } + + final int globalX = delta.get(screenLeft.getAxis()); + final int globalY = delta.get(screenUp.getAxis()); + + final ProjectorBlockEntity.LayerSize layerSize = getLayerSize(distance); + final int discreteWidth = layerSize.discreteWidth(); + final int discreteHeight = layerSize.discreteHeight(); + + if (globalY < 0 || globalY >= discreteHeight) { + continue; + } + + final int localX = globalX + discreteWidth / 2; + if (localX < 0 || localX >= discreteWidth) { + continue; + } + + bounds = bounds.minmax(new AABB(hit.getBlockPos())); + + final int index = localX + globalY * discreteWidth; + visibilities[distance].set(index); + } + } + + visibilityBounds = bounds; + } + + private ByteBuffer getOrCreateBuffer() { + if (buffer == null) { + buffer = projectorDevice.allocateBuffer(); + } + return buffer; + } + + private static LayerSize[] computeLayerSizes() { + final LayerSize[] layerSizes = new LayerSize[MAX_RENDER_DISTANCE]; + for (int distance = 0; distance < layerSizes.length; distance++) { + final float bufferWidth = ProjectorVMDevice.WIDTH; + final float bufferHeight = ProjectorVMDevice.HEIGHT; + final float ratio = bufferHeight / bufferWidth; + final float width = (MAX_WIDTH - 1) * (float) (distance + 1) / MAX_RENDER_DISTANCE; + final float height = width * ratio; + int discreteWidth = (int) Math.ceil(width); + discreteWidth += 1 - (discreteWidth & 1); // we center, so even values eat up one more + int discreteHeight = (int) Math.ceil(height); // we align, so actual height is correct + if (Math.abs(discreteHeight - height) < 0.001) discreteHeight++; + layerSizes[distance] = new LayerSize(width, height, discreteWidth, discreteHeight); + } + return layerSizes; + } +} diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java index 76cad8e8..1d3eae48 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java @@ -32,6 +32,7 @@ public final class Providers { BLOCK_DEVICE_PROVIDERS.register("item_handler", ItemHandlerBlockDeviceProvider::new); BLOCK_DEVICE_PROVIDERS.register("disk_drive", DiskDriveDeviceProvider::new); + BLOCK_DEVICE_PROVIDERS.register("projector", ProjectorDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("memory", MemoryItemDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("hard_drive", HardDriveItemDeviceProvider::new); diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/ProjectorDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/ProjectorDeviceProvider.java new file mode 100644 index 00000000..ea77f8b4 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/ProjectorDeviceProvider.java @@ -0,0 +1,30 @@ +package li.cil.oc2.common.bus.device.provider.block; + +import li.cil.oc2.api.bus.device.Device; +import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; +import li.cil.oc2.common.block.ProjectorBlock; +import li.cil.oc2.common.blockentity.BlockEntities; +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityDeviceProvider; +import net.minecraft.core.Direction; + +public final class ProjectorDeviceProvider extends AbstractBlockEntityDeviceProvider { + public ProjectorDeviceProvider() { + super(BlockEntities.PROJECTOR.get()); + } + + /////////////////////////////////////////////////////////////// + + @Override + protected Invalidatable getBlockDevice(final BlockDeviceQuery query, final ProjectorBlockEntity blockEntity) { + final Direction blockFacing = blockEntity.getBlockState().getValue(ProjectorBlock.FACING); + if (query.getQuerySide() != blockFacing && + query.getQuerySide() != blockFacing.getClockWise() && + query.getQuerySide() != blockFacing.getCounterClockWise()) { + return Invalidatable.of(blockEntity.getProjectorDevice()); + } else { + return Invalidatable.empty(); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/bus/device/vm/KeyboardVMDevice.java b/src/main/java/li/cil/oc2/common/bus/device/vm/KeyboardVMDevice.java new file mode 100644 index 00000000..bc38d56e --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/vm/KeyboardVMDevice.java @@ -0,0 +1,125 @@ +package li.cil.oc2.common.bus.device.vm; + +import li.cil.oc2.api.bus.device.vm.VMDevice; +import li.cil.oc2.api.bus.device.vm.VMDeviceLoadResult; +import li.cil.oc2.api.bus.device.vm.context.VMContext; +import li.cil.oc2.common.Constants; +import li.cil.oc2.common.bus.device.util.IdentityProxy; +import li.cil.oc2.common.bus.device.util.OptionalAddress; +import li.cil.oc2.common.bus.device.util.OptionalInterrupt; +import li.cil.oc2.common.serialization.NBTSerialization; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.sedna.device.virtio.VirtIOKeyboardDevice; +import net.minecraft.nbt.CompoundTag; + +import javax.annotation.Nullable; + +public final class KeyboardVMDevice extends IdentityProxy implements VMDevice { + private static final String DEVICE_TAG_NAME = "device"; + private static final String ADDRESS_TAG_NAME = "address"; + private static final String INTERRUPT_TAG_NAME = "interrupt"; + + /////////////////////////////////////////////////////////////// + + @Nullable private VirtIOKeyboardDevice device; + + /////////////////////////////////////////////////////////////// + + private final OptionalAddress address = new OptionalAddress(); + private final OptionalInterrupt interrupt = new OptionalInterrupt(); + private CompoundTag deviceTag; + + /////////////////////////////////////////////////////////////// + + public KeyboardVMDevice(final T identity) { + super(identity); + } + + /////////////////////////////////////////////////////////////// + + public void sendKeyEvent(final int keycode, final boolean isDown) { + if (device != null) { + device.sendKeyEvent(keycode, isDown); + } + } + + @Override + public VMDeviceLoadResult mount(final VMContext context) { + if (!allocateDevice(context)) { + return VMDeviceLoadResult.fail(); + } + + assert device != null; + if (!address.claim(context, device)) { + return VMDeviceLoadResult.fail(); + } + + if (interrupt.claim(context)) { + device.getInterrupt().set(interrupt.getAsInt(), context.getInterruptController()); + } else { + return VMDeviceLoadResult.fail(); + } + + context.getEventBus().register(this); + + return VMDeviceLoadResult.success(); + } + + @Override + public void unmount() { + suspend(); + deviceTag = null; + address.clear(); + interrupt.clear(); + } + + @Override + public void suspend() { + device = null; + } + + @Override + public CompoundTag serializeNBT() { + final CompoundTag tag = new CompoundTag(); + + if (device != null) { + deviceTag = NBTSerialization.serialize(device); + } + if (deviceTag != null) { + tag.put(DEVICE_TAG_NAME, deviceTag); + } + if (address.isPresent()) { + tag.putLong(ADDRESS_TAG_NAME, address.getAsLong()); + } + if (interrupt.isPresent()) { + tag.putInt(INTERRUPT_TAG_NAME, interrupt.getAsInt()); + } + + return tag; + } + + @Override + public void deserializeNBT(final CompoundTag tag) { + if (tag.contains(DEVICE_TAG_NAME, NBTTagIds.TAG_COMPOUND)) { + deviceTag = tag.getCompound(DEVICE_TAG_NAME); + } + if (tag.contains(ADDRESS_TAG_NAME, NBTTagIds.TAG_LONG)) { + address.set(tag.getLong(ADDRESS_TAG_NAME)); + } + if (tag.contains(INTERRUPT_TAG_NAME, NBTTagIds.TAG_INT)) { + interrupt.set(tag.getInt(INTERRUPT_TAG_NAME)); + } + } + + /////////////////////////////////////////////////////////////// + + private boolean allocateDevice(final VMContext context) { + if (!context.getMemoryAllocator().claimMemory(Constants.PAGE_SIZE)) { + return false; + } + + device = new VirtIOKeyboardDevice(context.getMemoryMap()); + + return true; + } +} diff --git a/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java b/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java new file mode 100644 index 00000000..2c327ba4 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java @@ -0,0 +1,165 @@ +package li.cil.oc2.common.bus.device.vm; + +import li.cil.oc2.api.bus.device.vm.VMDevice; +import li.cil.oc2.api.bus.device.vm.VMDeviceLoadResult; +import li.cil.oc2.api.bus.device.vm.context.VMContext; +import li.cil.oc2.common.Constants; +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.bus.device.util.IdentityProxy; +import li.cil.oc2.common.bus.device.util.OptionalAddress; +import li.cil.oc2.common.serialization.BlobStorage; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.oc2.common.vm.device.SimpleFramebufferDevice; +import net.minecraft.nbt.CompoundTag; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Optional; +import java.util.UUID; + +public final class ProjectorVMDevice extends IdentityProxy implements VMDevice { + private static final String ADDRESS_TAG_NAME = "address"; + private static final String BLOB_HANDLE_TAG_NAME = "blob"; + + public static final int WIDTH = 640; + public static final int HEIGHT = 480; + + /////////////////////////////////////////////////////////////// + + @Nullable private SimpleFramebufferDevice device; + + /////////////////////////////////////////////////////////////// + + private final OptionalAddress address = new OptionalAddress(); + @Nullable private UUID blobHandle; + + /////////////////////////////////////////////////////////////// + + public ProjectorVMDevice(final ProjectorBlockEntity identity) { + super(identity); + } + + /////////////////////////////////////////////////////////////// + + public static int toRGBA(final int r5g6b5) { + final int r5 = (r5g6b5 >>> 11) & 0b11111; + final int g6 = (r5g6b5 >>> 5) & 0b111111; + final int b5 = r5g6b5 & 0b11111; + final int r = r5 * 255 / 0b11111; + final int g = g6 * 255 / 0b111111; + final int b = b5 * 255 / 0b11111; + return r | (g << 8) | (b << 16) | (0xFF << 24); + } + + public ByteBuffer allocateBuffer() { + return ByteBuffer.allocate(WIDTH * HEIGHT * SimpleFramebufferDevice.STRIDE).order(ByteOrder.LITTLE_ENDIAN); + } + + public void setAllDirty() { + if (device != null) { + device.setAllDirty(); + } + } + + public Optional getNextDirtyTile() { + if (device != null) { + return device.getNextDirtyTile(); + } else { + return Optional.empty(); + } + } + + @Override + public VMDeviceLoadResult mount(final VMContext context) { + if (!allocateDevice(context)) { + return VMDeviceLoadResult.fail(); + } + + assert device != null; + if (!address.claim(context, device)) { + return VMDeviceLoadResult.fail(); + } + + identity.setProjecting(true); + + return VMDeviceLoadResult.success(); + } + + @Override + public void unmount() { + suspend(); + address.clear(); + + identity.setProjecting(false); + } + + @Override + public void suspend() { + closeBlockDevice(); + + if (blobHandle != null) { + BlobStorage.close(blobHandle); + } + } + + @Override + public CompoundTag serializeNBT() { + final CompoundTag tag = new CompoundTag(); + + if (blobHandle != null) { + tag.putUUID(BLOB_HANDLE_TAG_NAME, blobHandle); + } + if (address.isPresent()) { + tag.putLong(ADDRESS_TAG_NAME, address.getAsLong()); + } + + return tag; + } + + @Override + public void deserializeNBT(final CompoundTag tag) { + if (tag.hasUUID(BLOB_HANDLE_TAG_NAME)) { + blobHandle = tag.getUUID(BLOB_HANDLE_TAG_NAME); + } + if (tag.contains(ADDRESS_TAG_NAME, NBTTagIds.TAG_LONG)) { + address.set(tag.getLong(ADDRESS_TAG_NAME)); + } + } + + /////////////////////////////////////////////////////////////// + + private boolean allocateDevice(final VMContext context) { + if (!context.getMemoryAllocator().claimMemory(Constants.PAGE_SIZE)) { + return false; + } + + try { + device = createFrameBufferDevice(); + } catch (final IOException e) { + return false; + } + + return true; + } + + private SimpleFramebufferDevice createFrameBufferDevice() throws IOException { + blobHandle = BlobStorage.validateHandle(blobHandle); + final FileChannel channel = BlobStorage.getOrOpen(blobHandle); + final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, WIDTH * HEIGHT * SimpleFramebufferDevice.STRIDE); + return new SimpleFramebufferDevice(WIDTH, HEIGHT, buffer); + } + + private void closeBlockDevice() { + if (device == null) { + return; + } + + device.close(); + + device = null; + } +} diff --git a/src/main/java/li/cil/oc2/common/bus/device/vm/package-info.java b/src/main/java/li/cil/oc2/common/bus/device/vm/package-info.java new file mode 100644 index 00000000..4522920b --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/vm/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.common.bus.device.vm; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/li/cil/oc2/common/item/Items.java b/src/main/java/li/cil/oc2/common/item/Items.java index c10bbdb8..6e458576 100644 --- a/src/main/java/li/cil/oc2/common/item/Items.java +++ b/src/main/java/li/cil/oc2/common/item/Items.java @@ -29,6 +29,7 @@ public final class Items { public static final RegistryObject NETWORK_CONNECTOR = register(Blocks.NETWORK_CONNECTOR); public static final RegistryObject NETWORK_HUB = register(Blocks.NETWORK_HUB); public static final RegistryObject REDSTONE_INTERFACE = register(Blocks.REDSTONE_INTERFACE); + public static final RegistryObject PROJECTOR = register(Blocks.PROJECTOR); /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/network/Network.java b/src/main/java/li/cil/oc2/common/network/Network.java index 9977b92b..f0610df8 100644 --- a/src/main/java/li/cil/oc2/common/network/Network.java +++ b/src/main/java/li/cil/oc2/common/network/Network.java @@ -73,6 +73,9 @@ public final class Network { registerMessage(NetworkInterfaceCardConfigurationMessage.class, NetworkInterfaceCardConfigurationMessage::new, NetworkDirection.PLAY_TO_SERVER); registerMessage(NetworkTunnelLinkMessage.class, NetworkTunnelLinkMessage::new, NetworkDirection.PLAY_TO_SERVER); + + registerMessage(ProjectorFrameBufferTileMessage.class, ProjectorFrameBufferTileMessage::new, NetworkDirection.PLAY_TO_CLIENT); + registerMessage(ProjectorStateMessage.class, ProjectorStateMessage::new, NetworkDirection.PLAY_TO_CLIENT); } public static void sendToServer(final T message) { diff --git a/src/main/java/li/cil/oc2/common/network/message/ProjectorFrameBufferTileMessage.java b/src/main/java/li/cil/oc2/common/network/message/ProjectorFrameBufferTileMessage.java new file mode 100644 index 00000000..d0ca3558 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/network/message/ProjectorFrameBufferTileMessage.java @@ -0,0 +1,56 @@ +package li.cil.oc2.common.network.message; + +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.network.MessageUtils; +import li.cil.oc2.common.vm.device.SimpleFramebufferDevice; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public final class ProjectorFrameBufferTileMessage extends AbstractMessage { + private BlockPos pos; + private SimpleFramebufferDevice.Tile tile; + + /////////////////////////////////////////////////////////////////// + + public ProjectorFrameBufferTileMessage(final ProjectorBlockEntity projector, final SimpleFramebufferDevice.Tile tile) { + this.pos = projector.getBlockPos(); + this.tile = tile; + } + + public ProjectorFrameBufferTileMessage(final FriendlyByteBuf buffer) { + super(buffer); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public void fromBytes(final FriendlyByteBuf buffer) { + pos = buffer.readBlockPos(); + final int startPixelX = buffer.readVarInt(); + final int startPixelY = buffer.readVarInt(); + final ByteBuffer data = ByteBuffer.allocate(SimpleFramebufferDevice.TILE_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN); + buffer.readBytes(data); + data.flip(); + tile = new SimpleFramebufferDevice.Tile(startPixelX, startPixelY, data); + } + + @Override + public void toBytes(final FriendlyByteBuf buffer) { + buffer.writeBlockPos(pos); + buffer.writeVarInt(tile.startPixelX()); + buffer.writeVarInt(tile.startPixelY()); + buffer.writeBytes(tile.data()); + } + + /////////////////////////////////////////////////////////////////// + + @Override + protected void handleMessage(final NetworkEvent.Context context) { + MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class, + projector -> projector.applyFramebufferTile(tile)); + } +} diff --git a/src/main/java/li/cil/oc2/common/network/message/ProjectorStateMessage.java b/src/main/java/li/cil/oc2/common/network/message/ProjectorStateMessage.java new file mode 100644 index 00000000..cc194d65 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/network/message/ProjectorStateMessage.java @@ -0,0 +1,45 @@ +package li.cil.oc2.common.network.message; + +import li.cil.oc2.common.blockentity.ProjectorBlockEntity; +import li.cil.oc2.common.network.MessageUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +public class ProjectorStateMessage extends AbstractMessage { + private BlockPos pos; + private boolean isProjecting; + + /////////////////////////////////////////////////////////////////// + + public ProjectorStateMessage(final ProjectorBlockEntity projector, final boolean isProjecting) { + this.pos = projector.getBlockPos(); + this.isProjecting = isProjecting; + } + + public ProjectorStateMessage(final FriendlyByteBuf buffer) { + super(buffer); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public void fromBytes(final FriendlyByteBuf buffer) { + pos = buffer.readBlockPos(); + isProjecting = buffer.readBoolean(); + } + + @Override + public void toBytes(final FriendlyByteBuf buffer) { + buffer.writeBlockPos(pos); + buffer.writeBoolean(isProjecting); + } + + /////////////////////////////////////////////////////////////////// + + @Override + protected void handleMessage(final NetworkEvent.Context context) { + MessageUtils.withClientBlockEntityAt(pos, ProjectorBlockEntity.class, + projector -> projector.setProjecting(isProjecting)); + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java b/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java new file mode 100644 index 00000000..40b5bb94 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java @@ -0,0 +1,175 @@ +package li.cil.oc2.common.vm.device; + +import li.cil.sedna.api.device.MemoryMappedDevice; +import li.cil.sedna.api.memory.MemoryAccessException; +import li.cil.sedna.utils.DirectByteBufferUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.BitSet; +import java.util.Optional; + +public final class SimpleFramebufferDevice implements MemoryMappedDevice { + public static final int STRIDE = 2; + public static final int TILE_WIDTH = 32; + public static final int TILE_SIZE = TILE_WIDTH * TILE_WIDTH; + public static final int TILE_SIZE_IN_BYTES = TILE_SIZE * STRIDE; + + public record Tile(int startPixelX, int startPixelY, ByteBuffer data) { + public void apply(final int width, final ByteBuffer buffer) { + if (buffer.capacity() < TILE_SIZE * STRIDE) { + throw new IllegalArgumentException(); + } + + final int startIndex = (startPixelX + startPixelY * width) * STRIDE; + final int rowWidth = width * STRIDE; + final int tileRowBytes = TILE_WIDTH * STRIDE; + buffer.position(startIndex); + for (int i = 0; i < TILE_WIDTH; i++) { + buffer.slice(startIndex + i * rowWidth, tileRowBytes) + .put(data.slice(i * tileRowBytes, tileRowBytes)); + } + } + } + + /////////////////////////////////////////////////////////////// + + private final int width, height; + private final ByteBuffer buffer; + private int length; + private final int tileCount; + private final BitSet dirty; + private int lastDirtyIndex; + + /////////////////////////////////////////////////////////////// + + public SimpleFramebufferDevice(final int width, final int height, final ByteBuffer buffer) { + this.width = width; + this.height = height; + this.length = width * height * STRIDE; + + if (buffer.capacity() < length) { + throw new IllegalArgumentException("Buffer too small."); + } + + this.buffer = buffer.order(ByteOrder.LITTLE_ENDIAN); + this.tileCount = width * height / TILE_SIZE; + this.dirty = new BitSet(tileCount); + setAllDirty(); + } + + /////////////////////////////////////////////////////////////// + + public void close() { + length = 0; + DirectByteBufferUtils.release(buffer); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void setAllDirty() { + synchronized (dirty) { + dirty.clear(); + dirty.flip(0, tileCount); + } + } + + public Optional getNextDirtyTile() { + final int index = dirty.nextSetBit(lastDirtyIndex); + if (index < 0) { + lastDirtyIndex = 0; + return Optional.empty(); + } + + synchronized (dirty) { + dirty.clear(index); + } + + lastDirtyIndex = index + 1; + if (lastDirtyIndex >= tileCount) { + lastDirtyIndex = 0; + } + + final int tileCountX = width / TILE_WIDTH; + final int tileX = index % tileCountX; + final int tileY = index / tileCountX; + final int startPixelX = tileX * TILE_WIDTH; + final int startPixelY = tileY * TILE_WIDTH; + return Optional.of(new Tile(startPixelX, startPixelY, getTileData(startPixelX, startPixelY))); + } + + @Override + public int getLength() { + return length; + } + + @Override + public long load(final int offset, final int sizeLog2) throws MemoryAccessException { + if (offset >= 0 && offset <= length - (1 << sizeLog2)) { + return switch (sizeLog2) { + case 0 -> buffer.get(offset); + case 1 -> buffer.getShort(offset); + case 2 -> buffer.getInt(offset); + case 3 -> buffer.getLong(offset); + default -> throw new IllegalArgumentException(); + }; + } else { + return 0; + } + } + + @Override + public void store(final int offset, final long value, final int sizeLog2) throws MemoryAccessException { + if (offset >= 0 && offset <= length - (1 << sizeLog2)) { + switch (sizeLog2) { + case 0 -> buffer.put(offset, (byte) value); + case 1 -> buffer.putShort(offset, (short) value); + case 2 -> buffer.putInt(offset, (int) value); + case 3 -> buffer.putLong(offset, value); + default -> throw new IllegalArgumentException(); + } + setDirty(offset); + } + } + + /////////////////////////////////////////////////////////////// + + private int getTileIndex(final int offset) { + final int pixelIndex = offset / STRIDE; + final int tileCountX = width / TILE_WIDTH; + final int pixelX = pixelIndex % width; + final int pixelY = pixelIndex / width; + final int tileX = pixelX / TILE_WIDTH; + final int tileY = pixelY / TILE_WIDTH; + return tileX + tileY * tileCountX; + } + + private ByteBuffer getTileData(final int startPixelX, final int startPixelY) { + final ByteBuffer result = ByteBuffer.allocate(TILE_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN); + + final int startIndex = (startPixelX + startPixelY * width) * STRIDE; + final int rowWidth = width * STRIDE; + for (int i = 0; i < TILE_WIDTH; i++) { + result.put(buffer.slice(startIndex + i * rowWidth, TILE_WIDTH * 2)); + } + + result.flip(); + + return result; + } + + private void setDirty(final int offset) { + final int tileIndex = getTileIndex(offset); + if (!dirty.get(tileIndex)) { + synchronized (dirty) { + dirty.set(tileIndex); + } + } + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/provider/DeviceTreeProviders.java b/src/main/java/li/cil/oc2/common/vm/provider/DeviceTreeProviders.java new file mode 100644 index 00000000..f9dee2ca --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/provider/DeviceTreeProviders.java @@ -0,0 +1,10 @@ +package li.cil.oc2.common.vm.provider; + +import li.cil.oc2.common.vm.device.SimpleFramebufferDevice; +import li.cil.sedna.devicetree.DeviceTreeRegistry; + +public final class DeviceTreeProviders { + public static void initialize() { + DeviceTreeRegistry.putProvider(SimpleFramebufferDevice.class, new SimpleFramebufferDeviceProvider()); + } +} diff --git a/src/main/java/li/cil/oc2/common/vm/provider/SimpleFramebufferDeviceProvider.java b/src/main/java/li/cil/oc2/common/vm/provider/SimpleFramebufferDeviceProvider.java new file mode 100644 index 00000000..dc043918 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/vm/provider/SimpleFramebufferDeviceProvider.java @@ -0,0 +1,43 @@ +package li.cil.oc2.common.vm.provider; + +import li.cil.oc2.common.vm.device.SimpleFramebufferDevice; +import li.cil.sedna.api.device.Device; +import li.cil.sedna.api.device.MemoryMappedDevice; +import li.cil.sedna.api.devicetree.DevicePropertyNames; +import li.cil.sedna.api.devicetree.DeviceTree; +import li.cil.sedna.api.devicetree.DeviceTreeProvider; +import li.cil.sedna.api.memory.MappedMemoryRange; +import li.cil.sedna.api.memory.MemoryMap; + +import java.util.Optional; + +public final class SimpleFramebufferDeviceProvider implements DeviceTreeProvider { + @Override + public Optional getName(final Device device) { + return Optional.of("framebuffer"); + } + + @Override + public Optional createNode(final DeviceTree root, final MemoryMap memoryMap, final Device device, final String deviceName) { + final Optional range = memoryMap.getMemoryRange((MemoryMappedDevice) device); + return range.map(r -> { + final DeviceTree chosen = root.find("/chosen"); + chosen.addProp(DevicePropertyNames.RANGES); + + return chosen.getChild(deviceName, r.address()); + }); + } + + @Override + public void visit(final DeviceTree node, final MemoryMap memoryMap, final Device device) { + final SimpleFramebufferDevice fb = (SimpleFramebufferDevice) device; + node + .addProp(DevicePropertyNames.COMPATIBLE, "simple-framebuffer") + .addProp("width", fb.getWidth()) + .addProp("height", fb.getHeight()) + .addProp("stride", fb.getWidth() * SimpleFramebufferDevice.STRIDE) + .addProp("format", "r5g6b5") + .addProp("no-map") + .addProp(DevicePropertyNames.STATUS, "okay"); + } +} diff --git a/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java b/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java index fbf33c02..cdd1e09b 100644 --- a/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java +++ b/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java @@ -23,6 +23,7 @@ public final class ModBlockStateProvider extends BlockStateProvider { private static final ResourceLocation DISK_DRIVE_MODEL = new ResourceLocation(API.MOD_ID, "block/disk_drive"); private static final ResourceLocation NETWORK_CONNECTOR_MODEL = new ResourceLocation(API.MOD_ID, "block/network_connector"); private static final ResourceLocation NETWORK_HUB_MODEL = new ResourceLocation(API.MOD_ID, "block/network_hub"); + private static final ResourceLocation PROJECTOR_MODEL = new ResourceLocation(API.MOD_ID, "block/projector"); private static final ResourceLocation REDSTONE_INTERFACE_MODEL = new ResourceLocation(API.MOD_ID, "block/redstone_interface"); public ModBlockStateProvider(final DataGenerator generator, final ExistingFileHelper existingFileHelper) { @@ -50,6 +51,7 @@ public final class ModBlockStateProvider extends BlockStateProvider { horizontalBlock(Blocks.DISK_DRIVE, Items.DISK_DRIVE, DISK_DRIVE_MODEL); horizontalBlock(Blocks.CHARGER, Items.CHARGER, CHARGER_MODEL); simpleBlock(Blocks.CREATIVE_ENERGY, Items.CREATIVE_ENERGY); + horizontalBlock(Blocks.PROJECTOR, Items.PROJECTOR, PROJECTOR_MODEL); registerCableStates(); } diff --git a/src/main/java/li/cil/oc2/data/ModBlockTagsProvider.java b/src/main/java/li/cil/oc2/data/ModBlockTagsProvider.java index c553e634..b3fd7e8a 100644 --- a/src/main/java/li/cil/oc2/data/ModBlockTagsProvider.java +++ b/src/main/java/li/cil/oc2/data/ModBlockTagsProvider.java @@ -20,7 +20,8 @@ public final class ModBlockTagsProvider extends BlockTagsProvider { tag(DEVICES).add( COMPUTER.get(), REDSTONE_INTERFACE.get(), - DISK_DRIVE.get() + DISK_DRIVE.get(), + PROJECTOR.get() ); tag(CABLES).add( BUS_CABLE.get() @@ -32,7 +33,8 @@ public final class ModBlockTagsProvider extends BlockTagsProvider { NETWORK_HUB.get(), REDSTONE_INTERFACE.get(), DISK_DRIVE.get(), - CHARGER.get() + CHARGER.get(), + PROJECTOR.get() ); } } diff --git a/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java b/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java index be4167a6..a61d26e0 100644 --- a/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java +++ b/src/main/java/li/cil/oc2/data/ModItemTagsProvider.java @@ -81,7 +81,8 @@ public final class ModItemTagsProvider extends ItemTagsProvider { Items.NETWORK_INTERFACE_CARD.get(), Items.DISK_DRIVE.get(), Items.NETWORK_TUNNEL_CARD.get(), - Items.NETWORK_TUNNEL_MODULE.get() + Items.NETWORK_TUNNEL_MODULE.get(), + Items.PROJECTOR.get() ); } } diff --git a/src/main/java/li/cil/oc2/data/ModLootTableProvider.java b/src/main/java/li/cil/oc2/data/ModLootTableProvider.java index 6381f484..c3a80ef7 100644 --- a/src/main/java/li/cil/oc2/data/ModLootTableProvider.java +++ b/src/main/java/li/cil/oc2/data/ModLootTableProvider.java @@ -54,6 +54,7 @@ public final class ModLootTableProvider extends LootTableProvider { dropSelf(Blocks.NETWORK_HUB.get()); dropSelf(Blocks.DISK_DRIVE.get()); dropSelf(Blocks.CHARGER.get()); + dropSelf(Blocks.PROJECTOR.get()); add(Blocks.COMPUTER.get(), ModBlockLootTables::droppingWithInventory); } diff --git a/src/main/resources/assets/oc2/blockstates/projector.json b/src/main/resources/assets/oc2/blockstates/projector.json new file mode 100644 index 00000000..ccdc265e --- /dev/null +++ b/src/main/resources/assets/oc2/blockstates/projector.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "oc2:block/projector" + }, + "facing=south": { + "model": "oc2:block/projector", + "y": 180 + }, + "facing=west": { + "model": "oc2:block/projector", + "y": 270 + }, + "facing=east": { + "model": "oc2:block/projector", + "y": 90 + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/oc2/lang/en_us.json b/src/main/resources/assets/oc2/lang/en_us.json index 6784922a..56652273 100644 --- a/src/main/resources/assets/oc2/lang/en_us.json +++ b/src/main/resources/assets/oc2/lang/en_us.json @@ -18,6 +18,8 @@ "block.oc2.charger.desc": "Charges entities and items in containers on top of it.", "block.oc2.creative_energy": "Infinite Energy Cube", "block.oc2.creative_energy.desc": "Provides unlimited energy to adjacent blocks. Intended for testing.", + "block.oc2.projector": "Projector", + "block.oc2.projector.desc": "Projects to be displayed content onto surfaces in front of it.", "item.oc2.wrench": "Scrench", "item.oc2.wrench.desc": "Configures devices and dismantles them (while sneaking).", diff --git a/src/main/resources/assets/oc2/models/block/projector.json b/src/main/resources/assets/oc2/models/block/projector.json new file mode 100644 index 00000000..98cdaba0 --- /dev/null +++ b/src/main/resources/assets/oc2/models/block/projector.json @@ -0,0 +1 @@ +{"parent":"block/block","textures":{"atlas0":"oc2:block/projector/projector_atlas0","atlas1":"oc2:block/projector/projector_atlas1","atlas2":"oc2:block/projector/projector_atlas2","atlas3":"oc2:block/projector/projector_atlas3","atlas4":"oc2:block/projector/projector_atlas4","atlas5":"oc2:block/projector/projector_atlas5","atlas6":"oc2:block/projector/projector_atlas6","particle":"#atlas0"},"elements":[{"from":[0,0,10],"to":[16,1,16],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[8.0,11.0,11.0,11.5]},"west":{"texture":"atlas3","cullface":"west","uv":[11.0,11.0,14.0,11.5]},"down":{"texture":"atlas0","cullface":"down","uv":[0.0,0.0,8.0,3.0]},"north":{"texture":"atlas0","uv":[0.0,3.0,8.0,3.5]},"south":{"texture":"atlas0","cullface":"south","uv":[0.0,3.5,8.0,4.0]}}},{"from":[0,0,6],"to":[6,1,10],"faces":{"east":{"texture":"atlas3","uv":[14.0,11.0,16.0,11.5]},"west":{"texture":"atlas3","cullface":"west","uv":[14.0,2.0,16.0,2.5]},"down":{"texture":"atlas5","cullface":"down","uv":[0.0,14.0,3.0,16.0]},"north":{"texture":"atlas5","uv":[3.0,14.0,6.0,14.5]},"south":{"texture":"atlas5","uv":[3.0,14.5,6.0,15.0]}}},{"from":[10,0,6],"to":[16,1,10],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[14.0,2.5,16.0,3.0]},"west":{"texture":"atlas3","uv":[14.0,3.0,16.0,3.5]},"down":{"texture":"atlas5","cullface":"down","uv":[7.0,3.5,10.0,5.5]},"north":{"texture":"atlas5","uv":[3.0,15.0,6.0,15.5]},"south":{"texture":"atlas5","uv":[3.0,15.5,6.0,16.0]}}},{"from":[0,0,0],"to":[16,1,6],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[6.0,15.5,9.0,16.0]},"west":{"texture":"atlas5","cullface":"west","uv":[9.0,15.5,12.0,16.0]},"down":{"texture":"atlas0","cullface":"down","uv":[0.0,4.0,8.0,7.0]},"north":{"texture":"atlas0","cullface":"north","uv":[0.0,7.0,8.0,7.5]},"south":{"texture":"atlas0","uv":[0.0,7.5,8.0,8.0]}}},{"from":[0,1,0],"to":[16,2,16],"faces":{"east":{"texture":"atlas0","cullface":"east","uv":[0.0,8.0,8.0,8.5]},"west":{"texture":"atlas0","cullface":"west","uv":[0.0,8.5,8.0,9.0]},"up":{"texture":"atlas1","uv":[0.0,0.0,8.0,8.0]},"down":{"texture":"atlas1","uv":[0.0,8.0,8.0,16.0]},"north":{"texture":"atlas0","cullface":"north","uv":[0.0,9.0,8.0,9.5]},"south":{"texture":"atlas0","cullface":"south","uv":[0.0,9.5,8.0,10.0]}}},{"from":[0,2,14],"to":[16,6,16],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[15.0,0.0,16.0,2.0]},"west":{"texture":"atlas4","cullface":"west","uv":[15.0,4.0,16.0,6.0]},"up":{"texture":"atlas0","uv":[0.0,10.0,8.0,11.0]},"north":{"texture":"atlas0","uv":[0.0,11.0,8.0,13.0]},"south":{"texture":"atlas0","cullface":"south","uv":[0.0,13.0,8.0,15.0]}}},{"from":[1,2,2],"to":[15,6,14],"faces":{"east":{"texture":"atlas3","uv":[8.0,2.0,14.0,4.0]},"west":{"texture":"atlas4","uv":[8.0,14.0,14.0,16.0]},"up":{"texture":"atlas4","uv":[8.0,8.0,15.0,14.0]},"down":{"texture":"atlas4","uv":[8.0,0.0,15.0,6.0]},"north":{"texture":"atlas3","uv":[8.0,0.0,15.0,2.0]}}},{"from":[0,2,0],"to":[16,3,2],"faces":{"east":{"texture":"atlas2","cullface":"east","uv":[15.0,15.5,16.0,16.0]},"west":{"texture":"atlas2","cullface":"west","uv":[15.0,15.0,16.0,15.5]},"up":{"texture":"atlas0","uv":[0.0,15.0,8.0,16.0]},"north":{"texture":"atlas0","cullface":"north","uv":[8.0,15.0,16.0,15.5]},"south":{"texture":"atlas0","uv":[8.0,15.5,16.0,16.0]}}},{"from":[0,3,1],"to":[16,4,2],"faces":{"east":{"texture":"atlas2","cullface":"east","uv":[15.5,3.0,16.0,3.5]},"west":{"texture":"atlas2","cullface":"west","uv":[15.5,2.5,16.0,3.0]},"up":{"texture":"atlas0","uv":[8.0,13.0,16.0,13.5]},"north":{"texture":"atlas0","uv":[8.0,13.5,16.0,14.0]},"south":{"texture":"atlas0","uv":[8.0,14.0,16.0,14.5]}}},{"from":[0,3,0],"to":[7,4,1],"faces":{"east":{"texture":"atlas3","uv":[15.5,15.5,16.0,16.0]},"west":{"texture":"atlas3","cullface":"west","uv":[15.5,15.0,16.0,15.5]},"up":{"texture":"atlas3","uv":[12.0,15.5,15.5,16.0]},"north":{"texture":"atlas3","cullface":"north","uv":[4.0,15.0,7.5,15.5]}}},{"from":[9,3,0],"to":[16,4,1],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[13.0,3.0,13.5,3.5]},"west":{"texture":"atlas5","uv":[13.5,3.0,14.0,3.5]},"up":{"texture":"atlas3","uv":[7.5,15.0,11.0,15.5]},"north":{"texture":"atlas3","cullface":"north","uv":[11.0,15.0,14.5,15.5]}}},{"from":[0,4,0],"to":[4,12,2],"faces":{"east":{"texture":"atlas4","uv":[15.0,0.0,16.0,4.0]},"west":{"texture":"atlas4","cullface":"west","uv":[15.0,8.0,16.0,12.0]},"up":{"texture":"atlas4","uv":[14.0,14.0,16.0,15.0]},"down":{"texture":"atlas4","uv":[14.0,15.0,16.0,16.0]},"north":{"texture":"atlas6","cullface":"north","uv":[0.0,0.0,2.0,4.0]},"south":{"texture":"atlas6","uv":[2.0,0.0,4.0,4.0]}}},{"from":[6,4,0],"to":[10,5,2],"faces":{"east":{"texture":"atlas2","uv":[15.0,14.5,16.0,15.0]},"west":{"texture":"atlas2","uv":[15.0,14.0,16.0,14.5]},"up":{"texture":"atlas5","uv":[14.0,10.5,16.0,11.5]},"down":{"texture":"atlas5","uv":[14.0,11.5,16.0,12.5]},"north":{"texture":"atlas3","cullface":"north","uv":[14.0,3.5,16.0,4.0]}}},{"from":[12,4,0],"to":[16,12,2],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[4.0,0.0,5.0,4.0]},"west":{"texture":"atlas6","uv":[5.0,0.0,6.0,4.0]},"up":{"texture":"atlas5","uv":[14.0,12.5,16.0,13.5]},"down":{"texture":"atlas5","uv":[14.0,9.5,16.0,10.5]},"north":{"texture":"atlas6","cullface":"north","uv":[6.0,0.0,8.0,4.0]},"south":{"texture":"atlas6","uv":[8.0,0.0,10.0,4.0]}}},{"from":[0,6,15],"to":[2,7,16],"faces":{"east":{"texture":"atlas5","uv":[14.0,3.0,14.5,3.5]},"west":{"texture":"atlas5","cullface":"west","uv":[14.5,3.0,15.0,3.5]},"up":{"texture":"atlas2","uv":[15.0,6.5,16.0,7.0]},"down":{"texture":"atlas2","uv":[15.0,6.0,16.0,6.5]},"south":{"texture":"atlas2","cullface":"south","uv":[14.5,3.0,15.5,3.5]}}},{"from":[5,6,15],"to":[6,7,16],"faces":{"east":{"texture":"atlas5","uv":[15.0,3.0,15.5,3.5]},"west":{"texture":"atlas5","uv":[15.5,3.0,16.0,3.5]},"up":{"texture":"atlas5","uv":[14.0,2.5,14.5,3.0]},"down":{"texture":"atlas5","uv":[14.5,2.5,15.0,3.0]},"south":{"texture":"atlas5","cullface":"south","uv":[15.0,2.5,15.5,3.0]}}},{"from":[10,6,15],"to":[11,7,16],"faces":{"east":{"texture":"atlas5","uv":[15.5,2.5,16.0,3.0]},"west":{"texture":"atlas5","uv":[14.0,2.0,14.5,2.5]},"up":{"texture":"atlas5","uv":[14.5,2.0,15.0,2.5]},"down":{"texture":"atlas5","uv":[15.0,2.0,15.5,2.5]},"south":{"texture":"atlas5","cullface":"south","uv":[15.5,2.0,16.0,2.5]}}},{"from":[14,6,15],"to":[16,7,16],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[14.0,1.5,14.5,2.0]},"west":{"texture":"atlas5","uv":[14.5,1.5,15.0,2.0]},"up":{"texture":"atlas2","uv":[14.5,2.5,15.5,3.0]},"down":{"texture":"atlas3","uv":[14.5,15.0,15.5,15.5]},"south":{"texture":"atlas3","cullface":"south","uv":[15.0,14.5,16.0,15.0]}}},{"from":[0,6,2],"to":[16,7,15],"faces":{"east":{"texture":"atlas2","cullface":"east","uv":[8.0,2.5,14.5,3.0]},"west":{"texture":"atlas2","cullface":"west","uv":[8.0,3.0,14.5,3.5]},"up":{"texture":"atlas1","uv":[8.0,8.0,16.0,14.5]},"down":{"texture":"atlas1","uv":[8.0,0.0,16.0,6.5]},"north":{"texture":"atlas0","uv":[8.0,14.5,16.0,15.0]},"south":{"texture":"atlas0","uv":[8.0,11.0,16.0,11.5]}}},{"from":[0,7,15],"to":[6,8,16],"faces":{"east":{"texture":"atlas5","uv":[15.0,1.5,15.5,2.0]},"west":{"texture":"atlas5","cullface":"west","uv":[15.5,1.5,16.0,2.0]},"up":{"texture":"atlas5","uv":[12.0,15.5,15.0,16.0]},"down":{"texture":"atlas5","uv":[6.0,15.0,9.0,15.5]},"south":{"texture":"atlas5","cullface":"south","uv":[9.0,15.0,12.0,15.5]}}},{"from":[10,7,15],"to":[16,8,16],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[15.0,1.0,15.5,1.5]},"west":{"texture":"atlas5","uv":[15.5,1.0,16.0,1.5]},"up":{"texture":"atlas5","uv":[12.0,15.0,15.0,15.5]},"down":{"texture":"atlas5","uv":[6.0,14.5,9.0,15.0]},"south":{"texture":"atlas5","cullface":"south","uv":[9.0,14.5,12.0,15.0]}}},{"from":[0,7,8],"to":[16,11,15],"faces":{"east":{"texture":"atlas4","cullface":"east","uv":[8.0,6.0,11.5,8.0]},"west":{"texture":"atlas4","cullface":"west","uv":[11.5,6.0,15.0,8.0]},"up":{"texture":"atlas2","uv":[0.0,0.0,8.0,3.5]},"down":{"texture":"atlas2","uv":[0.0,3.5,8.0,7.0]},"north":{"texture":"atlas0","uv":[8.0,4.0,16.0,6.0]},"south":{"texture":"atlas0","uv":[8.0,0.0,16.0,2.0]}}},{"from":[1,7,7],"to":[15,14,8],"faces":{"east":{"texture":"atlas5","uv":[7.0,0.0,7.5,3.5]},"west":{"texture":"atlas5","uv":[7.5,0.0,8.0,3.5]},"up":{"texture":"atlas2","uv":[8.0,15.5,15.0,16.0]},"down":{"texture":"atlas2","uv":[8.0,15.0,15.0,15.5]},"north":{"texture":"atlas5","uv":[0.0,0.0,7.0,3.5]},"south":{"texture":"atlas5","uv":[0.0,3.5,7.0,7.0]}}},{"from":[0,7,6],"to":[16,14,7],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[8.0,0.0,8.5,3.5]},"west":{"texture":"atlas5","cullface":"west","uv":[8.5,0.0,9.0,3.5]},"up":{"texture":"atlas0","uv":[8.0,11.5,16.0,12.0]},"down":{"texture":"atlas0","uv":[8.0,12.0,16.0,12.5]},"north":{"texture":"atlas2","uv":[0.0,7.0,8.0,10.5]},"south":{"texture":"atlas2","uv":[0.0,10.5,8.0,14.0]}}},{"from":[1,7,5],"to":[15,14,6],"faces":{"east":{"texture":"atlas5","uv":[9.0,0.0,9.5,3.5]},"west":{"texture":"atlas5","uv":[9.5,0.0,10.0,3.5]},"up":{"texture":"atlas2","uv":[8.0,14.5,15.0,15.0]},"down":{"texture":"atlas2","uv":[8.0,14.0,15.0,14.5]},"north":{"texture":"atlas5","uv":[0.0,7.0,7.0,10.5]},"south":{"texture":"atlas5","uv":[0.0,10.5,7.0,14.0]}}},{"from":[0,7,4],"to":[16,14,5],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[10.0,0.0,10.5,3.5]},"west":{"texture":"atlas5","cullface":"west","uv":[10.5,0.0,11.0,3.5]},"up":{"texture":"atlas0","uv":[8.0,12.5,16.0,13.0]},"down":{"texture":"atlas0","uv":[8.0,10.0,16.0,10.5]},"north":{"texture":"atlas2","uv":[8.0,10.5,16.0,14.0]},"south":{"texture":"atlas2","uv":[8.0,7.0,16.0,10.5]}}},{"from":[1,7,3],"to":[15,14,4],"faces":{"east":{"texture":"atlas5","uv":[11.0,0.0,11.5,3.5]},"west":{"texture":"atlas5","uv":[11.5,0.0,12.0,3.5]},"up":{"texture":"atlas2","uv":[8.0,6.0,15.0,6.5]},"down":{"texture":"atlas2","uv":[8.0,6.5,15.0,7.0]},"north":{"texture":"atlas5","uv":[7.0,10.5,14.0,14.0]},"south":{"texture":"atlas5","uv":[7.0,7.0,14.0,10.5]}}},{"from":[0,7,2],"to":[16,12,3],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[14.0,7.0,14.5,9.5]},"west":{"texture":"atlas5","cullface":"west","uv":[14.5,7.0,15.0,9.5]},"up":{"texture":"atlas0","uv":[8.0,10.5,16.0,11.0]},"down":{"texture":"atlas0","uv":[8.0,9.5,16.0,10.0]},"north":{"texture":"atlas2","uv":[8.0,3.5,16.0,6.0]},"south":{"texture":"atlas2","uv":[8.0,0.0,16.0,2.5]}}},{"from":[0,8,15],"to":[2,9,16],"faces":{"east":{"texture":"atlas5","uv":[15.0,0.5,15.5,1.0]},"west":{"texture":"atlas5","cullface":"west","uv":[15.5,0.5,16.0,1.0]},"up":{"texture":"atlas4","uv":[15.0,12.0,16.0,12.5]},"down":{"texture":"atlas4","uv":[15.0,12.5,16.0,13.0]},"south":{"texture":"atlas4","cullface":"south","uv":[15.0,13.0,16.0,13.5]}}},{"from":[5,8,15],"to":[6,9,16],"faces":{"east":{"texture":"atlas5","uv":[15.0,0.0,15.5,0.5]},"west":{"texture":"atlas5","uv":[15.5,0.0,16.0,0.5]},"up":{"texture":"atlas6","uv":[0.0,6.0,0.5,6.5]},"down":{"texture":"atlas6","uv":[0.0,6.5,0.5,7.0]},"south":{"texture":"atlas6","cullface":"south","uv":[0.0,7.0,0.5,7.5]}}},{"from":[10,8,15],"to":[11,9,16],"faces":{"east":{"texture":"atlas6","uv":[0.0,7.5,0.5,8.0]},"west":{"texture":"atlas6","uv":[0.0,8.0,0.5,8.5]},"up":{"texture":"atlas6","uv":[0.0,8.5,0.5,9.0]},"down":{"texture":"atlas6","uv":[0.0,9.0,0.5,9.5]},"south":{"texture":"atlas6","cullface":"south","uv":[0.0,9.5,0.5,10.0]}}},{"from":[14,8,15],"to":[16,9,16],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[0.0,10.0,0.5,10.5]},"west":{"texture":"atlas6","uv":[0.0,10.5,0.5,11.0]},"up":{"texture":"atlas4","uv":[15.0,13.5,16.0,14.0]},"down":{"texture":"atlas4","uv":[15.0,6.0,16.0,6.5]},"south":{"texture":"atlas4","cullface":"south","uv":[15.0,6.5,16.0,7.0]}}},{"from":[0,9,15],"to":[6,10,16],"faces":{"east":{"texture":"atlas6","uv":[0.0,11.0,0.5,11.5]},"west":{"texture":"atlas6","cullface":"west","uv":[0.0,11.5,0.5,12.0]},"up":{"texture":"atlas5","uv":[12.0,14.5,15.0,15.0]},"down":{"texture":"atlas5","uv":[6.0,14.0,9.0,14.5]},"south":{"texture":"atlas5","cullface":"south","uv":[9.0,14.0,12.0,14.5]}}},{"from":[10,9,15],"to":[16,10,16],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[0.0,12.0,0.5,12.5]},"west":{"texture":"atlas6","uv":[0.0,12.5,0.5,13.0]},"up":{"texture":"atlas5","uv":[12.0,14.0,15.0,14.5]},"down":{"texture":"atlas5","uv":[7.0,5.5,10.0,6.0]},"south":{"texture":"atlas5","cullface":"south","uv":[7.0,6.0,10.0,6.5]}}},{"from":[0,10,15],"to":[2,11,16],"faces":{"east":{"texture":"atlas6","uv":[0.0,13.0,0.5,13.5]},"west":{"texture":"atlas6","cullface":"west","uv":[0.0,13.5,0.5,14.0]},"up":{"texture":"atlas4","uv":[15.0,7.0,16.0,7.5]},"down":{"texture":"atlas4","uv":[15.0,7.5,16.0,8.0]},"south":{"texture":"atlas5","cullface":"south","uv":[15.0,15.5,16.0,16.0]}}},{"from":[5,10,15],"to":[11,11,16],"faces":{"east":{"texture":"atlas6","uv":[0.0,14.0,0.5,14.5]},"west":{"texture":"atlas6","uv":[0.0,14.5,0.5,15.0]},"down":{"texture":"atlas5","uv":[7.0,6.5,10.0,7.0]},"south":{"texture":"atlas5","cullface":"south","uv":[10.0,6.5,13.0,7.0]}}},{"from":[14,10,15],"to":[16,11,16],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[0.0,15.0,0.5,15.5]},"west":{"texture":"atlas6","uv":[0.0,15.5,0.5,16.0]},"up":{"texture":"atlas5","uv":[15.0,15.0,16.0,15.5]},"down":{"texture":"atlas5","uv":[15.0,14.5,16.0,15.0]},"south":{"texture":"atlas5","cullface":"south","uv":[15.0,14.0,16.0,14.5]}}},{"from":[0,11,8],"to":[16,12,16],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[0.0,15.0,4.0,15.5]},"west":{"texture":"atlas3","cullface":"west","uv":[0.0,15.5,4.0,16.0]},"up":{"texture":"atlas3","uv":[0.0,0.0,8.0,4.0]},"down":{"texture":"atlas3","uv":[0.0,4.0,8.0,8.0]},"north":{"texture":"atlas0","uv":[8.0,9.0,16.0,9.5]},"south":{"texture":"atlas0","cullface":"south","uv":[8.0,8.5,16.0,9.0]}}},{"from":[0,12,15],"to":[2,13,16],"faces":{"east":{"texture":"atlas6","uv":[0.5,15.5,1.0,16.0]},"west":{"texture":"atlas6","cullface":"west","uv":[1.0,15.5,1.5,16.0]},"up":{"texture":"atlas5","uv":[15.0,7.0,16.0,7.5]},"down":{"texture":"atlas5","uv":[15.0,7.5,16.0,8.0]},"south":{"texture":"atlas5","cullface":"south","uv":[15.0,8.0,16.0,8.5]}}},{"from":[14,12,15],"to":[16,13,16],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[1.5,15.5,2.0,16.0]},"west":{"texture":"atlas6","uv":[2.0,15.5,2.5,16.0]},"up":{"texture":"atlas5","uv":[15.0,8.5,16.0,9.0]},"down":{"texture":"atlas5","uv":[15.0,9.0,16.0,9.5]},"south":{"texture":"atlas5","cullface":"south","uv":[12.0,3.0,13.0,3.5]}}},{"from":[0,12,8],"to":[16,13,15],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[8.0,14.5,11.5,15.0]},"west":{"texture":"atlas3","cullface":"west","uv":[11.5,14.5,15.0,15.0]},"up":{"texture":"atlas3","uv":[0.0,8.0,8.0,11.5]},"down":{"texture":"atlas3","uv":[0.0,11.5,8.0,15.0]},"north":{"texture":"atlas0","uv":[8.0,8.0,16.0,8.5]},"south":{"texture":"atlas0","uv":[8.0,7.5,16.0,8.0]}}},{"from":[0,12,0],"to":[16,14,3],"faces":{"east":{"texture":"atlas6","cullface":"east","uv":[0.0,4.0,1.5,5.0]},"west":{"texture":"atlas6","cullface":"west","uv":[0.0,5.0,1.5,6.0]},"down":{"texture":"atlas1","uv":[8.0,14.5,16.0,16.0]},"north":{"texture":"atlas0","cullface":"north","uv":[8.0,6.0,16.0,7.0]},"south":{"texture":"atlas0","uv":[8.0,2.0,16.0,3.0]}}},{"from":[0,13,8],"to":[16,14,16],"faces":{"east":{"texture":"atlas3","cullface":"east","uv":[4.0,15.5,8.0,16.0]},"west":{"texture":"atlas3","cullface":"west","uv":[8.0,15.5,12.0,16.0]},"down":{"texture":"atlas3","uv":[8.0,4.0,16.0,8.0]},"north":{"texture":"atlas0","uv":[8.0,7.0,16.0,7.5]},"south":{"texture":"atlas0","cullface":"south","uv":[8.0,3.5,16.0,4.0]}}},{"from":[0,14,0],"to":[16,15,16],"faces":{"east":{"texture":"atlas0","cullface":"east","uv":[8.0,3.0,16.0,3.5]},"west":{"texture":"atlas1","cullface":"west","uv":[8.0,6.5,16.0,7.0]},"up":{"texture":"atlas4","uv":[0.0,0.0,8.0,8.0]},"down":{"texture":"atlas4","uv":[0.0,8.0,8.0,16.0]},"north":{"texture":"atlas1","cullface":"north","uv":[8.0,7.0,16.0,7.5]},"south":{"texture":"atlas1","cullface":"south","uv":[8.0,7.5,16.0,8.0]}}},{"from":[0,15,10],"to":[16,16,16],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[13.0,6.5,16.0,7.0]},"west":{"texture":"atlas5","cullface":"west","uv":[10.0,6.0,13.0,6.5]},"up":{"texture":"atlas3","cullface":"up","uv":[8.0,11.5,16.0,14.5]},"north":{"texture":"atlas2","uv":[0.0,14.0,8.0,14.5]},"south":{"texture":"atlas2","cullface":"south","uv":[0.0,14.5,8.0,15.0]}}},{"from":[0,15,6],"to":[6,16,10],"faces":{"east":{"texture":"atlas5","uv":[14.0,13.5,16.0,14.0]},"west":{"texture":"atlas5","cullface":"west","uv":[12.0,1.5,14.0,2.0]},"up":{"texture":"atlas5","cullface":"up","uv":[10.0,3.5,13.0,5.5]},"north":{"texture":"atlas5","uv":[13.0,6.0,16.0,6.5]},"south":{"texture":"atlas5","uv":[10.0,5.5,13.0,6.0]}}},{"from":[10,15,6],"to":[16,16,10],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[12.0,2.0,14.0,2.5]},"west":{"texture":"atlas5","uv":[12.0,2.5,14.0,3.0]},"up":{"texture":"atlas5","cullface":"up","uv":[13.0,3.5,16.0,5.5]},"north":{"texture":"atlas5","uv":[13.0,5.5,16.0,6.0]},"south":{"texture":"atlas5","uv":[12.0,0.0,15.0,0.5]}}},{"from":[0,15,0],"to":[16,16,6],"faces":{"east":{"texture":"atlas5","cullface":"east","uv":[12.0,0.5,15.0,1.0]},"west":{"texture":"atlas5","cullface":"west","uv":[12.0,1.0,15.0,1.5]},"up":{"texture":"atlas3","cullface":"up","uv":[8.0,8.0,16.0,11.0]},"north":{"texture":"atlas2","cullface":"north","uv":[0.0,15.0,8.0,15.5]},"south":{"texture":"atlas2","uv":[0.0,15.5,8.0,16.0]}}}]} \ No newline at end of file diff --git a/src/main/resources/assets/oc2/models/item/projector.json b/src/main/resources/assets/oc2/models/item/projector.json new file mode 100644 index 00000000..f40a65a6 --- /dev/null +++ b/src/main/resources/assets/oc2/models/item/projector.json @@ -0,0 +1,3 @@ +{ + "parent": "oc2:block/projector" +} \ No newline at end of file diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas0.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas0.png new file mode 100644 index 00000000..ed29aad0 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas0.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas1.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas1.png new file mode 100644 index 00000000..4d0e3ff0 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas1.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas2.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas2.png new file mode 100644 index 00000000..4e185496 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas2.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas3.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas3.png new file mode 100644 index 00000000..5ccfee3a Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas3.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas4.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas4.png new file mode 100644 index 00000000..21c735a7 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas4.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas5.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas5.png new file mode 100644 index 00000000..f9ad0cdc Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas5.png differ diff --git a/src/main/resources/assets/oc2/textures/block/projector/projector_atlas6.png b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas6.png new file mode 100644 index 00000000..65320de0 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/block/projector/projector_atlas6.png differ diff --git a/src/main/resources/data/oc2/loot_tables/blocks/projector.json b/src/main/resources/data/oc2/loot_tables/blocks/projector.json new file mode 100644 index 00000000..8d17d357 --- /dev/null +++ b/src/main/resources/data/oc2/loot_tables/blocks/projector.json @@ -0,0 +1,20 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1.0, + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:item", + "name": "oc2:projector" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/data/oc2/tags/blocks/devices.json b/src/main/resources/data/oc2/tags/blocks/devices.json index ada1907c..70f6bc35 100644 --- a/src/main/resources/data/oc2/tags/blocks/devices.json +++ b/src/main/resources/data/oc2/tags/blocks/devices.json @@ -3,6 +3,7 @@ "values": [ "oc2:computer", "oc2:redstone_interface", - "oc2:disk_drive" + "oc2:disk_drive", + "oc2:projector" ] -} \ No newline at end of file +} diff --git a/src/main/resources/data/oc2/tags/blocks/wrench_breakable.json b/src/main/resources/data/oc2/tags/blocks/wrench_breakable.json index ff9510d8..5a72f850 100644 --- a/src/main/resources/data/oc2/tags/blocks/wrench_breakable.json +++ b/src/main/resources/data/oc2/tags/blocks/wrench_breakable.json @@ -7,6 +7,7 @@ "oc2:network_hub", "oc2:redstone_interface", "oc2:disk_drive", - "oc2:charger" + "oc2:charger", + "oc2:projector" ] -} \ No newline at end of file +} diff --git a/src/main/resources/data/oc2/tags/items/device_needs_reboot.json b/src/main/resources/data/oc2/tags/items/device_needs_reboot.json index f1fcfe8d..34e820ac 100644 --- a/src/main/resources/data/oc2/tags/items/device_needs_reboot.json +++ b/src/main/resources/data/oc2/tags/items/device_needs_reboot.json @@ -13,6 +13,7 @@ "oc2:network_interface_card", "oc2:disk_drive", "oc2:network_tunnel_card", - "oc2:network_tunnel_module" + "oc2:network_tunnel_module", + "oc2:projector" ] -} \ No newline at end of file +} diff --git a/src/main/resources/data/oc2/tags/items/devices.json b/src/main/resources/data/oc2/tags/items/devices.json index 2fea5170..c056bb1f 100644 --- a/src/main/resources/data/oc2/tags/items/devices.json +++ b/src/main/resources/data/oc2/tags/items/devices.json @@ -4,6 +4,7 @@ "oc2:computer", "oc2:redstone_interface", "oc2:disk_drive", + "oc2:projector", "#oc2:devices/memory", "#oc2:devices/hard_drive", "#oc2:devices/flash_memory", @@ -11,4 +12,4 @@ "#oc2:devices/robot_module", "#oc2:devices/floppy" ] -} \ No newline at end of file +}