This commit is contained in:
Jackson Abney
2024-05-26 20:36:39 -08:00
parent 6b2b10be01
commit a8cbd46d9d
54 changed files with 2488 additions and 52 deletions

View File

@@ -49,6 +49,7 @@ public final class ClientSetup {
BusInterfaceNameRenderer.initialize();
BlockEntityRenderers.register(BlockEntities.COMPUTER.get(), ComputerRenderer::new);
BlockEntityRenderers.register(BlockEntities.MONITOR.get(), MonitorRenderer::new);
BlockEntityRenderers.register(BlockEntities.DISK_DRIVE.get(), DiskDriveRenderer::new);
BlockEntityRenderers.register(BlockEntities.CHARGER.get(), ChargerRenderer::new);
BlockEntityRenderers.register(BlockEntities.PROJECTOR.get(), ProjectorRenderer::new);
@@ -59,6 +60,7 @@ public final class ClientSetup {
MenuScreens.register(Containers.COMPUTER.get(), ComputerContainerScreen::new);
MenuScreens.register(Containers.COMPUTER_TERMINAL.get(), ComputerTerminalScreen::new);
MenuScreens.register(Containers.MONITOR.get(), MonitorDisplayScreen::new);
MenuScreens.register(Containers.ROBOT.get(), RobotContainerScreen::new);
MenuScreens.register(Containers.ROBOT_TERMINAL.get(), RobotTerminalScreen::new);
MenuScreens.register(Containers.NETWORK_TUNNEL.get(), NetworkTunnelScreen::new);

View File

@@ -228,7 +228,7 @@ public abstract class AbstractMachineTerminalScreen<T extends AbstractMachineTer
Component.translatable(Constants.TOOLTIP_ENERGY_CONSUMPTION,
withFormat(String.valueOf(menu.getEnergyConsumption()), ChatFormatting.GREEN))
);
//TooltipUtils.drawTooltip(graphics, tooltip, mouseX, mouseY, 200);
TooltipUtils.drawTooltip(graphics, tooltip, mouseX, mouseY, 200);
}
}
}

View File

