Monitor
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
169
src/main/java/li/cil/oc2/client/gui/MonitorDisplayWidget.java
Normal 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);
|
||||
}
|
||||
}
|
||||
195
src/main/java/li/cil/oc2/client/renderer/MonitorGUIRenderer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
151
src/main/java/li/cil/oc2/common/block/MonitorBlock.java
Normal 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;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package li.cil.oc2.common.bus.device;
|
||||
|
||||
import li.cil.oc2.common.bus.AbstractDeviceBusElement;
|
||||
|
||||
public class BlockDeviceBusElement extends AbstractDeviceBusElement {
|
||||
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
316
src/main/java/li/cil/oc2/common/network/MonitorLoadBalancer.java
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
19
src/main/resources/assets/oc2/blockstates/monitor.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
1
src/main/resources/assets/oc2/models/block/monitor.json
Normal file
3
src/main/resources/assets/oc2/models/item/monitor.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parent": "oc2:block/computer"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 527 B |
@@ -27,7 +27,7 @@
|
||||
"nbt": {
|
||||
"BlockEntityTag": {
|
||||
"items": {
|
||||
"oc2:flash_memory": {
|
||||
"translation{key='gui.oc2.device_type.flash_memory', args=[]}": {
|
||||
"Items": [
|
||||
{
|
||||
"Slot": 0,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"nbt": {
|
||||
"oc2": {
|
||||
"items": {
|
||||
"oc2:flash_memory": {
|
||||
"translation{key='gui.oc2.device_type.flash_memory', args=[]}": {
|
||||
"Items": [
|
||||
{
|
||||
"Slot": 0,
|
||||
|
||||