package ocelot.desktop.node.nodes import ocelot.desktop.color.{Color, IntColor} import ocelot.desktop.geometry.{Rect2D, Size2D} import ocelot.desktop.graphics.Texture.MinFilteringMode import ocelot.desktop.graphics.{Graphics, IconSource, ScreenViewport} import ocelot.desktop.node.Node.{HighlightThickness, NoHighlightSize, Size} import ocelot.desktop.node.nodes.ScreenNode.{FontHeight, FontWidth, Margin} import ocelot.desktop.node.{EntityNode, LabeledEntityNode, Node, WindowedNode} import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.ClickEvent import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.{ScreenAspectRatioDialog, TickUpdatable} import ocelot.desktop.util.TierColor import ocelot.desktop.windows.ScreenWindow import ocelot.desktop.{OcelotDesktop, Settings} import totoro.ocelot.brain.entity.{Keyboard, Screen} import totoro.ocelot.brain.nbt.NBTTagCompound import totoro.ocelot.brain.util.PackedColor class ScreenNode(val screen: Screen) extends EntityNode(screen) with LabeledEntityNode with WindowedNode[ScreenWindow] with TickUpdatable { // NOTE: whenever you access screen's TextBuffer methods, make sure to synchronize on `screen`: // computers may update the screen data concurrently via direct methods! // conversely, if a property is only modified via indirect methods, no additional synchronization is necessary. override def minimumSize: Size2D = Size2D( Size * screen.aspectRatio._1, Size * screen.aspectRatio._2, ) override def maximumSize: Size2D = minimumSize // ------------------------------- private var viewport: Option[ScreenViewport] = Some(new ScreenViewport(UiHandler.graphics, 160 * 8, 160 * 8)) // the cached contents of the screen, updated every tick to synchronize with the TPS rate private var (colorBuffer, textBuffer) = screen.synchronized { ( Array.fill[Short](screen.getHeight, screen.getWidth)(0), Array.fill[Int](screen.getHeight, screen.getWidth)(0x20), ) } def screenWidth: Int = textBuffer.headOption.map(_.length).getOrElse(0) def screenHeight: Int = textBuffer.length private var bufferRendered = false copyBuffer() private var keyboard: Option[Keyboard] = None private val keyboardNBTKey: String = "keyboard" override def load(nbt: NBTTagCompound): Unit = { super.load(nbt) if (nbt.hasKey(keyboardNBTKey)) { keyboard = OcelotDesktop.workspace.entityByAddress(nbt.getString(keyboardNBTKey)).map(_.asInstanceOf[Keyboard]) } } override def save(nbt: NBTTagCompound): Unit = { super.save(nbt) if (keyboard.isDefined) { nbt.setString(keyboardNBTKey, keyboard.get.node.address) } } override def dispose(): Unit = { super.dispose() keyboard.foreach(OcelotDesktop.workspace.remove(_)) viewport.foreach(_.freeResource()) viewport = None } def attachKeyboard(): Unit = { detachKeyboard() val kbd = new Keyboard OcelotDesktop.workspace.add(kbd) screen.connect(kbd) keyboard = Some(kbd) } def detachKeyboard(): Unit = { if (keyboard.isDefined) { screen.disconnect(keyboard.get) OcelotDesktop.workspace.remove(keyboard.get) keyboard = None } } override def iconSource: IconSource = IconSource.Nodes.Screen.Standalone override def iconColor: Color = TierColor.get(screen.tier) override def rotatable: Boolean = true override def setupContextMenu(menu: ContextMenu, event: ClickEvent): Unit = { // no synchronization here: the methods to turn the screen on/off are indirect. if (screen.getPowerState) { menu.addEntry(ContextMenuEntry("Turn off", IconSource.Icons.Power) { screen.setPowerState(false) }) } else { menu.addEntry(ContextMenuEntry("Turn on", IconSource.Icons.Power) { screen.setPowerState(true) }) } menu.addEntry(ContextMenuEntry("Set aspect ratio", IconSource.Icons.AspectRatio) { new ScreenAspectRatioDialog(this).show() }) if (keyboard.isDefined) { menu.addEntry(ContextMenuEntry("Remove keyboard", IconSource.Icons.KeyboardOff) { detachKeyboard() }) } else { menu.addEntry(ContextMenuEntry("Add keyboard", IconSource.Icons.Keyboard) { attachKeyboard() }) } menu.addSeparator() super.setupContextMenu(menu, event) } private def drawScreenTexture(needsMipmap: Boolean): Unit = { if (!bufferRendered) { for (viewport <- viewport) { viewport.renderWith { val width = (screenWidth * FontWidth).toInt val height = (screenHeight * FontHeight).toInt viewport.resize(width, height) var color: Short = 0 for (y <- 0 until screenHeight) { for (x <- 0 until screenWidth) { if (x == 0 || viewport.font.charWidth(textBuffer(y)(x - 1)) != 16) { color = colorBuffer(y)(x) // no synchronization here: the color format cannot be changed via direct methods. viewport.background = IntColor(PackedColor.unpackBackground(color, screen.data.format)) viewport.foreground = IntColor(PackedColor.unpackForeground(color, screen.data.format)) viewport.char(x * FontWidth, y * FontHeight, textBuffer(y)(x)) } } } } } bufferRendered = true } for (viewport <- viewport if needsMipmap) { viewport.generateMipmap() } } def drawScreenData( g: Graphics, startX: Float, startY: Float, scaleX: Float, scaleY: Float, filteringMode: MinFilteringMode, ): Unit = { for (viewport <- viewport) { g.save() g.scale(scaleX, scaleY) val startXScaled = startX / scaleX val startYScaled = startY / scaleY drawScreenTexture(filteringMode.needsMipmap) val bounds = Rect2D(startXScaled, startYScaled, viewport.width, viewport.height) g.blitScreenViewport(viewport, bounds, filteringMode = filteringMode) g.restore() } } override def draw(g: Graphics): Unit = { drawHighlight(g) // Drawing multiblock screen sprite def drawScreenPart(icon: IconSource, x: Float, y: Float, width: Float, height: Float): Unit = { g.sprite( icon, x, y, width, height, iconColor, ) } val aspectRatioWidth = screen.aspectRatio._1.toInt val aspectRatioHeight = screen.aspectRatio._2.toInt // 1 x n if (aspectRatioWidth == 1) { // 1 x 1 if (aspectRatioHeight == 1) { drawScreenPart( IconSource.Nodes.Screen.Standalone, position.x + HighlightThickness, position.y + HighlightThickness, NoHighlightSize, NoHighlightSize, ) } // 1 x n else { // Top drawScreenPart( IconSource.Nodes.Screen.ColumnTop, position.x + HighlightThickness, position.y + HighlightThickness, NoHighlightSize, Size - HighlightThickness, ) // Middle for (y <- 1 until aspectRatioHeight - 1) { drawScreenPart( IconSource.Nodes.Screen.ColumnMiddle, position.x + HighlightThickness, position.y + y * Size, NoHighlightSize, Size, ) } // Bottom drawScreenPart( IconSource.Nodes.Screen.ColumnBottom, position.x + HighlightThickness, position.y + (aspectRatioHeight - 1) * Size, NoHighlightSize, NoHighlightSize, ) } } // n x n else { // n x 1 if (aspectRatioHeight == 1) { // Left drawScreenPart( IconSource.Nodes.Screen.RowLeft, position.x + HighlightThickness, position.y + HighlightThickness, Size - HighlightThickness, NoHighlightSize, ) // Middle for (x <- 1 until aspectRatioWidth - 1) { drawScreenPart( IconSource.Nodes.Screen.RowMiddle, position.x + x * Size, position.y + HighlightThickness, Size, NoHighlightSize, ) } // Right drawScreenPart( IconSource.Nodes.Screen.RowRight, position.x + (aspectRatioWidth - 1) * Size, position.y + HighlightThickness, Size - HighlightThickness, NoHighlightSize, ) } // n x n else { def drawLine( y: Float, height: Float, leftIcon: IconSource, middleIcon: IconSource, rightIcon: IconSource, ): Unit = { // Left drawScreenPart( leftIcon, position.x + HighlightThickness, y, Size, height, ) // Middle for (x <- 1 until aspectRatioWidth - 1) { drawScreenPart( middleIcon, position.x + x * Size, y, Size, height, ) } // Right drawScreenPart( rightIcon, position.x + (aspectRatioWidth - 1) * Size - HighlightThickness, y, Size, height, ) } // Top drawLine( position.y + HighlightThickness, Size - HighlightThickness, IconSource.Nodes.Screen.TopLeft, IconSource.Nodes.Screen.TopMiddle, IconSource.Nodes.Screen.TopRight, ) // Middle for (y <- 1 until aspectRatioHeight - 1) { drawLine( position.y + (y * Size), Size, IconSource.Nodes.Screen.MiddleLeft, IconSource.Nodes.Screen.Middle, IconSource.Nodes.Screen.MiddleRight, ) } // Bottom drawLine( position.y + (aspectRatioHeight - 1) * Size, Size - HighlightThickness, IconSource.Nodes.Screen.BottomLeft, IconSource.Nodes.Screen.BottomMiddle, IconSource.Nodes.Screen.BottomRight, ) } } // If screen is on // no synchronization here: the methods to turn the screen on/off are indirect. if (screen.getPowerState) { // If realtime rendering of screen data is allowed if (Settings.get.renderScreenDataOnNodes) { val virtualScreenBounds = Rect2D( position.x + Margin, position.y + Margin, size.width - Margin * 2, size.height - Margin * 2, ) // Black background rect g.rect( virtualScreenBounds.x, virtualScreenBounds.y, virtualScreenBounds.w, virtualScreenBounds.h, Color.Black, ) // Calculating pixel data bounds, so that they fit perfectly into the virtual screen val pixelDataSize = Size2D( screenWidth * FontWidth, screenHeight * FontHeight, ) val scale = Math.min( virtualScreenBounds.w / pixelDataSize.width, virtualScreenBounds.h / pixelDataSize.height, ) // Drawing pixel data drawScreenData( g, virtualScreenBounds.x + virtualScreenBounds.w / 2 - (pixelDataSize.width * scale) / 2, virtualScreenBounds.y + virtualScreenBounds.h / 2 - (pixelDataSize.height * scale) / 2, scale, scale, MinFilteringMode.NearestMipmapNearest, ) } // Drawing simple overlay otherwise else { g.sprite( IconSource.Nodes.Screen.PowerOnOverlay, position.x + HighlightThickness, position.y + HighlightThickness, NoHighlightSize, NoHighlightSize, ) } } } override def tickUpdate(): Unit = { super.tickUpdate() copyBuffer() } private def copyBuffer(): Unit = screen.synchronized { val newWidth = screen.getWidth val newHeight = screen.getHeight if (textBuffer.length != newHeight || textBuffer(0).length != newWidth) { // create new arrays colorBuffer = Array.tabulate(newHeight, newWidth)((y, x) => screen.data.color(y)(x)) textBuffer = Array.tabulate(newHeight, newWidth)((y, x) => screen.data.buffer(y)(x)) } else { // reuse existing arrays for (y <- 0 until newHeight) { for (x <- 0 until newWidth) { colorBuffer(y)(x) = screen.data.color(y)(x) textBuffer(y)(x) = screen.data.buffer(y)(x) } } } bufferRendered = false } override def createWindow(): ScreenWindow = new ScreenWindow(this) } object ScreenNode { val FontWidth = 8f val FontHeight = 16f val BorderSize = 8f // the contents of a screen are offset by a quarter of a texel in OpenComputers val Margin: Float = HighlightThickness + BorderSize + Node.Scale * 0.25f }