@@ -0,0 +1,261 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.client.gui;
import com.mojang.blaze3d.platform.InputConstants;
import li.cil.oc2.client.gui.widget.ImageButton;
import li.cil.oc2.client.gui.widget.ToggleImageButton;
import li.cil.oc2.common.Constants;
import li.cil.oc2.common.container.AbstractMachineTerminalContainer;
import li.cil.oc2.common.container.AbstractMonitorContainer;
import li.cil.oc2.common.util.TooltipUtils;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.FormattedText;
import net.minecraft.world.entity.player.Inventory;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
import static li.cil.oc2.common.util.TextFormatUtils.withFormat;
@OnlyIn(Dist.CLIENT)
public abstract class AbstractMonitorDisplayScreen<T extends AbstractMonitorContainer> extends AbstractModContainerScreen<T> {
private static final int CONTROLS_TOP = 8;
private static final int ENERGY_TOP = CONTROLS_TOP + Sprites.SIDEBAR_3.height + 4;
private static boolean isInputCaptureEnabled;
private final MonitorDisplayWidget terminalWidget;
///////////////////////////////////////////////////////////////////
protected AbstractMonitorDisplayScreen(final T container, final Inventory playerInventory, final Component title) {
super(container, playerInventory, title);
this.terminalWidget = new MonitorDisplayWidget(this);
imageWidth = Sprites.TERMINAL_SCREEN.width;
imageHeight = Sprites.TERMINAL_SCREEN.height;
}
///////////////////////////////////////////////////////////////////
public static boolean isInputCaptureEnabled() {
return isInputCaptureEnabled;
}
public List<Rect2i> getExtraAreas() {
final List<Rect2i> list = new ArrayList<>();
list.add(new Rect2i(
leftPos - Sprites.SIDEBAR_3.width, topPos + CONTROLS_TOP,
Sprites.SIDEBAR_3.width, Sprites.SIDEBAR_3.height
));
if (shouldRenderEnergyBar()) {
list.add(new Rect2i(
leftPos - Sprites.SIDEBAR_2.width, topPos + ENERGY_TOP,
Sprites.SIDEBAR_2.width, Sprites.SIDEBAR_2.height
));
}
return list;
}
@Override
public void containerTick() {
super.containerTick();
terminalWidget.tick();
}
@Override
public boolean charTyped(final char ch, final int modifiers) {
return terminalWidget.charTyped(ch, modifiers) ||
super.charTyped(ch, modifiers);
}
@Override
public boolean keyPressed(final int keyCode, final int scanCode, final int modifiers) {
if (terminalWidget.keyPressed(keyCode, scanCode, modifiers)) {
return true;
}
// Don't close with inventory binding since we usually want to use that as terminal input
// even without input capture enabled.
final InputConstants.Key input = InputConstants.getKey(keyCode, scanCode);
if (getMinecraft().options.keyInventory.isActiveAndMatches(input)) {
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean keyReleased(final int keyCode, final int scanCode, final int modifiers) {
if (terminalWidget.keyReleased(keyCode, scanCode, modifiers)) {
return true;
}
// Don't close with inventory binding since we usually want to use that as terminal input
// even without input capture enabled.
final InputConstants.Key input = InputConstants.getKey(keyCode, scanCode);
if (getMinecraft().options.keyInventory.isActiveAndMatches(input)) {
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public void init() {
super.init();
terminalWidget.init();
final EditBox focusIndicatorEditBox = new EditBox(font, 0, 0, 0, 0, Component.empty());
focusIndicatorEditBox.setFocused(true);
setFocusIndicatorEditBox(focusIndicatorEditBox);
addRenderableWidget(new ToggleImageButton(
leftPos - Sprites.SIDEBAR_3.width + 4, topPos + CONTROLS_TOP + 4,
12, 12,
Sprites.POWER_BUTTON_BASE,
Sprites.POWER_BUTTON_PRESSED,
Sprites.POWER_BUTTON_ACTIVE
) {
@Override
protected void updateWidgetNarration(final NarrationElementOutput narrationElementOutput) {
}
@Override
public void onPress() {
super.onPress();
menu.sendPowerStateToServer(!menu.getPowerState());
}
@Override
public boolean isToggled() {
return menu.getPowerState();
}
}).withTooltip(
Component.translatable(Constants.COMPUTER_SCREEN_POWER_CAPTION),
Component.translatable(Constants.COMPUTER_SCREEN_POWER_DESCRIPTION)
);
addRenderableWidget(new ToggleImageButton(
leftPos - Sprites.SIDEBAR_3.width + 4, topPos + CONTROLS_TOP + 4 + 14,
12, 12,
Sprites.INPUT_BUTTON_BASE,
Sprites.INPUT_BUTTON_PRESSED,
Sprites.INPUT_BUTTON_ACTIVE
) {
@Override
protected void updateWidgetNarration(final NarrationElementOutput narrationElementOutput) {
}
@Override
public void onPress() {
super.onPress();
isInputCaptureEnabled = !isInputCaptureEnabled;
}
@Override
public boolean isToggled() {
return isInputCaptureEnabled;
}
}).withTooltip(
Component.translatable(Constants.TERMINAL_CAPTURE_INPUT_CAPTION),
Component.translatable(Constants.TERMINAL_CAPTURE_INPUT_DESCRIPTION)
);
addRenderableWidget(new ImageButton(
leftPos - Sprites.SIDEBAR_3.width + 4, topPos + CONTROLS_TOP + 4 + 14 + 14,
12, 12,
Sprites.INVENTORY_BUTTON_INACTIVE,
Sprites.INVENTORY_BUTTON_ACTIVE
) {
@Override
protected void updateWidgetNarration(final NarrationElementOutput narrationElementOutput) {
}
@Override
public void onPress() {
menu.switchToInventory();
}
}).withTooltip(Component.translatable(Constants.MACHINE_OPEN_INVENTORY_CAPTION));
}
@Override
public void onClose() {
super.onClose();
terminalWidget.onClose();
}
///////////////////////////////////////////////////////////////////
// We use this text box to indicate to Forge that we want all input, and event handlers should not be allowed
// to steal input from us (e.g. via custom key bindings). Since Forge is lazy and just uses getDeclaredFields
// to get private fields, which completely skips fields in base classes, we require subclasses to hold the field...
protected abstract void setFocusIndicatorEditBox(final EditBox editBox);
@Override
protected void renderFg(final GuiGraphics graphics, final float partialTicks, final int mouseX, final int mouseY) {
super.renderFg(graphics, partialTicks, mouseX, mouseY);
if (shouldRenderEnergyBar()) {
final int x = leftPos - Sprites.SIDEBAR_2.width + 4;
final int y = topPos + ENERGY_TOP + 4;
Sprites.ENERGY_BAR.drawFillY(graphics, x, y, menu.getEnergy() / (float) menu.getEnergyCapacity());
}
terminalWidget.render(graphics, mouseX, mouseY, Component.literal("RENDERING"));
}
@Override
protected void renderBg(final GuiGraphics graphics, final float partialTicks, final int mouseX, final int mouseY) {
Sprites.SIDEBAR_3.draw(graphics, leftPos - Sprites.SIDEBAR_3.width, topPos + CONTROLS_TOP);
if (shouldRenderEnergyBar()) {
final int x = leftPos - Sprites.SIDEBAR_2.width;
final int y = topPos + ENERGY_TOP;
Sprites.SIDEBAR_2.draw(graphics, x, y);
Sprites.ENERGY_BASE.draw(graphics, x + 4, y + 4);
}
terminalWidget.renderBackground(graphics, mouseX, mouseY);
}
@Override
protected void renderTooltip(final GuiGraphics graphics, final int mouseX, final int mouseY) {
super.renderTooltip(graphics, mouseX, mouseY);
if (shouldRenderEnergyBar()) {
if (isMouseOver(mouseX, mouseY, -Sprites.SIDEBAR_2.width + 4, ENERGY_TOP + 4, Sprites.ENERGY_BAR.width, Sprites.ENERGY_BAR.height)) {
final List<? extends FormattedText> tooltip = asList(
Component.translatable(Constants.TOOLTIP_ENERGY,
withFormat(menu.getEnergy() + "/" + menu.getEnergyCapacity(), ChatFormatting.GREEN)),
Component.translatable(Constants.TOOLTIP_ENERGY_CONSUMPTION,
withFormat(String.valueOf(menu.getEnergyConsumption()), ChatFormatting.GREEN))
);
TooltipUtils.drawTooltip(graphics, tooltip, mouseX, mouseY, 200);
}
}
}
@Override
protected void renderLabels(final GuiGraphics graphics, final int mouseX, final int mouseY) {
// This is required to prevent the labels from being rendered
}
///////////////////////////////////////////////////////////////////
private boolean shouldRenderEnergyBar() {
return menu.getEnergyCapacity() > 0;
}
}

View File

@@ -14,6 +14,8 @@ import net.minecraft.client.gui.components.ObjectSelectionList;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -160,7 +162,9 @@ public final class FileChooserScreen extends Screen {
final int buttonTop = fileNameTop + TEXT_FIELD_HEIGHT + WIDGET_SPACING;
final int buttonCount = 2;
final int buttonWidth = widgetsWidth / buttonCount - (buttonCount - 1) * WIDGET_SPACING;
okButton = addRenderableWidget(new Button(MARGIN, buttonTop, buttonWidth, BUTTON_HEIGHT, Component.empty(), this::handleOkPressed, null));
okButton = addRenderableWidget(new Button(MARGIN, buttonTop, buttonWidth, BUTTON_HEIGHT, Component.empty(), this::handleOkPressed, (p_253298_) -> {
return (MutableComponent)p_253298_.get();
}));
addRenderableWidget(new Button(MARGIN + buttonWidth + WIDGET_SPACING, buttonTop, buttonWidth, BUTTON_HEIGHT, CANCEL_TEXT, this::handleCancelPressed, null));
fileList.refreshFiles(directory);

View File

@@ -63,7 +63,7 @@ public final class MachineTerminalWidget {
public void render(final GuiGraphics graphics, final int mouseX, final int mouseY, @Nullable final Component error) {
if (container.getVirtualMachine().isRunning()) {
final PoseStack terminalStack = new PoseStack();
terminalStack.translate(leftPos + TERMINAL_X, topPos + TERMINAL_Y, 0); //TODO: REPLACE 0 with blitOffset fix
terminalStack.translate(leftPos + TERMINAL_X, topPos + TERMINAL_Y, 0);
terminalStack.scale(TERMINAL_WIDTH / (float) terminal.getWidth(), TERMINAL_HEIGHT / (float) terminal.getHeight(), 1f);
if (rendererView == null) {

View File

@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.client.gui;
import li.cil.oc2.common.container.ComputerTerminalContainer;
import li.cil.oc2.common.container.MonitorDisplayContainer;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.message.MonitorInputMessage;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.lwjgl.glfw.GLFW;
import static dev.architectury.utils.GameInstance.getClient;
@OnlyIn(Dist.CLIENT)
public final class MonitorDisplayScreen extends AbstractMonitorDisplayScreen<MonitorDisplayContainer> {
@SuppressWarnings("all") private EditBox focusIndicatorEditBox;
public static boolean hideHotbar = false;
///////////////////////////////////////////////////////////////////
public MonitorDisplayScreen(final MonitorDisplayContainer container, final Inventory playerInventory, final Component title) {
super(container, playerInventory, title);
}
///////////////////////////////////////////////////////////////////
@Override
protected void setFocusIndicatorEditBox(final EditBox editBox) {
focusIndicatorEditBox = editBox;
}
@Override
public void removed() {
super.removed();
hideHotbar = false;
}
}

View File

@@ -0,0 +1,169 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.client.gui;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import li.cil.oc2.client.gui.terminal.TerminalInput;
import li.cil.oc2.client.renderer.MonitorGUIRenderer;
import li.cil.oc2.common.bus.device.vm.block.MonitorDevice;
import li.cil.oc2.common.container.AbstractMachineTerminalContainer;
import li.cil.oc2.common.container.AbstractMonitorContainer;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.message.MonitorInputMessage;
import li.cil.oc2.common.vm.Terminal;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.joml.Matrix4f;
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import static dev.architectury.utils.GameInstance.getClient;
import static java.awt.SystemColor.menu;
@OnlyIn(Dist.CLIENT)
public final class MonitorDisplayWidget {
private static final int TERMINAL_WIDTH = Terminal.WIDTH * Terminal.CHAR_WIDTH / 2;
private static final int TERMINAL_HEIGHT = Terminal.HEIGHT * Terminal.CHAR_HEIGHT / 2;
private static final int MARGIN_SIZE = 8;
private static final int TERMINAL_X = MARGIN_SIZE;
private static final int TERMINAL_Y = MARGIN_SIZE;
public static final int WIDTH = Sprites.TERMINAL_SCREEN.width;
public static final int HEIGHT = Sprites.TERMINAL_SCREEN.height;
///////////////////////////////////////////////////////////////////
private final AbstractMonitorDisplayScreen<?> parent;
private final AbstractMonitorContainer container;
private int leftPos, topPos;
private boolean isMouseOverTerminal;
private MonitorGUIRenderer.RendererView rendererView;
private MonitorGUIRenderer monitor;
///////////////////////////////////////////////////////////////////
public MonitorDisplayWidget(final AbstractMonitorDisplayScreen<?> parent) {
this.parent = parent;
this.container = this.parent.getMenu();
this.monitor = new MonitorGUIRenderer();
}
public void renderBackground(final GuiGraphics graphics, final int mouseX, final int mouseY) {
isMouseOverTerminal = isMouseOverTerminal(mouseX, mouseY);
Sprites.TERMINAL_SCREEN.draw(graphics, leftPos, topPos);
if (shouldCaptureInput()) {
Sprites.TERMINAL_FOCUSED.draw(graphics, leftPos, topPos);
}
}
public void render(final GuiGraphics graphics, final int mouseX, final int mouseY, @Nullable final Component error) {
if (container.getPowerState() && container.isMounted()) {
final PoseStack terminalStack = new PoseStack();
terminalStack.translate(leftPos + TERMINAL_X, topPos + TERMINAL_Y, 0);
terminalStack.scale((Sprites.TERMINAL_SCREEN.width - 16f) / MonitorDevice.WIDTH, (Sprites.TERMINAL_SCREEN.height - 16f) / MonitorDevice.HEIGHT, 1f);
if (rendererView == null) {
rendererView = monitor.getRenderer(container.getMonitor());
}
final Matrix4f projectionMatrix = (new Matrix4f()).setOrtho(0, parent.width, parent.height, 0, -10f, 10f);
rendererView.render(terminalStack, projectionMatrix, MonitorDevice.WIDTH, MonitorDevice.HEIGHT);
} else {
final Font font = getClient().font;
if (error != null) {
final int textWidth = font.width(error);
final int textOffsetX = (TERMINAL_WIDTH - textWidth) / 2;
final int textOffsetY = (TERMINAL_HEIGHT - font.lineHeight) / 2;
drawShadow(
font,
graphics,
error,
leftPos + TERMINAL_X + textOffsetX,
topPos + TERMINAL_Y + textOffsetY,
0xEE3322
);
}
}
}
private void drawShadow(Font font, GuiGraphics graphics, Component text, float x, float y, int color) {
var batch = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
font.drawInBatch(text, x, y, color, true, graphics.pose().last().pose(), batch, Font.DisplayMode.NORMAL, 0, 15728880);
batch.endBatch();
}
public void tick() {
}
public boolean charTyped(final char ch, final int modifier) {
if (modifier == 0 || modifier == GLFW.GLFW_MOD_SHIFT) {
}
return true;
}
public void init() {
this.leftPos = (parent.width - WIDTH) / 2;
this.topPos = (parent.height - HEIGHT) / 2;
}
public void onClose() {
if (rendererView != null) {
rendererView = null;
}
}
public boolean keyPressed(final int keycode, final int scancode, final int modifiers) {
if (keycode == GLFW.GLFW_KEY_ESCAPE && !shouldCaptureInput())
{
return false;
}
sendInputMessage(keycode, true);
return true;
}
public boolean keyReleased(final int keycode, final int scancode, final int modifiers) {
if (keycode == GLFW.GLFW_KEY_ESCAPE && !shouldCaptureInput())
{
return false;
}
sendInputMessage(keycode, false);
return true;
}
private void sendInputMessage(final int keycode, final boolean isDown) {
if (KeyCodeMapping.MAPPING.containsKey(keycode)) {
final int evdevCode = KeyCodeMapping.MAPPING.get(keycode);
Network.sendToServer(new MonitorInputMessage(container.getMonitor(), evdevCode, isDown));
System.out.println("SENDING KEY");
}
}
///////////////////////////////////////////////////////////////////
private Minecraft getClient() {
return parent.getMinecraft();
}
private boolean shouldCaptureInput() {
return isMouseOverTerminal && AbstractMachineTerminalScreen.isInputCaptureEnabled();
}
private boolean isMouseOverTerminal(final int mouseX, final int mouseY) {
return parent.isMouseOver(mouseX, mouseY,
MonitorDisplayWidget.TERMINAL_X, MonitorDisplayWidget.TERMINAL_Y,
MonitorDisplayWidget.TERMINAL_WIDTH, MonitorDisplayWidget.TERMINAL_HEIGHT);
}
}

View File

@@ -0,0 +1,195 @@
package li.cil.oc2.client.renderer;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.platform.Window;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
import li.cil.oc2.api.API;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.bus.device.vm.block.MonitorDevice;
import li.cil.oc2.common.bus.device.vm.block.ProjectorDevice;
import li.cil.oc2.common.vm.Terminal;
import li.cil.oc2.jcodec.common.model.Picture;
import li.cil.oc2.jcodec.scale.Yuv420jToRgb;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.ShaderInstance;
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.joml.Matrix4f;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class MonitorGUIRenderer {
private final transient Set<RendererModel> renderers = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
@OnlyIn(Dist.CLIENT)
public RendererView getRenderer(MonitorBlockEntity monitor) {
final Renderer renderer = new Renderer(this, monitor);
renderers.add(renderer);
return renderer;
}
private static void handleProjectorNoLongerRendering(final RemovalNotification<MonitorBlockEntity, Renderer.RenderInfo> notification) {
final MonitorBlockEntity monitor = notification.getKey();
if (monitor != null) {
monitor.setFrameConsumer(null);
}
final Renderer.RenderInfo renderInfo = notification.getValue();
if (renderInfo != null) {
renderInfo.close();
}
}
private interface RendererModel {
AtomicInteger getDirtyMask();
void close();
}
public interface RendererView {
void render(final PoseStack stack, final Matrix4f projectionMatrix, float width, float height);
}
@OnlyIn(Dist.CLIENT)
private static final class Renderer implements RendererModel, RendererView {
private record RenderInfo(DynamicTexture texture) implements ProjectorBlockEntity.FrameConsumer {
private static final ThreadLocal<byte[]> RGB = ThreadLocal.withInitial(() -> new byte[3]);
public synchronized void close() {
texture.close();
}
@Override
public synchronized void processFrame(final Picture picture) {
final NativeImage image = texture.getPixels();
if (image == null) {
return;
}
final byte[] y = picture.getPlaneData(0);
final byte[] u = picture.getPlaneData(1);
final byte[] v = picture.getPlaneData(2);
// Convert in quads, based on the half resolution of UV. As such, skip every other row, since
// we're setting the current and the next.
int lumaIndex = 0, chromaIndex = 0;
for (int halfRow = 0; halfRow < ProjectorDevice.HEIGHT / 2; halfRow++, lumaIndex += ProjectorDevice.WIDTH * 2) {
final int row = halfRow * 2;
for (int halfCol = 0; halfCol < ProjectorDevice.WIDTH / 2; halfCol++, chromaIndex++) {
final int col = halfCol * 2;
final int yIndex = lumaIndex + col;
final byte cb = u[chromaIndex];
final byte cr = v[chromaIndex];
setFromYUV420(image, col, row, y[yIndex], cb, cr);
setFromYUV420(image, col + 1, row, y[yIndex + 1], cb, cr);
setFromYUV420(image, col, row + 1, y[yIndex + ProjectorDevice.WIDTH], cb, cr);
setFromYUV420(image, col + 1, row + 1, y[yIndex + ProjectorDevice.WIDTH + 1], cb, cr);
}
}
texture.upload();
}
private static void setFromYUV420(final NativeImage image, final int col, final int row, final byte y, final byte cb, final byte cr) {
final byte[] bytes = RGB.get();
Yuv420jToRgb.YUVJtoRGB(y, cb, cr, bytes, 0);
final int r = bytes[0] + 128;
final int g = bytes[1] + 128;
final int b = bytes[2] + 128;
image.setPixelRGBA(col, row, r | (g << 8) | (b << 16) | (0xFF << 24));
}
}
private final MonitorBlockEntity monitorBlock;
public Renderer(MonitorGUIRenderer monitor, MonitorBlockEntity monitorBlock) {
this.monitorBlock = monitorBlock;
}
private static final Cache<MonitorBlockEntity, RenderInfo> RENDER_INFO = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofSeconds(5))
.removalListener(MonitorGUIRenderer::handleProjectorNoLongerRendering)
.build();
private static DynamicTexture getColorBuffer(final MonitorBlockEntity monitor) {
try {
return RENDER_INFO.get(monitor, () -> {
final DynamicTexture texture = new DynamicTexture(MonitorDevice.WIDTH, MonitorDevice.HEIGHT, false);
texture.upload();
final RenderInfo renderInfo = new RenderInfo(texture);
monitor.setFrameConsumer(renderInfo);
return renderInfo;
}).texture();
} catch (final ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getDirtyMask() {
return null;
}
@Override
public void close() {
}
@Override
public void render(final PoseStack stack, final Matrix4f projectionMatrix, float width, float height) {
DynamicTexture texture = getColorBuffer(monitorBlock);
monitorBlock.onRendering();
RenderSystem.backupProjectionMatrix();
RenderSystem.getModelViewStack().pushPose();
RenderSystem.enableBlend();
RenderSystem.blendFunc(GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ONE);
RenderSystem.colorMask(true, true, true, true);
RenderSystem.disableDepthTest();
RenderSystem.depthMask(false);
final ShaderInstance shader = GameRenderer.getPositionTexShader();
final BufferBuilder builder = Tesselator.getInstance().getBuilder();
RenderSystem.setProjectionMatrix(projectionMatrix, VertexSorting.ORTHOGRAPHIC_Z);
RenderSystem.setShaderTexture(0, texture.getId());
VertexBuffer buffer = new VertexBuffer(VertexBuffer.Usage.DYNAMIC);
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX);
builder.vertex(0, 0, 0).uv(0, 0).endVertex();
builder.vertex(0, height, 0).uv(0, 1).endVertex();
builder.vertex(width, height, 0).uv(1, 1).endVertex();
builder.vertex(width, 0, 0).uv(1, 0).endVertex();
buffer.bind();
buffer.upload(builder.end());
buffer.drawWithShader(stack.last().pose(), projectionMatrix, shader);
VertexBuffer.unbind();
buffer.close();
RenderSystem.restoreProjectionMatrix();
RenderSystem.getModelViewStack().popPose();
RenderSystem.applyModelViewMatrix();
}
}
}

View File

@@ -0,0 +1,251 @@
/* SPDX-License-Identifier: MIT */
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.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import li.cil.oc2.api.API;
import li.cil.oc2.client.renderer.ModRenderType;
import li.cil.oc2.common.block.ComputerBlock;
import li.cil.oc2.common.block.MonitorBlock;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.util.ChainableVertexConsumer;
import li.cil.oc2.common.vm.Terminal;
import net.minecraft.client.gui.Font;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.resources.model.Material;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.FormattedText;
import net.minecraft.network.chat.Style;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.joml.Matrix4f;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutionException;
@Mod.EventBusSubscriber(value = Dist.CLIENT, modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class MonitorRenderer implements BlockEntityRenderer<MonitorBlockEntity> {
public static final ResourceLocation OVERLAY_POWER_LOCATION = new ResourceLocation(API.MOD_ID, "block/computer/computer_overlay_power");
public static final ResourceLocation OVERLAY_STATUS_LOCATION = new ResourceLocation(API.MOD_ID, "block/computer/computer_overlay_status");
public static final ResourceLocation OVERLAY_TERMINAL_LOCATION = new ResourceLocation(API.MOD_ID, "block/computer/computer_overlay_terminal");
private static final Material TEXTURE_POWER = new Material(InventoryMenu.BLOCK_ATLAS, OVERLAY_POWER_LOCATION);
private static final Material TEXTURE_STATUS = new Material(InventoryMenu.BLOCK_ATLAS, OVERLAY_STATUS_LOCATION);
private static final Material TEXTURE_TERMINAL = new Material(InventoryMenu.BLOCK_ATLAS, OVERLAY_TERMINAL_LOCATION);
private static final Cache<Terminal, Terminal.RendererView> rendererViews = CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofSeconds(5))
.removalListener(MonitorRenderer::handleNoLongerRendering)
.build();
///////////////////////////////////////////////////////////////////
private final BlockEntityRenderDispatcher renderer;
///////////////////////////////////////////////////////////////////
public MonitorRenderer(final BlockEntityRendererProvider.Context context) {
this.renderer = context.getBlockEntityRenderDispatcher();
}
///////////////////////////////////////////////////////////////////
@Override
public void render(final MonitorBlockEntity monitor, final float partialTicks, final PoseStack stack, final MultiBufferSource bufferSource, final int light, final int overlay) {
final Direction blockFacing = monitor.getBlockState().getValue(MonitorBlock.FACING);
final Vec3 cameraPosition = renderer.camera.getEntity().getEyePosition(partialTicks);
// If viewer is not in front of the block we can skip the rest, it cannot be visible.
// We check against the center of the block instead of the actual relevant face for simplicity.
final Vec3 relativeCameraPosition = cameraPosition.subtract(Vec3.atCenterOf(monitor.getBlockPos()));
final double projectedCameraPosition = relativeCameraPosition.dot(Vec3.atLowerCornerOf(blockFacing.getNormal()));
if (projectedCameraPosition <= 0) {
return;
}
stack.pushPose();
// Align with front face of block.
stack.translate(0.5f, 0, 0.5f);
stack.mulPose(Axis.YN.rotationDegrees(blockFacing.toYRot() + 180));
stack.translate(-0.5f, 0, -0.5f);
// Flip and align with top left corner.
stack.translate(1, 1, 0);
stack.scale(-1, -1, -1);
// Scale to make 1/16th of the block one unit and align with top left of terminal area.
final float pixelScale = 1 / 16f;
stack.scale(pixelScale, pixelScale, pixelScale);
if (false) {
//renderTerminal(monitor, stack, bufferSource, cameraPosition);
} else {
renderStatusText(monitor, stack, cameraPosition);
}
stack.translate(0, 0, -0.1f);
final Matrix4f matrix = stack.last().pose();
renderStatus(matrix, bufferSource);
stack.popPose();
}
///////////////////////////////////////////////////////////////////
/*private void renderTerminal(final MonitorBlockEntity computer, final PoseStack stack, final MultiBufferSource bufferSource, final Vec3 cameraPosition) {
// Render terminal content if close enough.
if (Vec3.atCenterOf(computer.getBlockPos()).closerThan(cameraPosition, 6f)) {
stack.pushPose();
stack.translate(2, 2, -0.9f);
// Scale to make terminal fit fully.
final Terminal terminal = computer.getTerminal();
final float textScaleX = 12f / terminal.getWidth();
final float textScaleY = 7f / terminal.getHeight();
final float scale = Math.min(textScaleX, textScaleY) * 0.95f;
// Center it on both axes.
final float scaleDeltaX = textScaleX - scale;
final float scaleDeltaY = textScaleY - scale;
stack.translate(
terminal.getWidth() * scaleDeltaX * 0.5f,
terminal.getHeight() * scaleDeltaY * 0.5f,
0f);
stack.scale(scale, scale, 1f);
// TODO Make terminal renderer use buffer+rendertype.
RenderSystem.enableBlend();
RenderSystem.enableDepthTest();
try {
rendererViews.get(terminal, terminal::getRenderer).render(stack, RenderSystem.getProjectionMatrix());
} catch (final ExecutionException e) {
throw new RuntimeException(e);
}
stack.popPose();
} else {
stack.pushPose();
stack.translate(0, 0, -0.9f);
final Matrix4f matrix = stack.last().pose();
renderQuad(matrix, TEXTURE_TERMINAL.buffer(bufferSource, ModRenderType::getUnlitBlock));
stack.popPose();
}
}*/
private void renderStatusText(final MonitorBlockEntity monitor, final PoseStack stack, final Vec3 cameraPosition) {
if (!Vec3.atCenterOf(monitor.getBlockPos()).closerThan(cameraPosition, 12f)) {
return;
}
final Component bootError = Component.literal("RENDERING");
stack.pushPose();
stack.translate(3, 3, -0.9f);
drawText(stack, bootError);
stack.popPose();
}
private void drawText(final PoseStack stack, final Component text) {
final int maxWidth = 100;
stack.pushPose();
stack.scale(10f / maxWidth, 10f / maxWidth, 10f / maxWidth);
final Font fontRenderer = renderer.font;
final List<FormattedText> wrappedText = fontRenderer.getSplitter().splitLines(text, maxWidth, Style.EMPTY);
if (wrappedText.size() == 1) {
final int textWidth = fontRenderer.width(text);
draw(fontRenderer, stack, text, (maxWidth - textWidth) * 0.5f, 0, 0xEE3322);
} else {
for (int i = 0; i < wrappedText.size(); i++) {
draw(fontRenderer, stack, wrappedText.get(i).getString(), 0, i * fontRenderer.lineHeight, 0xEE3322);
}
}
stack.popPose();
}
private void draw(Font font, PoseStack stack, Component text, float x, float y, int color) {
var batch = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
font.drawInBatch(text, x, y, color, false, stack.last().pose(), batch, Font.DisplayMode.NORMAL, 0, 15728880);
batch.endBatch();
}
private void draw(Font font, PoseStack stack, String text, float x, float y, int color) {
var batch = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
font.drawInBatch(text, x, y, color, false, stack.last().pose(), batch, Font.DisplayMode.NORMAL, 0, 15728880, false);
batch.endBatch();
}
private void renderStatus(final Matrix4f matrix, final MultiBufferSource bufferSource) {
renderStatus(matrix, bufferSource, 0);
}
private void renderStatus(final Matrix4f matrix, final MultiBufferSource bufferSource, final int frequency) {
if (frequency <= 0 || (((System.currentTimeMillis() + hashCode()) / frequency) % 2) == 1) {
renderQuad(matrix, TEXTURE_STATUS.buffer(bufferSource, ModRenderType::getUnlitBlock));
}
}
private void renderPower(final Matrix4f matrix, final MultiBufferSource bufferSource) {
renderQuad(matrix, TEXTURE_POWER.buffer(bufferSource, ModRenderType::getUnlitBlock));
}
private static void renderQuad(final Matrix4f matrix, final VertexConsumer consumer) {
final VertexConsumer wrapper = new ChainableVertexConsumer(consumer);
wrapper.vertex(matrix, 0, 0, 0)
.uv(0, 0)
.endVertex();
wrapper.vertex(matrix, 0, 16, 0)
.uv(0, 1)
.endVertex();
wrapper.vertex(matrix, 16, 16, 0)
.uv(1, 1)
.endVertex();
wrapper.vertex(matrix, 16, 0, 0)
.uv(1, 0)
.endVertex();
}
@SubscribeEvent
public static void updateCache(final TickEvent.ClientTickEvent event) {
rendererViews.cleanUp();
}
private static void handleNoLongerRendering(final RemovalNotification<Terminal, Terminal.RendererView> notification) {
final Terminal key = notification.getKey();
final Terminal.RendererView value = notification.getValue();
if (key != null && value != null) {
key.releaseRenderer(value);
}
}
}

View File

@@ -20,6 +20,7 @@ public final class Blocks {
public static final RegistryObject<BusCableBlock> BUS_CABLE = BLOCKS.register("bus_cable", BusCableBlock::new);
public static final RegistryObject<ChargerBlock> CHARGER = BLOCKS.register("charger", ChargerBlock::new);
public static final RegistryObject<ComputerBlock> COMPUTER = BLOCKS.register("computer", ComputerBlock::new);
public static final RegistryObject<MonitorBlock> MONITOR = BLOCKS.register("monitor", MonitorBlock::new);
public static final RegistryObject<CreativeEnergyBlock> CREATIVE_ENERGY = BLOCKS.register("creative_energy", CreativeEnergyBlock::new);
public static final RegistryObject<DiskDriveBlock> DISK_DRIVE = BLOCKS.register("disk_drive", DiskDriveBlock::new);
public static final RegistryObject<FlashMemoryFlasherBlock> FLASH_MEMORY_FLASHER = BLOCKS.register("flash_memory_flasher", FlashMemoryFlasherBlock::new);

View File

@@ -216,8 +216,8 @@ public final class ComputerBlock extends HorizontalDirectionalBlock implements E
///////////////////////////////////////////////////////////////////
private ItemStack getComputerWithFlash() {
final ItemStack computer = new ItemStack(this);
public static ItemStack getComputerWithFlash() {
final ItemStack computer = new ItemStack(Items.COMPUTER.get());
final CompoundTag itemsTag = NBTUtils.getOrCreateChildTag(computer.getOrCreateTag(), BLOCK_ENTITY_TAG_NAME_IN_ITEM, ITEMS_TAG_NAME);
itemsTag.put(DeviceTypes.FLASH_MEMORY.getName().toString(), makeInventoryTag(
@@ -227,23 +227,30 @@ public final class ComputerBlock extends HorizontalDirectionalBlock implements E
return computer;
}
private ItemStack getPreconfiguredComputer() {
public static ItemStack getPreconfiguredComputer() {
final ItemStack computer = getComputerWithFlash();
final CompoundTag itemsTag = NBTUtils.getOrCreateChildTag(computer.getOrCreateTag(), BLOCK_ENTITY_TAG_NAME_IN_ITEM, ITEMS_TAG_NAME);
itemsTag.put(DeviceTypes.MEMORY.getName().toString(), makeInventoryTag(
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get())
));
itemsTag.put(DeviceTypes.HARD_DRIVE.getName().toString(), makeInventoryTag(
new ItemStack(Items.HARD_DRIVE_CUSTOM.get())
));
itemsTag.put(DeviceTypes.CARD.getName().toString(), makeInventoryTag(
new ItemStack(Items.NETWORK_INTERFACE_CARD.get())
));
itemsTag.put(DeviceTypes.CPU.getName().toString(), makeInventoryTag(
new ItemStack(Items.CPU_TIER_3.get())
));
computer.setHoverName(text("block.{mod}.computer.preconfigured"));
return computer;

View File

@@ -0,0 +1,151 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.block;
import li.cil.oc2.common.blockentity.BlockEntities;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.blockentity.TickableBlockEntity;
import li.cil.oc2.common.integration.Wrenches;
import li.cil.oc2.common.util.TooltipUtils;
import li.cil.oc2.common.util.VoxelShapeUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
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.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import javax.annotation.Nullable;
import java.util.List;
public final class MonitorBlock extends HorizontalDirectionalBlock implements EnergyConsumingBlock, EntityBlock {
public static final BooleanProperty LIT = BlockStateProperties.LIT;
// We bake the "screen" indent on the front into the collision shape, to prevent stuff being
// placeable on that side, such as network connectors, torches, etc.
private static final VoxelShape NEG_Z_SHAPE = Shapes.or(
Block.box(0, 0, 1, 16, 16, 16), // main body
Block.box(0, 15, 0, 16, 16, 1), // across top
Block.box(0, 0, 0, 16, 6, 1), // across bottom
Block.box(0, 0, 0, 1, 16, 1), // up left
Block.box(15, 0, 0, 16, 16, 1) // up right
);
private static final VoxelShape NEG_X_SHAPE = VoxelShapeUtils.rotateHorizontalClockwise(NEG_Z_SHAPE);
private static final VoxelShape POS_Z_SHAPE = VoxelShapeUtils.rotateHorizontalClockwise(NEG_X_SHAPE);
private static final VoxelShape POS_X_SHAPE = VoxelShapeUtils.rotateHorizontalClockwise(POS_Z_SHAPE);
///////////////////////////////////////////////////////////////////
public MonitorBlock() {
super(Properties
.of()
.mapColor(MapColor.METAL)
.sound(SoundType.METAL)
.lightLevel(state -> state.getValue(LIT) ? 8 : 0)
.strength(1.5f, 6.0f));
registerDefaultState(getStateDefinition().any()
.setValue(FACING, Direction.NORTH)
.setValue(LIT, false));
}
///////////////////////////////////////////////////////////////////
@OnlyIn(Dist.CLIENT)
@Override
public void appendHoverText(final ItemStack stack, @Nullable final BlockGetter level, final List<Component> tooltip, final TooltipFlag advanced) {
super.appendHoverText(stack, level, tooltip, advanced);
TooltipUtils.addEnergyConsumption(2, tooltip);
}
@SuppressWarnings("deprecation")
@Override
public VoxelShape getShape(final BlockState state, final BlockGetter level, final BlockPos pos, final CollisionContext context) {
return switch (state.getValue(FACING)) {
case NORTH -> NEG_Z_SHAPE;
case SOUTH -> POS_Z_SHAPE;
case WEST -> NEG_X_SHAPE;
default -> POS_X_SHAPE;
};
}
@SuppressWarnings("deprecation")
@Override
public InteractionResult use(final BlockState state, final Level level, final BlockPos pos, final Player player, final InteractionHand hand, final BlockHitResult hit) {
final BlockEntity blockEntity = level.getBlockEntity(pos);
if (!(blockEntity instanceof final MonitorBlockEntity monitor)) {
return super.use(state, level, pos, player, hand, hit);
}
final ItemStack heldItem = player.getItemInHand(hand);
if (!Wrenches.isWrench(heldItem)) {
if (!level.isClientSide()) {
if (player.isShiftKeyDown()) {
monitor.start();
} else if (player instanceof final ServerPlayer serverPlayer) {
monitor.openTerminalScreen(serverPlayer);
}
}
return InteractionResult.sidedSuccess(level.isClientSide());
}
return super.use(state, level, pos, player, hand, hit);
}
@Override
public BlockState getStateForPlacement(final BlockPlaceContext context) {
return super.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
///////////////////////////////////////////////////////////////////
// EntityBlock
@Nullable
@Override
public BlockEntity newBlockEntity(final BlockPos pos, final BlockState state) {
return BlockEntities.MONITOR.get().create(pos, state);
}
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(final Level level, final BlockState state, final BlockEntityType<T> type) {
return TickableBlockEntity.createTicker(level, type, BlockEntities.MONITOR.get());
}
///////////////////////////////////////////////////////////////////
@Override
protected void createBlockStateDefinition(final StateDefinition.Builder<Block, BlockState> builder) {
super.createBlockStateDefinition(builder);
builder.add(FACING, LIT);
}
@Override
public int getEnergyConsumption() {
return 2;
}
///////////////////////////////////////////////////////////////////
}

View File

@@ -13,6 +13,8 @@ import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import javax.management.monitor.Monitor;
public final class BlockEntities {
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES = DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, API.MOD_ID);
@@ -21,6 +23,7 @@ public final class BlockEntities {
public static final RegistryObject<BlockEntityType<BusCableBlockEntity>> BUS_CABLE = register(Blocks.BUS_CABLE, BusCableBlockEntity::new);
public static final RegistryObject<BlockEntityType<ChargerBlockEntity>> CHARGER = register(Blocks.CHARGER, ChargerBlockEntity::new);
public static final RegistryObject<BlockEntityType<ComputerBlockEntity>> COMPUTER = register(Blocks.COMPUTER, ComputerBlockEntity::new);
public static final RegistryObject<BlockEntityType<MonitorBlockEntity>> MONITOR = register(Blocks.MONITOR, MonitorBlockEntity::new);
public static final RegistryObject<BlockEntityType<CreativeEnergyBlockEntity>> CREATIVE_ENERGY = register(Blocks.CREATIVE_ENERGY, CreativeEnergyBlockEntity::new);
public static final RegistryObject<BlockEntityType<DiskDriveBlockEntity>> DISK_DRIVE = register(Blocks.DISK_DRIVE, DiskDriveBlockEntity::new);
public static final RegistryObject<BlockEntityType<FlashMemoryFlasherBlockEntity>> FLASH_MEMORY_FLASHER = register(Blocks.FLASH_MEMORY_FLASHER, FlashMemoryFlasherBlockEntity::new);

View File

@@ -0,0 +1,342 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.blockentity;
import li.cil.oc2.api.bus.DeviceBusElement;
import li.cil.oc2.api.bus.device.Device;
import li.cil.oc2.api.bus.device.DeviceTypes;
import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery;
import li.cil.oc2.api.capabilities.TerminalUserProvider;
import li.cil.oc2.client.audio.LoopingSoundManager;
import li.cil.oc2.common.Config;
import li.cil.oc2.common.block.ComputerBlock;
import li.cil.oc2.common.block.ProjectorBlock;
import li.cil.oc2.common.bus.AbstractBlockDeviceBusElement;
import li.cil.oc2.common.bus.BlockDeviceBusController;
import li.cil.oc2.common.bus.CommonDeviceBusController;
import li.cil.oc2.common.bus.device.BlockDeviceBusElement;
import li.cil.oc2.common.bus.device.util.Devices;
import li.cil.oc2.common.bus.device.vm.block.KeyboardDevice;
import li.cil.oc2.common.bus.device.vm.block.MonitorDevice;
import li.cil.oc2.common.bus.device.vm.block.ProjectorDevice;
import li.cil.oc2.common.capabilities.Capabilities;
import li.cil.oc2.common.container.ComputerInventoryContainer;
import li.cil.oc2.common.container.ComputerTerminalContainer;
import li.cil.oc2.common.container.MonitorDisplayContainer;
import li.cil.oc2.common.energy.FixedEnergyStorage;
import li.cil.oc2.common.network.MonitorLoadBalancer;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.ProjectorLoadBalancer;
import li.cil.oc2.common.network.message.*;
import li.cil.oc2.common.serialization.NBTSerialization;
import li.cil.oc2.common.util.*;
import li.cil.oc2.common.vm.*;
import li.cil.oc2.jcodec.codecs.h264.H264Decoder;
import li.cil.oc2.jcodec.codecs.h264.H264Encoder;
import li.cil.oc2.jcodec.codecs.h264.encode.CQPRateControl;
import li.cil.oc2.jcodec.common.model.ColorSpace;
import li.cil.oc2.jcodec.common.model.Picture;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import static li.cil.oc2.common.Constants.BLOCK_ENTITY_TAG_NAME_IN_ITEM;
import static li.cil.oc2.common.Constants.ITEMS_TAG_NAME;
public final class MonitorBlockEntity extends ModBlockEntity implements TickableBlockEntity {
private static final String STATE_TAG_NAME = "state";
private static final String ENERGY_TAG_NAME = "energy";
private static final String IS_PROJECTING_TAG_NAME = "projecting";
private static final String HAS_ENERGY_TAG_NAME = "has_energy";
private static final ExecutorService DECODER_WORKERS = Executors.newCachedThreadPool(r -> {
final Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Projector Frame Decoder");
return thread;
});
///////////////////////////////////////////////////////////////////
private final FixedEnergyStorage energy = new FixedEnergyStorage(Config.computerEnergyStorage);
///////////////////////////////////////////////////////////////////
private boolean hasEnergy;
private boolean isMounted;
private boolean isPowered;
@Nullable private CompletableFuture<?> runningDecode;
private final H264Decoder decoder = new H264Decoder();
private final ByteBuffer decoderBuffer = ByteBuffer.allocateDirect(1024 * 1024);
@Nullable private ProjectorBlockEntity.FrameConsumer frameConsumer;
private boolean needsIDR;
private final BlockDeviceBusElement busElement = new BlockDeviceBusElement();
private final MonitorDevice monitorDevice = new MonitorDevice(this, this::handleMountedChanged);
private final KeyboardDevice<BlockEntity> keyboardDevice = new KeyboardDevice<>(this);
private final Picture picture = Picture.create(MonitorDevice.WIDTH, MonitorDevice.HEIGHT, ColorSpace.YUV420J);
private final H264Encoder encoder = new H264Encoder(new CQPRateControl(12));
private final ByteBuffer encoderBuffer = ByteBuffer.allocateDirect(1024 * 1024);
///////////////////////////////////////////////////////////////////
public void setRequiresKeyframe() {
needsIDR = true;
}
public boolean getPowerState() { return isPowered; }
public boolean isMounted() { return isMounted; }
private long lastKeepAliveSentAt;
public void handleInput(final int keycode, final boolean isDown) {
keyboardDevice.sendKeyEvent(keycode, isDown);
}
@Nullable
private ByteBuffer encodeFrame() {
final boolean hasChanges = monitorDevice.applyChanges(picture);
if (!hasChanges && !needsIDR) {
return null;
}
encoderBuffer.clear();
final ByteBuffer frameData;
try {
if (needsIDR) {
frameData = encoder.encodeIDRFrame(picture, encoderBuffer);
needsIDR = false;
} else {
frameData = encoder.encodeFrame(picture, encoderBuffer).data();
}
} catch (final BufferOverflowException ignored) {
return null;
}
final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
deflater.setInput(frameData);
deflater.finish();
final ByteBuffer compressedFrameData = ByteBuffer.allocateDirect(1024 * 1024);
deflater.deflate(compressedFrameData, Deflater.FULL_FLUSH);
deflater.end();
compressedFrameData.flip();
return compressedFrameData;
}
private void handleMountedChanged(final boolean value) {
updateProjectorState(value, hasEnergy);
}
public void setFrameConsumer(@Nullable final ProjectorBlockEntity.FrameConsumer consumer) {
if (consumer == frameConsumer) {
return;
}
synchronized (picture) {
this.frameConsumer = consumer;
if (frameConsumer != null) {
frameConsumer.processFrame(picture);
}
}
}
private void updateProjectorState(final boolean isMounted, final boolean hasEnergy) {
if (isMounted == this.isMounted && hasEnergy == this.hasEnergy) {
return;
}
// We may get called from unmount() of our device, which can be triggered due to chunk unload.
// Hence, we need to check the loaded state here, lest we ghost load the chunk, breaking everything.
if (level != null && !level.isClientSide() && level.isLoaded(getBlockPos())) {
if (this.isMounted && !isMounted) {
Arrays.fill(picture.getPlaneData(0), (byte) -128);
Arrays.fill(picture.getPlaneData(1), (byte) 0);
Arrays.fill(picture.getPlaneData(2), (byte) 0);
}
this.isMounted = isMounted;
this.hasEnergy = hasEnergy;
level.setBlock(getBlockPos(), getBlockState().setValue(ProjectorBlock.LIT, isMounted), Block.UPDATE_CLIENTS);
Network.sendToClientsTrackingBlockEntity(new MonitorStateMessage(this, isMounted, hasEnergy), this);
}
}
public void applyMonitorStateClient(final boolean isProjecting, final boolean hasEnergy) {
if (level == null || !level.isClientSide()) {
return;
}
this.isMounted = isProjecting;
this.hasEnergy = hasEnergy;
}
public MonitorBlockEntity(final BlockPos pos, final BlockState state) {
super(BlockEntities.MONITOR.get(), pos, state);
busElement.addDevice(keyboardDevice);
busElement.addDevice(monitorDevice);
encoder.setKeyInterval(100);
// We want to unload devices even on level unload to free global resources.
setNeedsLevelUnloadEvent();
}
public void start() {
isPowered = true;
}
public void stop() {
isPowered = false;
}
public void openTerminalScreen(final ServerPlayer player) {
MonitorDisplayContainer.createServer(this, energy, player);
}
public void onRendering() {
final long now = System.currentTimeMillis();
if (now - lastKeepAliveSentAt > 1000) {
lastKeepAliveSentAt = now;
Network.sendToServer(new MonitorRequestFramebufferMessage(this));
}
}
@Override
public void serverTick() {
if (level == null) {
return;
}
final boolean hasPowered;
if (Config.projectorsUseEnergy()) {
hasPowered = energy.extractEnergy(Config.projectorEnergyPerTick, true) >= Config.projectorEnergyPerTick;
if (hasPowered) {
energy.extractEnergy(Config.projectorEnergyPerTick, false);
}
} else {
hasPowered = true;
}
updateProjectorState(isMounted, isPowered);
if (!hasEnergy || !isPowered || (!monitorDevice.hasChanges() && !needsIDR)) {
return;
}
MonitorLoadBalancer.offerFrame(this, this::encodeFrame);
}
public void applyNextFrameClient(final ByteBuffer frameData) {
if (level == null || !level.isClientSide()) {
return;
}
final CompletableFuture<?> lastDecode = runningDecode;
runningDecode = CompletableFuture.runAsync(() -> {
try {
try {
if (lastDecode != null) lastDecode.join();
} catch (final CompletionException ignored) {
}
final Inflater inflater = new Inflater();
inflater.setInput(frameData);
decoderBuffer.clear();
inflater.inflate(decoderBuffer);
decoderBuffer.flip();
decoder.decodeFrame(decoderBuffer, picture.getData());
synchronized (picture) {
if (frameConsumer != null) {
frameConsumer.processFrame(picture);
}
}
} catch (final DataFormatException ignored) {
}
}, DECODER_WORKERS);
}
@Override
public CompoundTag getUpdateTag() {
final CompoundTag tag = super.getUpdateTag();
tag.putBoolean(IS_PROJECTING_TAG_NAME, isMounted);
tag.putBoolean(HAS_ENERGY_TAG_NAME, hasEnergy);
tag.putBoolean(STATE_TAG_NAME, isPowered);
return tag;
}
@Override
public void handleUpdateTag(final CompoundTag tag) {
super.handleUpdateTag(tag);
isMounted = tag.getBoolean(IS_PROJECTING_TAG_NAME);
hasEnergy = tag.getBoolean(HAS_ENERGY_TAG_NAME);
isPowered = tag.getBoolean(STATE_TAG_NAME);
}
@Override
protected void saveAdditional(final CompoundTag tag) {
super.saveAdditional(tag);
tag.put(ENERGY_TAG_NAME, energy.serializeNBT());
tag.putBoolean(IS_PROJECTING_TAG_NAME, isPowered);
}
@Override
public void load(final CompoundTag tag) {
super.load(tag);
energy.deserializeNBT(tag.getCompound(ENERGY_TAG_NAME));
hasEnergy = tag.getBoolean(HAS_ENERGY_TAG_NAME);
isPowered = tag.getBoolean(IS_PROJECTING_TAG_NAME);
}
///////////////////////////////////////////////////////////////////
@Override
protected void collectCapabilities(final CapabilityCollector collector, @Nullable final Direction direction) {
collector.offer(Capabilities.deviceBusElement(), busElement);
if (Config.computersUseEnergy()) {
collector.offer(Capabilities.energyStorage(), energy);
}
}
}

View File

@@ -0,0 +1,8 @@
package li.cil.oc2.common.bus.device;
import li.cil.oc2.common.bus.AbstractDeviceBusElement;
public class BlockDeviceBusElement extends AbstractDeviceBusElement {
}

View File

@@ -11,6 +11,11 @@ import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.IForgeRegistry;
import net.minecraftforge.registries.RegistryBuilder;
import org.checkerframework.framework.qual.Unused;
import java.util.function.Supplier;
import static li.cil.oc2.common.util.TranslationUtils.text;
@@ -19,6 +24,11 @@ public final class DeviceTypes {
///////////////////////////////////////////////////////////////////
@SuppressWarnings("unused")
public static final Supplier<IForgeRegistry<DeviceType>> DEVICE_TYPE_REGISTRY = DEVICE_TYPES.makeRegistry(RegistryBuilder::new);
///////////////////////////////////////////////////////////////////
public static void initialize() {
DEVICE_TYPES.register(FMLJavaModLoadingContext.get().getModEventBus());

View File

@@ -28,7 +28,7 @@ import java.util.Set;
import java.util.WeakHashMap;
public final class FileImportExportCardItemDevice extends AbstractItemRPCDevice implements DocumentedDevice {
public static final int MAX_TRANSFERRED_FILE_SIZE = 512 * Constants.KILOBYTE;
public static final int MAX_TRANSFERRED_FILE_SIZE = Constants.MEGABYTE;
private static final String BEGIN_EXPORT_FILE = "beginExportFile";
private static final String WRITE_EXPORT_FILE = "writeExportFile";

View File

@@ -0,0 +1,149 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.bus.device.vm.block;
import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
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.serialization.BlobStorage;
import li.cil.oc2.common.util.NBTTagIds;
import li.cil.oc2.common.vm.device.SimpleFramebufferDevice;
import li.cil.oc2.jcodec.common.model.Picture;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.UUID;
public final class MonitorDevice extends IdentityProxy<BlockEntity> 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;
///////////////////////////////////////////////////////////////
private final BooleanConsumer onMountedChanged;
@Nullable private SimpleFramebufferDevice device;
///////////////////////////////////////////////////////////////
private final OptionalAddress address = new OptionalAddress();
@Nullable private UUID blobHandle;
///////////////////////////////////////////////////////////////
public MonitorDevice(final BlockEntity identity, final BooleanConsumer onMountedChanged) {
super(identity);
this.onMountedChanged = onMountedChanged;
}
///////////////////////////////////////////////////////////////
public boolean hasChanges() {
final SimpleFramebufferDevice framebufferDevice = device;
return framebufferDevice != null && framebufferDevice.hasChanges();
}
public boolean applyChanges(final Picture picture) {
final SimpleFramebufferDevice framebufferDevice = device;
return framebufferDevice != null && framebufferDevice.applyChanges(picture);
}
@Override
public VMDeviceLoadResult mount(final VMContext context) {
if (!allocateDevice(context)) {
return VMDeviceLoadResult.fail();
}
assert device != null;
if (!address.claim(context, device)) {
return VMDeviceLoadResult.fail();
}
onMountedChanged.accept(true);
return VMDeviceLoadResult.success();
}
@Override
public void unmount() {
final SimpleFramebufferDevice framebufferDevice = device;
device = null;
if (framebufferDevice != null) {
framebufferDevice.close();
}
if (blobHandle != null) {
BlobStorage.close(blobHandle);
}
onMountedChanged.accept(false);
}
@Override
public void dispose() {
if (blobHandle != null) {
BlobStorage.delete(blobHandle);
blobHandle = null;
}
address.clear();
}
@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);
}
}

View File

@@ -0,0 +1,99 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.container;
import li.cil.oc2.common.block.Blocks;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.bus.CommonDeviceBusController;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.network.message.*;
import li.cil.oc2.common.vm.Terminal;
import li.cil.oc2.common.vm.VirtualMachine;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.ContainerLevelAccess;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import net.minecraftforge.energy.IEnergyStorage;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
public abstract class AbstractMonitorContainer extends AbstractMachineContainer {
private final MonitorBlockEntity monitor;
///////////////////////////////////////////////////////////////////
protected AbstractMonitorContainer(final MenuType<?> type, final int id, final Player player, final MonitorBlockEntity monitor, final IntPrecisionContainerData energyInfo) {
super(type, id, energyInfo);
this.monitor = monitor;
}
///////////////////////////////////////////////////////////////////
@Override
public void switchToInventory() {}
@Override
@Nullable
public VirtualMachine getVirtualMachine() { return null; }
public MonitorBlockEntity getMonitor() { return monitor; };
public boolean getPowerState() { return monitor.getPowerState(); }
public boolean isMounted() { return monitor.isMounted(); }
@Override
public void sendPowerStateToServer(final boolean value) {
Network.sendToServer(new MonitorPowerMessage(monitor, value));
}
@Override
public boolean stillValid(final Player player) {
if (!monitor.isValid()) {
return false;
}
final Level level = monitor.getLevel();
return level != null && stillValid(ContainerLevelAccess.create(level, monitor.getBlockPos()), player, Blocks.MONITOR.get());
}
///////////////////////////////////////////////////////////////////
protected static IntPrecisionContainerData createEnergyInfo(final IEnergyStorage energy, final CommonDeviceBusController busController) {
return new IntPrecisionContainerData.Server() {
@Override
public int getInt(final int index) {
return switch (index) {
case AbstractMachineContainer.ENERGY_STORED_INDEX -> energy.getEnergyStored();
case AbstractMachineContainer.ENERGY_CAPACITY_INDEX -> energy.getMaxEnergyStored();
case AbstractMachineContainer.ENERGY_CONSUMPTION_INDEX -> busController.getEnergyConsumption();
default -> 0;
};
}
@Override
public int getIntCount() {
return ENERGY_INFO_SIZE;
}
};
}
protected static IntPrecisionContainerData createEnergyInfo(final IEnergyStorage energy) {
return new IntPrecisionContainerData.Server() {
@Override
public int getInt(final int index) {
return switch (index) {
case AbstractMachineContainer.ENERGY_STORED_INDEX -> energy.getEnergyStored();
case AbstractMachineContainer.ENERGY_CAPACITY_INDEX -> energy.getMaxEnergyStored();
default -> 0;
};
}
@Override
public int getIntCount() {
return ENERGY_INFO_SIZE;
}
};
}
}

View File

@@ -18,6 +18,7 @@ public final class Containers {
public static final RegistryObject<MenuType<ComputerInventoryContainer>> COMPUTER = CONTAINERS.register("computer", () -> IForgeMenuType.create(ComputerInventoryContainer::createClient));
public static final RegistryObject<MenuType<ComputerTerminalContainer>> COMPUTER_TERMINAL = CONTAINERS.register("computer_terminal", () -> IForgeMenuType.create(ComputerTerminalContainer::createClient));
public static final RegistryObject<MenuType<MonitorDisplayContainer>> MONITOR = CONTAINERS.register("monitor", () -> IForgeMenuType.create(MonitorDisplayContainer::createClient));
public static final RegistryObject<MenuType<RobotInventoryContainer>> ROBOT = CONTAINERS.register("robot", () -> IForgeMenuType.create(RobotInventoryContainer::createClient));
public static final RegistryObject<MenuType<RobotTerminalContainer>> ROBOT_TERMINAL = CONTAINERS.register("robot_terminal", () -> IForgeMenuType.create(RobotTerminalContainer::createClient));
public static final RegistryObject<MenuType<NetworkTunnelContainer>> NETWORK_TUNNEL = CONTAINERS.register("network_tunnel", () -> IForgeMenuType.create(NetworkTunnelContainer::createClient));

View File

@@ -0,0 +1,50 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.container;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.bus.CommonDeviceBusController;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.energy.IEnergyStorage;
import net.minecraftforge.network.NetworkHooks;
public final class MonitorDisplayContainer extends AbstractMonitorContainer {
public static void createServer(final MonitorBlockEntity monitor, final IEnergyStorage energy, final ServerPlayer player) {
NetworkHooks.openScreen(player, new MenuProvider() {
@Override
public Component getDisplayName() {
return Component.translatable(monitor.getBlockState().getBlock().getDescriptionId());
}
@Override
public AbstractContainerMenu createMenu(final int id, final Inventory inventory, final Player player) {
return new MonitorDisplayContainer(id, player, monitor, createEnergyInfo(energy));
}
}, monitor.getBlockPos());
}
public static MonitorDisplayContainer createClient(final int id, final Inventory inventory, final FriendlyByteBuf data) {
final BlockPos pos = data.readBlockPos();
final BlockEntity blockEntity = inventory.player.level().getBlockEntity(pos);
if (blockEntity instanceof final MonitorBlockEntity monitor) {
return new MonitorDisplayContainer(id, inventory.player, monitor, createClientEnergyInfo());
}
throw new IllegalArgumentException();
}
///////////////////////////////////////////////////////////////////
private MonitorDisplayContainer(final int id, final Player player, final MonitorBlockEntity monitor, final IntPrecisionContainerData energyInfo) {
super(Containers.MONITOR.get(), id, player, monitor, energyInfo);
}
}

View File

@@ -5,14 +5,24 @@ package li.cil.oc2.common.integration.jei;
import li.cil.oc2.api.API;
import li.cil.oc2.client.gui.AbstractMachineInventoryScreen;
import li.cil.oc2.client.gui.AbstractMachineTerminalScreen;
import li.cil.oc2.common.block.ComputerBlock;
import li.cil.oc2.common.item.Items;
import mezz.jei.api.IModPlugin;
import mezz.jei.api.JeiPlugin;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.gui.handlers.IGuiContainerHandler;
import mezz.jei.api.ingredients.IIngredientType;
import mezz.jei.api.registration.IGuiHandlerRegistration;
import mezz.jei.api.registration.IRecipeRegistration;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@JeiPlugin
public class ExtraGuiAreasJEIPlugin implements IModPlugin {
@@ -21,6 +31,14 @@ public class ExtraGuiAreasJEIPlugin implements IModPlugin {
return new ResourceLocation(API.MOD_ID, "extra_gui_areas");
}
@Override
public void registerRecipes(final IRecipeRegistration registration)
{
HashSet<ItemStack> removals = new HashSet<ItemStack>();
removals.add(ComputerBlock.getPreconfiguredComputer());
registration.getIngredientManager().removeIngredientsAtRuntime(VanillaTypes.ITEM_STACK, removals);
}
@Override
public void registerGuiHandlers(final IGuiHandlerRegistration registration) {
registration.addGenericGuiContainerHandler(AbstractMachineInventoryScreen.class, new AbstractMachineInventoryScreenGuiContainerHandler());

View File

@@ -37,41 +37,8 @@ public final class ItemGroup {
output.accept(Items.BUS_CABLE.get());
output.accept(Items.BUS_INTERFACE.get());
output.accept(Items.CHARGER.get());
// Computer With Flash
ItemStack computerWithFlash = new ItemStack(Blocks.COMPUTER.get());
final CompoundTag itemsTag = NBTUtils.getOrCreateChildTag(computerWithFlash.getOrCreateTag(), BLOCK_ENTITY_TAG_NAME_IN_ITEM, ITEMS_TAG_NAME);
itemsTag.put(key(DeviceTypes.FLASH_MEMORY), makeInventoryTag(
new ItemStack(Items.FLASH_MEMORY_CUSTOM.get())
));
output.accept(computerWithFlash);
// Preconfigured Computer
ItemStack preconfiguredComputer = new ItemStack(Blocks.COMPUTER.get());
final CompoundTag preconfiguredItemsTag = NBTUtils.getOrCreateChildTag(preconfiguredComputer.getOrCreateTag(), BLOCK_ENTITY_TAG_NAME_IN_ITEM, ITEMS_TAG_NAME);
preconfiguredItemsTag.put(key(DeviceTypes.FLASH_MEMORY), makeInventoryTag(
new ItemStack(Items.FLASH_MEMORY_CUSTOM.get())
));
preconfiguredItemsTag.put(key(DeviceTypes.MEMORY), makeInventoryTag(
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get()),
new ItemStack(Items.MEMORY_LARGE.get())
));
preconfiguredItemsTag.put(key(DeviceTypes.HARD_DRIVE), makeInventoryTag(
new ItemStack(Items.HARD_DRIVE_CUSTOM.get())
));
preconfiguredItemsTag.put(key(DeviceTypes.CARD), makeInventoryTag(
new ItemStack(Items.NETWORK_INTERFACE_CARD.get())
));
preconfiguredComputer.setHoverName(text("block.{mod}.computer.preconfigured"));
output.accept(preconfiguredComputer);
// Normal Items Again
output.accept(ComputerBlock.getComputerWithFlash());
output.accept(ComputerBlock.getPreconfiguredComputer());
output.accept(Items.CREATIVE_ENERGY.get());
output.accept(Items.DISK_DRIVE.get());
output.accept(Items.FLASH_MEMORY_FLASHER.get());
@@ -82,7 +49,7 @@ public final class ItemGroup {
output.accept(Items.REDSTONE_INTERFACE.get());
output.accept(Items.WRENCH.get());
output.accept(Items.MANUAL.get());
output.accept(Items.ROBOT.get());
output.accept(RobotItem.getRobotWithFlash());
output.accept(Items.NETWORK_CABLE.get());
output.accept(Items.MEMORY_SMALL.get());
output.accept(Items.MEMORY_MEDIUM.get());

View File

@@ -28,6 +28,7 @@ public final class Items {
public static final RegistryObject<BusInterfaceItem> BUS_INTERFACE = register("bus_interface", BusInterfaceItem::new);
public static final RegistryObject<Item> CHARGER = register(Blocks.CHARGER, ChargerItem::new);
public static final RegistryObject<Item> COMPUTER = register(Blocks.COMPUTER);
public static final RegistryObject<Item> MONITOR = register(Blocks.MONITOR);
public static final RegistryObject<Item> CREATIVE_ENERGY = register(Blocks.CREATIVE_ENERGY);
public static final RegistryObject<Item> DISK_DRIVE = register(Blocks.DISK_DRIVE);
public static final RegistryObject<Item> FLASH_MEMORY_FLASHER = register(Blocks.FLASH_MEMORY_FLASHER);

View File

@@ -123,8 +123,8 @@ public final class RobotItem extends ModItem {
});
}
private ItemStack getRobotWithFlash() {
final ItemStack robot = new ItemStack(this);
public static ItemStack getRobotWithFlash() {
final ItemStack robot = new ItemStack(Items.ROBOT.get());
final CompoundTag itemsTag = NBTUtils.getOrCreateChildTag(robot.getOrCreateTag(), API.MOD_ID, ITEMS_TAG_NAME);
itemsTag.put(key(DeviceTypes.FLASH_MEMORY), makeInventoryTag(

View File

@@ -8,8 +8,11 @@ import net.minecraft.client.multiplayer.MultiPlayerGameMode;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.DispensibleContainerItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;

View File

@@ -0,0 +1,316 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network;
import li.cil.oc2.api.API;
import li.cil.oc2.common.Config;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.network.message.MonitorFramebufferMessage;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.server.ServerStoppedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
/**
* Mostly round-robin load balancer for allowing projectors to send data to clients.
* <p>
* We try to satisfy two limits here:
* <ul>
* <li>
* Server side, limiting overall bandwidth consumed by projectors.
* </li>
* <li>
* Client side, limiting per-client bandwidth consumed by projectors.
* </li>
* </ul>
* <p>
* To achieve this, there's a global budget and a per-projector skip count. The global budget
* controls overall data sent from the server. The skip counts modulate the round-robin behaviour
* of the load balancer. For example, projectors further away from their closest player will get
* a penalty, as will projectors with a large number of players watching them.
*/
@Mod.EventBusSubscriber(modid = API.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class MonitorLoadBalancer {
private static final long CACHE_EXPIRES_AFTER = 2000; /* In milliseconds */
/**
* Maps projectors to their specific info, i.e. players watching them and sending state.
* <p>
* This is used to let projectors register new frames to be sent, and to register new players
* with already being tracked projectors.
* <p>
* Player watching state is expired manually, by checking the last update time for players in
* the info instance every tick. This is required in case two players start watching a projector
* but then only one keeps watching it. In that case, we want to still remove the other player
* from the projector info, but keep the info for the still watching player.
*/
private static final Map<MonitorBlockEntity, ProjectorInfo> PROJECTOR_INFO = new HashMap<>();
/**
* Global byte budget for sending stuff to clients. This is filled up every tick and consumed
* when sending packets to clients. We only send stuff when budget is non-negative.
*/
private static final AtomicInteger BUDGET = new AtomicInteger(getMaxBudget());
/**
* Pointer into our circular projector linked list pointing to the projector we last sent a packet for.
* <p>
* At the same time represents the head of our doubly-linked, circular list of projectors. This list is
* used to do the round-robin load balancing, by advancing it to the next projector until we find one
* that can send something every tick.
*/
@Nullable private static ProjectorInfo lastSender;
///////////////////////////////////////////////////////////////////
/**
* Updates timestamp of a player currently watching a projector.
*/
public static void updateWatcher(final MonitorBlockEntity monitor, final ServerPlayer player) {
PROJECTOR_INFO
.computeIfAbsent(monitor, MonitorLoadBalancer::addProjectorInfo)
.handleWatchedBy(player);
}
/**
* Notifies the load balancer that a projector has data to send.
* <p>
* Ignored if there are no players watching the projector.
*/
public static void offerFrame(final MonitorBlockEntity monitor, final Supplier<ByteBuffer> messageSupplier) {
final ProjectorInfo info = PROJECTOR_INFO.get(monitor);
if (info != null) {
info.nextFrameSupplier = messageSupplier;
}
}
/**
* Expires cached values. Checks if we can send something, and if so starts async
* generation of the package to send.
*/
@SubscribeEvent
public static void handleServerTick(final TickEvent.ServerTickEvent event) {
updateCache();
if (BUDGET.updateAndGet(MonitorLoadBalancer::replenishBudget) > 0) {
sendNextReadyPacket();
}
}
/**
* Cleanup when server stops.
*/
@SubscribeEvent
public static void handleServerStopped(final ServerStoppedEvent event) {
PROJECTOR_INFO.clear();
}
///////////////////////////////////////////////////////////////////
private static int getMaxBudget() {
// We allow over-budgeting projectors to some degree, to allow short bursts of larger frame changes.
// Otherwise, this would be divided by twenty, since we attempt to send every tick.
return Config.projectorAverageMaxBytesPerSecond / 2;
}
private static int replenishBudget(final int budget) {
return Math.min(getMaxBudget(), budget + Math.max(1, Config.projectorAverageMaxBytesPerSecond / 20));
}
private static void updateCache() {
final Iterator<ProjectorInfo> iterator = PROJECTOR_INFO.values().iterator();
while (iterator.hasNext()) {
final ProjectorInfo info = iterator.next();
info.removeExpiredPlayers();
if (info.isNoLongerWatched()) {
iterator.remove();
removeProjectorInfo(info);
}
}
}
private static ProjectorInfo addProjectorInfo(final MonitorBlockEntity monitor) {
monitor.setRequiresKeyframe(); // When first watcher starts, immediately request keyframe.
final ProjectorInfo info = new ProjectorInfo(monitor.getBlockPos());
if (lastSender == null) {
// No sender yet, start the circle.
lastSender = info;
} else {
// Just add after last sender.
lastSender.add(info);
}
return info;
}
private static void removeProjectorInfo(final ProjectorInfo info) {
if (lastSender == info) {
if (lastSender.next == lastSender) {
// Last element in list, clear list.
lastSender = null;
} else {
// Shift current entry to next.
lastSender = info.next;
}
}
info.remove();
}
private static void sendNextReadyPacket() {
if (lastSender == null) {
return;
}
final ProjectorInfo start = lastSender;
do {
lastSender = lastSender.next;
if (lastSender.sendIfReady()) {
return;
}
} while (lastSender != start);
}
///////////////////////////////////////////////////////////////////
/**
* Tracks info for a single projector. This class is an entry in a circular double linked list,
* i.e. the last entry will always point back to the first entry, which makes looping over it
* for the round-robin load-balancing more comfortable.
*/
private static class ProjectorInfo {
private static final ExecutorService ENCODER_WORKERS = Executors.newCachedThreadPool(r -> {
final Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("Projector Frame Encoder");
return thread;
});
/**
* Pointers to next and previous entries in linked list. May point to this if it's the only entry.
*/
private ProjectorInfo next, previous;
/**
* Position of the projector this info is for in the world.
*/
private final BlockPos projectorPos;
/**
* The players watching this projector, to know where to send the data and how strongly to penalize.
*/
private final WeakHashMap<ServerPlayer, Long> players = new WeakHashMap<>();
/**
* The current penalty, in the form of rounds in the round-robin to skip.
*/
private int skipCount;
@Nullable private Supplier<ByteBuffer> nextFrameSupplier;
@Nullable private Future<?> runningEncode;
public ProjectorInfo(final BlockPos projectorPos) {
next = previous = this;
this.projectorPos = projectorPos;
}
public void add(final ProjectorInfo info) {
info.next = next;
next.previous = info;
next = info;
info.previous = this;
}
public void remove() {
if (previous == null) {
return;
}
previous.next = next;
next.previous = previous;
previous = null;
next = null;
}
public void handleWatchedBy(final ServerPlayer player) {
players.put(player, System.currentTimeMillis());
}
public void removeExpiredPlayers() {
players.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > CACHE_EXPIRES_AFTER);
}
public boolean isNoLongerWatched() {
return players.isEmpty();
}
public boolean sendIfReady() {
if (skipCount > 0) {
skipCount--;
return false;
}
final boolean isReady = !players.isEmpty() && nextFrameSupplier != null && (runningEncode == null || runningEncode.isDone());
if (isReady) {
sendAsync();
updateSkipCount();
}
return isReady;
}
private void sendAsync() {
assert nextFrameSupplier != null;
final Supplier<ByteBuffer> frameSupplier = nextFrameSupplier;
nextFrameSupplier = null;
assert runningEncode == null || runningEncode.isDone();
runningEncode = ENCODER_WORKERS.submit(() -> {
final ByteBuffer frame = frameSupplier.get();
if (frame == null) {
return;
}
final int budgetCost = frame.limit() * players.size();
BUDGET.accumulateAndGet(budgetCost, (budget, cost) -> budget - cost);
final MonitorFramebufferMessage message = new MonitorFramebufferMessage(projectorPos, frame);
for (final ServerPlayer player : players.keySet()) {
Network.sendToClient(message, player);
}
});
}
private void updateSkipCount() {
skipCount = 0;
double closestPlayerDistanceSqr = Double.MAX_VALUE;
final Vec3 blockCenter = Vec3.atCenterOf(projectorPos);
for (final ServerPlayer player : players.keySet()) {
skipCount++;
final double distance = player.distanceToSqr(blockCenter);
closestPlayerDistanceSqr = Math.min(closestPlayerDistanceSqr, distance);
}
final double closestPlayerDistance = Math.sqrt(closestPlayerDistanceSqr);
if (closestPlayerDistance > 16) {
skipCount++;
}
}
}
}

View File

@@ -44,6 +44,8 @@ public final class Network {
registerMessage(ComputerBusStateMessage.class, ComputerBusStateMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(ComputerBootErrorMessage.class, ComputerBootErrorMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(ComputerPowerMessage.class, ComputerPowerMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MonitorPowerMessage.class, MonitorPowerMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MonitorPowerMessageForwarded.class, MonitorPowerMessageForwarded::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(OpenComputerInventoryMessage.class, OpenComputerInventoryMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(OpenComputerTerminalMessage.class, OpenComputerTerminalMessage::new, NetworkDirection.PLAY_TO_SERVER);
@@ -78,12 +80,18 @@ public final class Network {
registerMessage(NetworkInterfaceCardConfigurationMessage.class, NetworkInterfaceCardConfigurationMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(NetworkTunnelLinkMessage.class, NetworkTunnelLinkMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MonitorRequestFramebufferMessage.class, MonitorRequestFramebufferMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MonitorFramebufferMessage.class, MonitorFramebufferMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(ProjectorRequestFramebufferMessage.class, ProjectorRequestFramebufferMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(ProjectorFramebufferMessage.class, ProjectorFramebufferMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(ProjectorStateMessage.class, ProjectorStateMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(MonitorStateMessage.class, MonitorStateMessage::new, NetworkDirection.PLAY_TO_CLIENT);
registerMessage(KeyboardInputMessage.class, KeyboardInputMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MonitorInputMessage.class, MonitorInputMessage::new, NetworkDirection.PLAY_TO_SERVER);
registerMessage(MultipartMessage.class, MultipartMessage::new, NetworkDirection.PLAY_TO_SERVER);
MultipartMessage.registerMessage(ImportedFileMessage.class, ImportedFileMessage::new);

View File

@@ -0,0 +1,40 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import java.nio.ByteBuffer;
public abstract class AbstractTerminalDisplayMessage extends AbstractMessage {
protected BlockPos pos;
protected byte[] data;
///////////////////////////////////////////////////////////////////
protected AbstractTerminalDisplayMessage(final MonitorBlockEntity monitor, final ByteBuffer data) {
this.pos = monitor.getBlockPos();
this.data = data.array();
}
protected AbstractTerminalDisplayMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
data = buffer.readByteArray();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeByteArray(data);
}
}

View File

@@ -4,6 +4,7 @@ package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import li.cil.oc2.common.network.Network;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;

View File

@@ -0,0 +1,53 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
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;
import java.nio.ByteBuffer;
public final class MonitorFramebufferMessage extends AbstractMessage {
private BlockPos pos;
private ByteBuffer frame;
///////////////////////////////////////////////////////////////////
public MonitorFramebufferMessage(final BlockPos projectorPos, final ByteBuffer frame) {
this.pos = projectorPos;
this.frame = frame;
}
public MonitorFramebufferMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
frame = ByteBuffer.allocateDirect(buffer.readVarInt());
buffer.readBytes(frame);
frame.flip();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeVarInt(frame.limit());
buffer.writeBytes(frame);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withClientBlockEntityAt(pos, MonitorBlockEntity.class,
monitor -> monitor.applyNextFrameClient(frame));
}
}

View File

@@ -0,0 +1,52 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.KeyboardBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
public final class MonitorInputMessage extends AbstractMessage {
private BlockPos pos;
private int keycode;
private boolean isDown;
///////////////////////////////////////////////////////////////////
public MonitorInputMessage(final MonitorBlockEntity keyboard, final int keycode, final boolean isDown) {
this.pos = keyboard.getBlockPos();
this.keycode = keycode;
this.isDown = isDown;
}
public MonitorInputMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
keycode = buffer.readVarInt();
isDown = buffer.readBoolean();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeVarInt(keycode);
buffer.writeBoolean(isDown);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, MonitorBlockEntity.class,
(player, monitor) -> monitor.handleInput(keycode, isDown));
}
}

View File

@@ -0,0 +1,56 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.ComputerBlockEntity;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import li.cil.oc2.common.network.Network;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
public final class MonitorPowerMessage extends AbstractMessage {
private BlockPos pos;
private boolean power;
///////////////////////////////////////////////////////////////////
public MonitorPowerMessage(final MonitorBlockEntity monitor, final boolean power) {
this.pos = monitor.getBlockPos();
this.power = power;
}
public MonitorPowerMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
power = buffer.readBoolean();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeBoolean(power);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntityForInteraction(context, pos, MonitorBlockEntity.class,
(player, monitor) -> {
if (power) {
monitor.start();
} else {
monitor.stop();
}
Network.sendToClientsTrackingBlockEntity(new MonitorPowerMessageForwarded(monitor, power), monitor);
});
}
}

View File

@@ -0,0 +1,53 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
public final class MonitorPowerMessageForwarded extends AbstractMessage {
private BlockPos pos;
private boolean power;
///////////////////////////////////////////////////////////////////
public MonitorPowerMessageForwarded(final MonitorBlockEntity monitor, final boolean power) {
this.pos = monitor.getBlockPos();
this.power = power;
}
public MonitorPowerMessageForwarded(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
power = buffer.readBoolean();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeBoolean(power);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withClientBlockEntityAt(pos, MonitorBlockEntity.class,
(monitor) -> {
if (power) {
monitor.start();
} else {
monitor.stop();
}
});
}
}

View File

@@ -0,0 +1,46 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
import li.cil.oc2.common.blockentity.ProjectorBlockEntity;
import li.cil.oc2.common.network.MessageUtils;
import li.cil.oc2.common.network.MonitorLoadBalancer;
import li.cil.oc2.common.network.ProjectorLoadBalancer;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
public final class MonitorRequestFramebufferMessage extends AbstractMessage {
private BlockPos pos;
///////////////////////////////////////////////////////////////////
public MonitorRequestFramebufferMessage(final MonitorBlockEntity projector) {
this.pos = projector.getBlockPos();
}
public MonitorRequestFramebufferMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withNearbyServerBlockEntity(context, pos, MonitorBlockEntity.class,
(player, monitor) -> MonitorLoadBalancer.updateWatcher(monitor, player));
}
}

View File

@@ -0,0 +1,52 @@
/* SPDX-License-Identifier: MIT */
package li.cil.oc2.common.network.message;
import li.cil.oc2.common.blockentity.MonitorBlockEntity;
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 MonitorStateMessage extends AbstractMessage {
private BlockPos pos;
private boolean isMounted;
private boolean hasEnergy;
///////////////////////////////////////////////////////////////////
public MonitorStateMessage(final MonitorBlockEntity monitor, final boolean isMounted, final boolean hasEnergy) {
this.pos = monitor.getBlockPos();
this.isMounted = isMounted;
this.hasEnergy = hasEnergy;
}
public MonitorStateMessage(final FriendlyByteBuf buffer) {
super(buffer);
}
///////////////////////////////////////////////////////////////////
@Override
public void fromBytes(final FriendlyByteBuf buffer) {
pos = buffer.readBlockPos();
isMounted = buffer.readBoolean();
hasEnergy = buffer.readBoolean();
}
@Override
public void toBytes(final FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeBoolean(isMounted);
buffer.writeBoolean(hasEnergy);
}
///////////////////////////////////////////////////////////////////
@Override
protected void handleMessage(final NetworkEvent.Context context) {
MessageUtils.withClientBlockEntityAt(pos, MonitorBlockEntity.class,
monitor -> monitor.applyMonitorStateClient(isMounted, hasEnergy));
}
}

View File

@@ -13,7 +13,6 @@ import li.cil.sedna.device.serial.UART16550A;
import li.cil.sedna.device.virtio.VirtIOConsoleDevice;
import li.cil.sedna.device.virtio.VirtIOFileSystemDevice;
import java.util.OptionalLong;
import java.util.function.Function;
public final class BuiltinDevices {

View File

@@ -25,6 +25,7 @@ public final class ModBlockStateProvider extends BlockStateProvider {
private static final ResourceLocation CABLE_STRAIGHT_MODEL = new ResourceLocation(API.MOD_ID, "block/cable_straight");
private static final ResourceLocation CHARGER_MODEL = new ResourceLocation(API.MOD_ID, "block/charger");
private static final ResourceLocation COMPUTER_MODEL = new ResourceLocation(API.MOD_ID, "block/computer");
private static final ResourceLocation MONITOR_MODEL = new ResourceLocation(API.MOD_ID, "block/monitor");
private static final ResourceLocation DISK_DRIVE_MODEL = new ResourceLocation(API.MOD_ID, "block/disk_drive");
private static final ResourceLocation KEYBOARD_MODEL = new ResourceLocation(API.MOD_ID, "block/keyboard");
private static final ResourceLocation NETWORK_CONNECTOR_MODEL = new ResourceLocation(API.MOD_ID, "block/network_connector");
@@ -42,6 +43,7 @@ public final class ModBlockStateProvider extends BlockStateProvider {
protected void registerStatesAndModels() {
horizontalBlock(Blocks.CHARGER, Items.CHARGER, CHARGER_MODEL);
horizontalBlock(Blocks.COMPUTER, Items.COMPUTER, COMPUTER_MODEL);
horizontalBlock(Blocks.MONITOR, Items.MONITOR, MONITOR_MODEL);
simpleBlock(Blocks.CREATIVE_ENERGY, Items.CREATIVE_ENERGY);
horizontalBlock(Blocks.DISK_DRIVE, Items.DISK_DRIVE, DISK_DRIVE_MODEL);
horizontalBlock(Blocks.KEYBOARD, Items.KEYBOARD, KEYBOARD_MODEL);

View File

@@ -105,10 +105,7 @@ public final class WrenchRecipeBuilder {
public void save(final Consumer<FinishedRecipe> consumerIn, final ResourceLocation id) {
this.validate(id);
this.advancementBuilder.parent(new ResourceLocation("recipes/root")).addCriterion("has_the_recipe", RecipeUnlockedTrigger.unlocked(id)).rewards(AdvancementRewards.Builder.recipe(id)).requirements(RequirementsStrategy.OR);
//final CreativeModeTab itemCategory = this.result.; TODO: FIX THIS
//if (itemCategory != null) {
// consumerIn.accept(new WrenchRecipeBuilder.Result(id, this.result, this.count, this.group == null ? "" : this.group, this.ingredients, this.advancementBuilder, new ResourceLocation(id.getNamespace(), "recipes/" + itemCategory.getRecipeFolderName() + "/" + id.getPath())));
//}
consumerIn.accept(new WrenchRecipeBuilder.Result(id, this.result, this.count, "", this.ingredients, this.advancementBuilder, new ResourceLocation(id.getNamespace(), "recipes/misc/" + id.getPath())));
}
private void validate(final ResourceLocation id) {

View File

@@ -0,0 +1,19 @@
{
"variants": {
"facing=north": {
"model": "oc2:block/monitor"
},
"facing=south": {
"model": "oc2:block/monitor",
"y": 180
},
"facing=west": {
"model": "oc2:block/monitor",
"y": 270
},
"facing=east": {
"model": "oc2:block/monitor",
"y": 90
}
}
}

View File

@@ -4,6 +4,8 @@
"block.oc2.computer": "Computer",
"block.oc2.computer.desc": "Runs software from Flash Memory and Hard Disks.",
"block.oc2.computer.preconfigured": "Preconfigured Computer",
"block.oc2.monitor": "Monitor",
"block.oc2.monitor.desc": "A special framebuffer based monitor for your computer.",
"block.oc2.bus_cable": "Bus Cable",
"block.oc2.bus_cable.desc": "Connects Bus Interfaces.",
"block.oc2.network_connector": "Network Connector",

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

View File

@@ -27,7 +27,7 @@
"nbt": {
"BlockEntityTag": {
"items": {
"oc2:flash_memory": {
"translation{key='gui.oc2.device_type.flash_memory', args=[]}": {
"Items": [
{
"Slot": 0,

View File

@@ -27,7 +27,7 @@
"nbt": {
"oc2": {
"items": {
"oc2:flash_memory": {
"translation{key='gui.oc2.device_type.flash_memory', args=[]}": {
"Items": [
{
"Slot": 0,

File diff suppressed because one or more lines are too long