Projector block.

This commit is contained in:
Florian Nücke
2022-01-23 23:25:12 +01:00
parent d6a93605a8
commit 8f89d318d4
42 changed files with 1464 additions and 34 deletions

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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<ProjectorBlockEntity> {
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<ProjectorBlockEntity, RenderInfo> 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<ProjectorBlockEntity, RenderInfo> notification) {
final RenderInfo renderInfo = notification.getValue();
assert renderInfo != null;
renderInfo.texture().close();
}
private record RenderInfo(DynamicTexture texture, RenderType renderType) { }
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -19,6 +19,7 @@ public final class Blocks {
public static final RegistryObject<NetworkConnectorBlock> NETWORK_CONNECTOR = BLOCKS.register("network_connector", NetworkConnectorBlock::new);
public static final RegistryObject<NetworkHubBlock> NETWORK_HUB = BLOCKS.register("network_hub", NetworkHubBlock::new);
public static final RegistryObject<RedstoneInterfaceBlock> REDSTONE_INTERFACE = BLOCKS.register("redstone_interface", RedstoneInterfaceBlock::new);
public static final RegistryObject<ProjectorBlock> PROJECTOR = BLOCKS.register("projector", ProjectorBlock::new);
///////////////////////////////////////////////////////////////////

View File

@@ -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 <T extends BlockEntity> BlockEntityTicker<T> getTicker(final Level level, final BlockState state, final BlockEntityType<T> 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<Block, BlockState> builder) {
builder.add(FACING);
}
}

View File

@@ -22,6 +22,7 @@ public final class BlockEntities {
public static final RegistryObject<BlockEntityType<DiskDriveBlockEntity>> DISK_DRIVE = register(Blocks.DISK_DRIVE, DiskDriveBlockEntity::new);
public static final RegistryObject<BlockEntityType<ChargerBlockEntity>> CHARGER = register(Blocks.CHARGER, ChargerBlockEntity::new);
public static final RegistryObject<BlockEntityType<CreativeEnergyBlockEntity>> CREATIVE_ENERGY = register(Blocks.CREATIVE_ENERGY, CreativeEnergyBlockEntity::new);
public static final RegistryObject<BlockEntityType<ProjectorBlockEntity>> PROJECTOR = register(Blocks.PROJECTOR, ProjectorBlockEntity::new);
///////////////////////////////////////////////////////////////////

View File

@@ -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;
}

View File

@@ -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<SimpleFramebufferDevice.Tile> 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;
}
}

View File

@@ -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);

View File

@@ -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<ProjectorBlockEntity> {
public ProjectorDeviceProvider() {
super(BlockEntities.PROJECTOR.get());
}
///////////////////////////////////////////////////////////////
@Override
protected Invalidatable<Device> 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();
}
}
}

View File

@@ -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<T> extends IdentityProxy<T> 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;
}
}

View File

@@ -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<ProjectorBlockEntity> 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<SimpleFramebufferDevice.Tile> 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;
}
}

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.common.bus.device.vm;
import net.minecraft.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -29,6 +29,7 @@ public final class Items {
public static final RegistryObject<Item> NETWORK_CONNECTOR = register(Blocks.NETWORK_CONNECTOR);
public static final RegistryObject<Item> NETWORK_HUB = register(Blocks.NETWORK_HUB);
public static final RegistryObject<Item> REDSTONE_INTERFACE = register(Blocks.REDSTONE_INTERFACE);
public static final RegistryObject<Item> PROJECTOR = register(Blocks.PROJECTOR);
///////////////////////////////////////////////////////////////////

View File

@@ -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 <T> void sendToServer(final T message) {

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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<Tile> 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);
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -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<String> getName(final Device device) {
return Optional.of("framebuffer");
}
@Override
public Optional<DeviceTree> createNode(final DeviceTree root, final MemoryMap memoryMap, final Device device, final String deviceName) {
final Optional<MappedMemoryRange> 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");
}
}

View File

@@ -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();
}

View File

@@ -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()
);
}
}

View File

@@ -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()
);
}
}

View File

@@ -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);
}

View File

@@ -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
}
}
}

View File

@@ -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).",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"parent": "oc2:block/projector"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1.0,
"bonus_rolls": 0.0,
"entries": [
{
"type": "minecraft:item",
"name": "oc2:projector"
}
],
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
]
}
]
}

View File

@@ -3,6 +3,7 @@
"values": [
"oc2:computer",
"oc2:redstone_interface",
"oc2:disk_drive"
"oc2:disk_drive",
"oc2:projector"
]
}
}

View File

@@ -7,6 +7,7 @@
"oc2:network_hub",
"oc2:redstone_interface",
"oc2:disk_drive",
"oc2:charger"
"oc2:charger",
"oc2:projector"
]
}
}

View File

@@ -13,6 +13,7 @@
"oc2:network_interface_card",
"oc2:disk_drive",
"oc2:network_tunnel_card",
"oc2:network_tunnel_module"
"oc2:network_tunnel_module",
"oc2:projector"
]
}
}

View File

@@ -4,6 +4,7 @@
"oc2:computer",
"oc2:redstone_interface",
"oc2:disk_drive",
"oc2:projector",
"#oc2:devices/memory",
"#oc2:devices/hard_drive",
"#oc2:devices/flash_memory",
@@ -11,4 +12,4 @@
"#oc2:devices/robot_module",
"#oc2:devices/floppy"
]
}
}