From 8f89d318d4a208879c19a6f6f45de6db32389887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 23 Jan 2022 23:25:12 +0100 Subject: [PATCH] Projector block. --- .../java/li/cil/oc2/client/ClientSetup.java | 6 +- .../oc2/client/renderer/ModRenderType.java | 82 ++++- .../blockentity/ProjectorRenderer.java | 256 +++++++++++++ src/main/java/li/cil/oc2/common/Config.java | 7 +- src/main/java/li/cil/oc2/common/Main.java | 2 + .../java/li/cil/oc2/common/block/Blocks.java | 1 + .../cil/oc2/common/block/ProjectorBlock.java | 60 ++++ .../oc2/common/blockentity/BlockEntities.java | 1 + .../blockentity/ComputerBlockEntity.java | 10 +- .../blockentity/ProjectorBlockEntity.java | 337 ++++++++++++++++++ .../common/bus/device/provider/Providers.java | 1 + .../block/ProjectorDeviceProvider.java | 30 ++ .../bus/device/vm/KeyboardVMDevice.java | 125 +++++++ .../bus/device/vm/ProjectorVMDevice.java | 165 +++++++++ .../common/bus/device/vm/package-info.java | 7 + .../java/li/cil/oc2/common/item/Items.java | 1 + .../li/cil/oc2/common/network/Network.java | 3 + .../ProjectorFrameBufferTileMessage.java | 56 +++ .../message/ProjectorStateMessage.java | 45 +++ .../vm/device/SimpleFramebufferDevice.java | 175 +++++++++ .../vm/provider/DeviceTreeProviders.java | 10 + .../SimpleFramebufferDeviceProvider.java | 43 +++ .../cil/oc2/data/ModBlockStateProvider.java | 2 + .../li/cil/oc2/data/ModBlockTagsProvider.java | 6 +- .../li/cil/oc2/data/ModItemTagsProvider.java | 3 +- .../li/cil/oc2/data/ModLootTableProvider.java | 1 + .../assets/oc2/blockstates/projector.json | 19 + src/main/resources/assets/oc2/lang/en_us.json | 2 + .../assets/oc2/models/block/projector.json | 1 + .../assets/oc2/models/item/projector.json | 3 + .../block/projector/projector_atlas0.png | Bin 0 -> 1900 bytes .../block/projector/projector_atlas1.png | Bin 0 -> 1649 bytes .../block/projector/projector_atlas2.png | Bin 0 -> 1390 bytes .../block/projector/projector_atlas3.png | Bin 0 -> 1644 bytes .../block/projector/projector_atlas4.png | Bin 0 -> 1574 bytes .../block/projector/projector_atlas5.png | Bin 0 -> 1561 bytes .../block/projector/projector_atlas6.png | Bin 0 -> 628 bytes .../oc2/loot_tables/blocks/projector.json | 20 ++ .../data/oc2/tags/blocks/devices.json | 5 +- .../oc2/tags/blocks/wrench_breakable.json | 5 +- .../oc2/tags/items/device_needs_reboot.json | 5 +- .../data/oc2/tags/items/devices.json | 3 +- 42 files changed, 1464 insertions(+), 34 deletions(-) create mode 100644 src/main/java/li/cil/oc2/client/renderer/blockentity/ProjectorRenderer.java create mode 100644 src/main/java/li/cil/oc2/common/block/ProjectorBlock.java create mode 100644 src/main/java/li/cil/oc2/common/blockentity/ProjectorBlockEntity.java create mode 100644 src/main/java/li/cil/oc2/common/bus/device/provider/block/ProjectorDeviceProvider.java create mode 100644 src/main/java/li/cil/oc2/common/bus/device/vm/KeyboardVMDevice.java create mode 100644 src/main/java/li/cil/oc2/common/bus/device/vm/ProjectorVMDevice.java create mode 100644 src/main/java/li/cil/oc2/common/bus/device/vm/package-info.java create mode 100644 src/main/java/li/cil/oc2/common/network/message/ProjectorFrameBufferTileMessage.java create mode 100644 src/main/java/li/cil/oc2/common/network/message/ProjectorStateMessage.java create mode 100644 src/main/java/li/cil/oc2/common/vm/device/SimpleFramebufferDevice.java create mode 100644 src/main/java/li/cil/oc2/common/vm/provider/DeviceTreeProviders.java create mode 100644 src/main/java/li/cil/oc2/common/vm/provider/SimpleFramebufferDeviceProvider.java create mode 100644 src/main/resources/assets/oc2/blockstates/projector.json create mode 100644 src/main/resources/assets/oc2/models/block/projector.json create mode 100644 src/main/resources/assets/oc2/models/item/projector.json create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas0.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas1.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas2.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas3.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas4.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas5.png create mode 100644 src/main/resources/assets/oc2/textures/block/projector/projector_atlas6.png create mode 100644 src/main/resources/data/oc2/loot_tables/blocks/projector.json 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 0000000000000000000000000000000000000000..ed29aad0d06451f5678709a388fad5e310327b97 GIT binary patch literal 1900 zcmV-y2b1`TP)sM4q62Rv#P`|3UfF7C0y>|vM4A0@2vm1lFF7jF`yzS$N-{i5P||xH@F%= z5c4pJkBu?eb(2j_B*{r)OiZ%-CI89(y6en6?Ag?*Q@6UhZ&h_yRd+dd?%bJ>kdT;| z7#|-G6TboepkbqgWuq-AFzuyAq|E3jHxKQ%Ix7@K!mzW#63VPBcJLW#PCnJ(9jvzo z8@wm#tp0=6v0CGJt$70GfZ2Z#w3t2F;KhUCCLgq;!fY=yBjqMcehl*R@{k}G#E+pS zuQG5tn7lG-u8f&)4jYT7&4rVm4=-jsm`Pt4^jse@7Dvn*!^X;(g%fNDOVDts*;Zme zTlaZ#b8~-Ne<=_MU?8;5gK8+V7{Fr(A6#1*wf=b}=ac!Yk7hIPPW$gor++ZzzyDhL zy=ni0*U}%&W-dp~LA`@PZKnBUcWbdHw9jbWXJl*mZGA6PT-)1tdoN&(_btt9d*5a-1YvXA35iCtk&GIW5>_83%jbk zqUB5|Wuh(hYyCEu}i>(+2d(uKoGi_z5OvDB%aq*uFkGrZ8Fgp=L7 zmynO9B5W&8+O};Q&Ytgh0lnwi_ke$nya3AZY&ccpo{6v;vW&&bz_u9tF27L-;3?{U zeXEe||N3^}^RCgMAIudzVI34CE*-A9r0n;Ratn?!MCR zx;y1328+W6$;x?tvzRM6o}eL77f`&eAzLD|Ec zA|&9=sHzV#;>&9R$?=Vl@V^x*K&F0qEk_v`4*J&4SdTAf{o``>$J)%pOUe>qe|Ry2 zojtysO%@E)DFa_D1W12di>(^Qu6z+<;IqGEI~bg9PQEp6t&Uq%!|IrYuD{P`Q4>_z za@1NJF_uS6JRqCd`=?j4DK1e&Qi)PqH4?E0Ln%DIlI7FF2Y%!WbaZyv0+hn)kZdHJzsmP_!*f<-)2G~_;9j^95HLn;D zy@Ik=Wgt@OjFcvIlsel>lRC>?@SSDO7^7&1UeI2m3~&ypvyaQpdO&WA1by1@nB*kY z5viZ`|H?**V$d$U=w&vd&0~Q7qb7s7t4k$PX0BfkIL6yjsbq?iXDD1^P=28j zqor8CCyMbXxqiP!T9P@OCw~pVZ!EF6xH!Dr5d*vE+DeVju4bzzli%Ju@B8XTF1R*s zt)1~w*u0>(#?9Lk-Y3@s$kBT`sA3BfE;UfmUuTN%LIq?8@^XYqJnJ`d9ep*PqX*33 zUU%?icf%i@jW4+x{^SI0206%(Wqmd3T|>JF*v8QRTOJ=cwCC4jO zp57XRZ8FZ(kP(CQ7%;^l^j3L}RC&Hw$O-AYNDf5KM`{ozPJ>Kgn+w#@Q|UfZ>DjKq z@ArT6u;|IXe9WvrC{V)>3*lv!Wf=I^8`;t^h$1l{dN*#FzBuJT+_YKN3&Tl+|tq! zDSp`CnWIPIZ6F5xHrAR#v~{EA?N?ozNsAT?PY1(OEs+`c0WIfxKGooZDg%T8UHA1X zMmt0!M8g7QsOWS$jSSsJZ?%Ds-TU3$6)w)Id%ufAgVr~nt#3YeFgVdu={daLeME;! zEvRrYs|t+||I-NFIiE&H^sc7x(i$-Y=Z`6+Ebh^f=+=r*(W9BV-~@P}aZIUJR#jYC z$*j8U4wXA+`<$2h6r~rUFS^*P@RAlvn*=3P7~tua0MNhMHN3G1bRDQld^DTE>+7KC z=}qsP_q~1AM}5)a-He*hC0B6Sz32afCevfQZPT8r%M#6JO4A?|GCb&bLSqm$9P8DV-MP5LoJb^=Ez`kc%V7V zeDF?$#RqNCv5xpyXRNZaa=as6RaMn{Gu(AGbnkji*VXDq5!ZDY+rJL7a`#%O=SEF= zdHHx(+!^aI+8=hr0YKw$Ym~A7R!#bLxc_zyV8boZk=E#NOXOjDoR7Dcy~46rQBhI; z9|;5kWg_~zG1r(|bRE$i`}pzW@$vED;o<)N{@&hRp-|Y~-e%n0-Q|@g$*^=7I8Cr$v4j*c)E)%koLrKnzAU0qvS+t}CuiFL3C?u3b0e#zxG8P9CZYr>k-Va! zqBCdC6!}D7@l~C`U1z)_2E%;%^y%~G&zxsmK^G%>bp#k9VRy96u-5tW=O=pVp7vh+ zsgEb|tS|ATr*5LhygvIj!3tYUo^;nShe@4BUGc|vYb#I123Jk6pH@TbA(B2e-1PQ$4jY>mdK@ZeZ`9E#?7)EVbv{!uM{d({;t3d}Ph z@VM(h?&#HiaM}{0rXNVv4>cS1Cby;^beMjy_bWN@m{1yo`|R1X{sf%lpgCG5gi4+r z>>{`}GP!~T ztCi`rA4~}sDS1#=R2SOUXAMA^uU{&FO@ji^4>EAkaI0Z?4Ajt1r-J}w{%lH!z!0Hw+hqrxfd;QFZu^vTiaE&YG*4mmy&x?TXa;eEt~^;DY7j= zNt+C@EBC$#be&%`2hD%r?L-%Kx3J{y{^G%=wNxr>%*{^Qj^6A5V6?X`z1hKiumwca z_f5F_dZ_z)4R=t_^IX?WrBaODH_UDI-nCHQ&9D-%^TXX*$nMLRF95*%mN2kd4=^ii z(J;*2v@>?_dlO34MtWS@9FCLrQYJj6QDTAxyDvTG$L>1%!I)_~ak(RzoNLK5jz|gU z?HW5Ci#5vXtgx>4Wh(fTeMJM^Tbc)78oCApfJdFRY@lgy9yc3NDT00000NkvXXu0mjf3|c4U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4e185496b721fa5b6f109af1886860ed1579370d GIT binary patch literal 1390 zcmV-!1(EuRP)440S6nHxjWw9X93%~-1pFoUBYB~4l*Z3S%;)3>&0RYnAh z6j5tm+k#i*B4E*0i^cNXrVsrO{r3J~vVVZYg$T6OR0!{x~mF+qs;+;jfH}l zP5Z?=-IZ&h&X5i*^P}d|srZq-0oV^`LRRl3B_;nXpQ19HsdBsBSjR7uQ3E@0YAMSY zrNTI+Z#VYFCY$niCcWKBFQ#&7AMOq34tu&r7=Gx^>Uj9>e52aq z?*_2G5BK}~Tf7FyfB-nJT%%99OZmd#uxXkv7v4hK@lJ(`l=f%SKEQ@Dq4miq_=jLv zBQM!1yz0-hHCTRMYw)+}xUn)G;r8WlicX)$Ma;@X6sOeNtn;92I}iR{`;1D7z7oiA zC-Xql6inhBSYrr5>74;E^=Lf0I<8oR?k9(W@b&lNkocuw6F5l#dKxuNS0*B|Ogg1^ zi6LPCL((jdM_~#5SPKSEZ@v)D=_KH$i`K+5aA;Bnb3AEvL4YcUF^!r z%47$e>Pq?^Tr|N~r_Ow~490jAjw67oSDjVH#`XICw&3epKt7Ka=xnJ_2KH6`d8+{* z@G|JuB3;2_ka*+wYjKo4E83;S+NILp9(*<(e{nt0+giyH0aK?FcA)?du<>LnhKvw* zL`Xa_h8$}Pu206E&m@lML=zE@)OJ-^USSp%&Jh{Zx8)(qki*dVqjlLW(6ay*n*}TGjU_{Mxw8Eo2DL3A(w?YqGJkd zem))Fn5h@MOc6G zcs2kwz$Dgph^#{RK!t;#DJmnF3cPp}Lu;41dJo1ThS1JBN2ryBXuDJDHv~U-P>2yL zg2e+BNO1?AJ9veOWy>QZuI&bYFg-*t*pKHUoyM!#>2dedlje z>bx%4O7M45IN=>=^Qr*CU(!Z&$p9p57Z?L zg;P+lajspSBfd@&hMVhny8`;rrZ-f8FY@Y$w)A6i`)n<~xslqRzpDOGomK7D4{97a we5R)cF?eBgB+KXHDmy@c%pPxFvlrd}0BK`^-NuHN@&Et;07*qoM6N<$f=xi8fB*mh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5ccfee3ae06f9efbbb7782d7a11de9dbb29f123e GIT binary patch literal 1644 zcmV-y29x=TP)99p6u`f~K%UzULumq?nJ!ZhI_+#wcu{FZgb=n)SK+~?N?YhgixmY*=>pw5Ed`WS z5D;6mEM2A>OHiz!0uMyckoZb8zWKY~ncj>|^yKE`eD|K^p7nmlWUn`Gjbr{SSS*&9 zn3(A3Xu+0Q;3(%!+i<75x6;99$wtR({{bZ%6JQQjI;>V}*``EeBIwD_R3Ct72Q(ox zz$s#*UGDC3JD(qHu+N(}4|T2x;FHbb^@^dVLg5cLIE?Y2cYz7|`l3;r(b@r+F`#Wn z@@PS!JIZX}qIYe^oqiwM#RG97k{hL>T`n=+<2hOquLCGp2u%yLmDvz4T4y)zo$wp8*Fw#vpHZAL>~iDQBh*a`2O=1 z3l}boH9;GQR4x9A`RP7yu+omkfhO12O|GvRQx!g(I$+|fjS6?|wrf8yW9%zfisX)W z5(}~%g;@y&nT~?2guFHO0+S~HVD<~M9NeHb$Jx2te(BKiFYA|sFE=Pc@I`Y61MOwD z#^S}SLq&_g24Z#jOYWWY6RW9SFHsJ4yQ$rYpqGG5AM-)qJ?_8V@7tT3g0F3*wzkrE zGI3<5S`dVr9l^3ml&B)gp|{ErHh}N9`~B0${GlGtc#n6a(|xNw{Z@y2tlI-U-RGO? z^VOOL;^N|Ja-6r1sh!E77k=Rx?edrl5haDHJ=soTW7Op-F(Od-c0%qNJHg3)%T7Uc+0@m3!2BOU|7ZWi9L>D=n?)Pg0b9m-3!$#T-YBArOrkW$h zYh^FU?L;KM z=oJhD_uxW=$XAe=AR!hrnp+6DKBs{22CFbQ-;iw*6E zrsa0?&8mQTPr!*Q#P?>0`&NgC$@=ZlH1J?^>h-qtq1H4R0o(wE#_togQl{dB)-DV~39Z6%xU1?y_Ex+2B%B;Q7?jG&*;GZ(^$DKMYFG?dV$@OIi zcoEtIuF7_fas75T(|@ofjVXP(A@$1P)JyfsfV>c#?%te;PUG%z)o8l=wT(zi;@<-E zJpfm1NgD3-3>{758DRP|L|aSZTXuqNt)=lyZ*1Tk$xsIN6)u(GMXD($nNtieXjZ=Y z7*Lm-!p$6&@)gj}R+I^P|Iqu3nu-@U6fHi?dr+5~g5>tKN|#;%bDFYDLd{n(AfuEJ zk%Q~4BtSioEJi=CJeW1ujAyB&A?(4Oe!sY{Lt&kri;>Y6;LT75wk3~sdr9MsHaDSQ zU2Srag8@@DpJ;zPR1TXe)0a~p>FH>l9Y;FD8szjvs7AdC0Vi)CPEd~MqBPAKbxELdigw5qcRb2lI>8p`S7W?&waB!eONi9I^fBTijgkQ z^Rb$mKuWmL{ztoRH(?+$3;-VIy?H5B*{YdYgrZ8dZg^4t)!E6lIVrWdW+y)zsV0q5 zom#1YltiSBXT#OJ$ueXXm>`~MA~pi}gwvp>=S)+JIfz8HVA%LbdyqRa3kLRA5LfR2R3si z1LC{SR@^_iqO06~r9m|(UCOUc@p|=BeX1@o)T91rSZDVoN!4P$Xq~NNmyQ1->1Swt zWXz!Y)Jjo(|D+${*apI%)3_`<43=Q19Dp)jA|pU-Eo`9vM9KDc99 qoC)OS;sO3>{5;)^#nW|3-0~l`JT)XH9AzT_0000 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..21c735a76a0425cb6a336f2663a1c184fd2c28a5 GIT binary patch literal 1574 zcmV+>2HE+EP)LUq(szOl6;y4bxw5mifNt~S|1tIoqkOfFe z%Tf?)TMAt$2}@Zj9{Pp)J9F^VwK}>Q&7C>t{Lh&)^FQO+ym|9@COX;`9Z5%rJ0tn_ z@IV`%n>YQyewUe{jxejE=}2W|<;_XAyu7@kqQd9%m7C`EdVQwpKv!>0g{OLI{=WW+ zXDl5V%S4CT!}&J1Z=Z|x{{3$MK9|04UnsXXG+^cj+rz^h5iq`=a@8D$itQE;0JwJ4 zMfm-Vvz2xMp>%oNemVft^7@M3b?#?lbabWo0E=tD_?P`f|zxPETvlDrm?h z3c3~DI~=0CF&VZ1Ueh*h+N6D%X^dIDI?mp@FE0r|w7Vrx60gP3UMDjx0rGL(@!Hzjty{Ow=KN~?13+6CO`q8`HBqbF+mcdrbrY5X95lDF9}A{Tej_nH>&CmQ$n=s38kbX13m22=}|AE)nqS8SV`C z>=Xc1xY*~ftE)>S5^>XrKyVcZR-!w9Kvy!r7ddBasHoaX*O>|e| z4Jw|anQHjXI;8->dn%pk_bpeO(Y!Oluh9a0S75N>Q8NmI$MtziG(UXL{JRiEg~6fogM%fSZA#DVgTEIo2y5K6bhTFvRYrvJMe@QgPec< zh<`38Tx9*?2yj}gyxQHkaihXmhcp5Plmsa_wjQvobX{wjke#UqYMs`YG4){WzY$ow)_rZimV=Ez(fzuh(EaUrfqH>#i2M&X^o1#-%}rAm)!y68yFvC^mi&?&~X!_TK2c74Q{N z_X~4fkQcf>urs9Ai~X|AVk^L{uZ6|*R%)}l7KX{&$0Ori!d?2Z6M$j~wW|YFC^rwU zq0Y#aVdv4g#N+9@Co}brr|an-ovY)Q$)C^H@r<}UY{qMjxDtKRlgya9aAMyaPRx-jNPrWzR&m2F4&0gB{_) zcJuf(PhELe%L7{&TR9x3s0SqG{;BxAlkqzzV)U54dm_fCzxPhY=5zjuY&A9oBOUc< z8R3mhG9W%BZFx)cly8%_yLnfKM>?bIDF@@cJo>7*fYZIw^Ud^D>3F8lBL={o<6)}X YKh9g(OG~2T>Hq)$07*qoM6N<$g1CM71^@s6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9ad0cdc80e69d27c0ac51fc5c9645f4b9d331d8 GIT binary patch literal 1561 zcmV+!2Il#RP)w5u<`8l}b{O#QZ1uCErZ%c0Jcr^3~K-Pft(x9N+Yea;DG`B@rn>atNS5U0`oK zqQiJ+G3(Ej>@x-SPYNA>ENAa5WkHGZV9<%??@QT#@AUT6#6?F(FM2hJRHXi^3zi8Vl^OkCvbbp>8{>n@lDJ-RzNk8G$a+ zpUhWJ=4&wi<;$#}<}>RLY4wM-UvFjo=FdhKf*Ds#Ln0y~Zg#3l-9Z&`*c}C94FSy) z2i3X*w%R<~k8`fA*$gyMNJdl?_KP|^JiNI`o%h%fL#aI=0+4GGOcJaiz#S|xo0m7z z-}o}Cb8RRPHm5V7M=u4SE%Lc-%RRckOP_7GDpk2QI6^3PM>1+-81h(~r@owaeLtC2 zlV_{kZ~ftm^cOcWs&cF-IOL6y_(#L>s8=r8{a)c+1gOlhhD*SsQJ_ey!8|JV#lq>+ z$uyM89MOC-k@if==mm4IKf8AA8f&(+mS`9<({9C56cCP2-&oUbGCvP<>Y)35x{r08Mgn=0-jgm#wU@cql)F_*h1|VhwB(sJrk*t1R$b{Tg z>e*_4_D(A0Qi$c$`^ z%}9A;a7eFbU68$%PNhyJyU1V$rP>@M4{1y?*@3A>1FFfA1nPz5^Xuv0TW5t6N(E_) zT)Yn+Y?8^2^6viv4j+%DB41rRF1AzwJtAcKG)s;WX9z4qfEiLInXz$*CHcWTE*#S@ zmO0$zcEnK1_r-?zG6EV(sK}kLjF9=2x;W!dYdIR*_i!C1__OMG#6>ehDg^a z3iwCXWW+E|6F4Pa`M^abM`T80xhBi=$8@-ED@Cn}Y#J%)MYCg>YHmuJ1_(`-#$L83 zOTWk^s^2T>z^{TV{sSD4yxZLFqCxgGu;wz+?6Pn7)P>Ux(A@cJw#KAoV|;NX1+BYTAWW zW5}FG5u8R@HW|p4c+;2GCAivXTLK$JCoV2+p_Pj>|2fsX0v%EDrV!(wXxIWKtHDexD?K7`;g z36LlwwG{C^?>;MfQz2O;72;DPygY{ZbjXg0Bb#oeF#g!_obGy10O@uCsilactw+b( z?)hO}ZZKkp!N)i^%o&YFWZMpOQFfAd!$HC95C@0UQpDF>&!Mcs8w`e-AU7RkWgBFZ zK^6>B@Zq4~+xy;Tt|uAfW?@4-98yaW&0KGB!8R4J>GgVylh0ZfkPQ<@cB