447 lines
13 KiB
Scala

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
}