mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
447 lines
13 KiB
Scala
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
|
|
}
|