hud: add support for dynamic text

This commit is contained in:
Lukas 2021-07-04 18:14:00 +02:00
parent ad9f8cade5
commit b2178e2cfa
11 changed files with 231 additions and 26 deletions

View File

@ -19,6 +19,7 @@ import de.bixilon.minosoft.data.locale.minecraft.Translator
import de.bixilon.minosoft.data.text.RGBColor.Companion.asColor import de.bixilon.minosoft.data.text.RGBColor.Companion.asColor
import de.bixilon.minosoft.data.text.events.ClickEvent import de.bixilon.minosoft.data.text.events.ClickEvent
import de.bixilon.minosoft.data.text.events.HoverEvent import de.bixilon.minosoft.data.text.events.HoverEvent
import de.bixilon.minosoft.gui.rendering.hud.elements.text.HUDTextTranslator
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.KUtil.nullCast import de.bixilon.minosoft.util.KUtil.nullCast
import javafx.collections.ObservableList import javafx.collections.ObservableList
@ -141,6 +142,71 @@ class BaseComponent : ChatComponent {
} }
} }
constructor(translator: Translator?, hudTextTranslator: HUDTextTranslator, parent: TextComponent?, json: Map<String, *>) {
val currentParent: TextComponent?
var currentText = ""
(json["text"] as String?)?.let {
if (it.indexOf(ProtocolDefinition.TEXT_COMPONENT_SPECIAL_PREFIX_CHAR) != -1) {
parts.add(ChatComponent.of(it, translator, parent, hudTextTranslator = hudTextTranslator))
return
}
currentText = it
}
val color = (json["color"] as String?)?.let { colorName ->
if (colorName.startsWith("#")) {
colorName.asColor()
} else {
ChatCode.FORMATTING_CODES[colorName]?.nullCast<RGBColor>()
}
} ?: parent?.color
val formatting = parent?.formatting?.toMutableSet() ?: mutableSetOf()
formatting.addOrRemove(PreChatFormattingCodes.BOLD, (json["bold"] as Boolean?))
formatting.addOrRemove(PreChatFormattingCodes.ITALIC, (json["italic"] as Boolean?))
formatting.addOrRemove(PreChatFormattingCodes.UNDERLINED, (json["underlined"] as Boolean?))
formatting.addOrRemove(PreChatFormattingCodes.STRIKETHROUGH, (json["strikethrough"] as Boolean?))
formatting.addOrRemove(PreChatFormattingCodes.OBFUSCATED, (json["obfuscated"] as Boolean?))
val clickEvent = (json["clickEvent"] as Map<String, *>?)?.let { click -> ClickEvent(click) }
val hoverEvent = (json["hoverEvent"] as Map<String, *>?)?.let { click -> HoverEvent(click) }
val textComponent = MultiChatComponent(
message = currentText,
color = color,
formatting = formatting,
clickEvent = clickEvent,
hoverEvent = hoverEvent,
)
if (currentText.isNotEmpty()) {
parts.add(textComponent)
}
currentParent = textComponent
(json["extra"] as Array<*>?)?.let {
for (data in it) {
parts.add(ChatComponent.of(data, translator, currentParent, hudTextTranslator = hudTextTranslator))
}
}
(json["translate"] as String?)?.let {
val with = mutableListOf<Any>()
(json["with"] as List<*>?)?.let { withArray ->
for (part in withArray) {
with.add(part!!)
}
}
parts.add(translator?.translate(it, currentParent, *with.toTypedArray()) ?: ChatComponent.of(json["with"], translator, currentParent, hudTextTranslator = hudTextTranslator))
}
(json["data"] as String?)?.let {
parts.add(hudTextTranslator.translate(it, currentParent))
}
}
override val ansiColoredMessage: String override val ansiColoredMessage: String
get() { get() {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()

View File

@ -17,6 +17,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import de.bixilon.minosoft.data.locale.minecraft.Translator import de.bixilon.minosoft.data.locale.minecraft.Translator
import de.bixilon.minosoft.gui.rendering.hud.elements.text.HUDTextTranslator
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.scene.Node import javafx.scene.Node
@ -57,7 +58,7 @@ interface ChatComponent {
companion object { companion object {
@JvmOverloads @JvmOverloads
fun of(raw: Any?, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false): ChatComponent { fun of(raw: Any?, translator: Translator? = null, parent: TextComponent? = null, ignoreJson: Boolean = false, hudTextTranslator: HUDTextTranslator? = null): ChatComponent {
if (raw == null) { if (raw == null) {
return BaseComponent() return BaseComponent()
} }
@ -75,6 +76,16 @@ interface ChatComponent {
} }
return component return component
} }
is List<*> -> {
val component = BaseComponent()
for (part in raw) {
component.parts.add(of(part, translator, parent, hudTextTranslator = hudTextTranslator))
}
return component
}
is Map<*, *> -> {
return BaseComponent(translator, hudTextTranslator!!, parent, raw as Map<String, *>)
}
is JsonPrimitive -> raw.asString is JsonPrimitive -> raw.asString
else -> raw.toString() else -> raw.toString()
} }

View File

@ -35,6 +35,16 @@ class ClickEvent {
this.value = value this.value = value
} }
constructor(json: Map<String, *>) {
action = ClickEventActions[(json["action"] as String).lowercase()]
val primitive = json["value"]
value = if (primitive is Number) {
primitive
} else {
primitive.toString()
}
}
enum class ClickEventActions { enum class ClickEventActions {
OPEN_URL, OPEN_URL,
RUN_COMMAND, RUN_COMMAND,

View File

@ -45,6 +45,22 @@ class HoverEvent {
this.value = value this.value = value
} }
constructor(json: Map<String, *>) {
action = HoverEventActions.valueOf((json["action"] as String).uppercase(Locale.getDefault()))
var data: Any = json
json["value"]?.let {
data = it
}
json["contents"]?.let {
data = it
}
this.value = when (action) {
HoverEventActions.SHOW_TEXT -> ChatComponent.of(data)
HoverEventActions.SHOW_ENTITY -> EntityHoverData.deserialize(data)
else -> TODO("Don't know what todo with $action: $json")
}
}
enum class HoverEventActions { enum class HoverEventActions {
SHOW_TEXT, SHOW_TEXT,
SHOW_ITEM, SHOW_ITEM,

View File

@ -46,5 +46,10 @@ class EntityHoverData(
return EntityHoverData(json["id"].asString.asUUID(), type, ChatComponent.of(json["name"])) return EntityHoverData(json["id"].asString.asUUID(), type, ChatComponent.of(json["name"]))
} }
fun deserialize(data: Any): EntityHoverData {
val json = JsonParser.parseString(data as String).asJsonObject
return deserialize(json)
}
} }
} }

View File

@ -21,19 +21,22 @@ import de.bixilon.minosoft.gui.rendering.Renderer
import de.bixilon.minosoft.gui.rendering.RendererBuilder import de.bixilon.minosoft.gui.rendering.RendererBuilder
import de.bixilon.minosoft.gui.rendering.hud.atlas.HUDAtlasElement import de.bixilon.minosoft.gui.rendering.hud.atlas.HUDAtlasElement
import de.bixilon.minosoft.gui.rendering.hud.elements.primitive.HUDElement import de.bixilon.minosoft.gui.rendering.hud.elements.primitive.HUDElement
import de.bixilon.minosoft.gui.rendering.hud.elements.text.HUDTextTranslator
import de.bixilon.minosoft.gui.rendering.modding.events.ScreenResizeEvent import de.bixilon.minosoft.gui.rendering.modding.events.ScreenResizeEvent
import de.bixilon.minosoft.gui.rendering.shader.Shader import de.bixilon.minosoft.gui.rendering.shader.Shader
import de.bixilon.minosoft.modding.event.CallbackEventInvoker import de.bixilon.minosoft.modding.event.CallbackEventInvoker
import de.bixilon.minosoft.protocol.network.connection.PlayConnection import de.bixilon.minosoft.protocol.network.connection.PlayConnection
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.json.ResourceLocationJsonMap.toResourceLocationMap import de.bixilon.minosoft.util.json.ResourceLocationJsonMap.toResourceLocationMap
import de.bixilon.minosoft.util.task.time.TimeWorker
import de.bixilon.minosoft.util.task.time.TimeWorkerTask
import glm_.glm import glm_.glm
import glm_.mat4x4.Mat4 import glm_.mat4x4.Mat4
class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow) : Renderer { class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow) : Renderer {
private var prepareNext: Boolean = true private lateinit var updateTask: TimeWorkerTask
private lateinit var hudElements: MutableMap<ResourceLocation, HUDElement> private lateinit var hudElements: MutableMap<ResourceLocation, HUDElement>
private val enabledHUDElements: MutableMap<ResourceLocation, HUDElement> = mutableMapOf() private val enabledHUDElements = mutableMapOf<ResourceLocation, HUDElement>()
private val hudShader = Shader( private val hudShader = Shader(
renderWindow = renderWindow, renderWindow = renderWindow,
resourceLocation = ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "hud"), resourceLocation = ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "hud"),
@ -42,13 +45,17 @@ class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow
var orthographicMatrix: Mat4 = Mat4() var orthographicMatrix: Mat4 = Mat4()
private set private set
val hudTextTranslator = HUDTextTranslator(connection)
var hudEnabled = true var hudEnabled = true
var currentMenu: HUDMenus = HUDMenus.IN_GAME var currentMenu: HUDMenus = HUDMenus.IN_GAME
set(value) { set(value) {
if (field == value) { if (field == value) {
return return
} }
prepareNext = true for (element in hudElements.values) {
element.prepareNext = true
}
field = value field = value
} }
@ -63,7 +70,15 @@ class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow
connection.registerEvent(CallbackEventInvoker.of<ScreenResizeEvent> { connection.registerEvent(CallbackEventInvoker.of<ScreenResizeEvent> {
orthographicMatrix = glm.ortho(0f, renderWindow.screenDimensions.x.toFloat(), renderWindow.screenDimensions.y.toFloat(), 0f) orthographicMatrix = glm.ortho(0f, renderWindow.screenDimensions.x.toFloat(), renderWindow.screenDimensions.y.toFloat(), 0f)
prepareNext = true for (element in hudElements.values) {
element.prepareNext = true
}
})
updateTask = TimeWorker.addTask(TimeWorkerTask(ProtocolDefinition.TICK_TIME*2) {
for (hudElement in hudElements.values) {
hudElement.update()
}
}) })
} }
@ -89,17 +104,12 @@ class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow
override fun draw() { override fun draw() {
if (RenderConstants.RENDER_HUD && hudEnabled) { if (RenderConstants.RENDER_HUD && hudEnabled) {
if (prepareNext) {
for (hudElement in hudElements.values) {
if (hudElement.isEnabled) {
hudElement.prepare(orthographicMatrix)
}
}
prepareNext = false
}
hudShader.use() hudShader.use()
for (hudElement in enabledHUDElements.values) { for (hudElement in enabledHUDElements.values) {
if (hudElement.isEnabled) { if (hudElement.isEnabled) {
if (hudElement.prepareNext) {
hudElement.prepare()
}
hudElement.draw() hudElement.draw()
} }
} }

View File

@ -18,7 +18,6 @@ import de.bixilon.minosoft.gui.rendering.hud.HUDRenderer
import de.bixilon.minosoft.gui.rendering.hud.elements.position.HUDElementPositionAnchors import de.bixilon.minosoft.gui.rendering.hud.elements.position.HUDElementPositionAnchors
import de.bixilon.minosoft.gui.rendering.hud.elements.position.HUDElementVec2 import de.bixilon.minosoft.gui.rendering.hud.elements.position.HUDElementVec2
import de.bixilon.minosoft.gui.rendering.util.VecUtil.toVec2 import de.bixilon.minosoft.gui.rendering.util.VecUtil.toVec2
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2i import glm_.vec2.Vec2i
abstract class HUDElement( abstract class HUDElement(
@ -30,8 +29,11 @@ abstract class HUDElement(
val isEnabled: Boolean get() = activeOnMenu?.let { val isEnabled: Boolean get() = activeOnMenu?.let {
hudRenderer.currentMenu == it hudRenderer.currentMenu == it
} ?: true } ?: true
lateinit var hudRenderer: HUDRenderer lateinit var hudRenderer: HUDRenderer
var prepareNext = true
open fun init(hudRenderer: HUDRenderer) { open fun init(hudRenderer: HUDRenderer) {
this.hudRenderer = hudRenderer this.hudRenderer = hudRenderer
position.init() position.init()
@ -41,7 +43,9 @@ abstract class HUDElement(
open fun draw() {} open fun draw() {}
open fun prepare(matrix: Mat4) {} open fun prepare() {
prepareNext = false
}
fun getPositionAtAnchor(anchor: HUDElementPositionAnchors): Vec2i { fun getPositionAtAnchor(anchor: HUDElementPositionAnchors): Vec2i {
val realSize = size.getRealVector(hudRenderer).toVec2 val realSize = size.getRealVector(hudRenderer).toVec2
@ -49,6 +53,8 @@ abstract class HUDElement(
return realPosition - realSize * position.anchor.positionTransform + realSize * anchor.positionTransform return realPosition - realSize * position.anchor.positionTransform + realSize * anchor.positionTransform
} }
open fun checkToPrepare() {}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is HUDElement) return false if (other !is HUDElement) return false
@ -68,4 +74,6 @@ abstract class HUDElement(
result = 31 * result + hudRenderer.hashCode() result = 31 * result + hudRenderer.hashCode()
return result return result
} }
open fun update() {}
} }

View File

@ -11,7 +11,6 @@ import de.bixilon.minosoft.gui.rendering.hud.elements.position.HUDElementVec2
import de.bixilon.minosoft.gui.rendering.util.mesh.Mesh import de.bixilon.minosoft.gui.rendering.util.mesh.Mesh
import de.bixilon.minosoft.gui.rendering.util.mesh.SimpleTextureMesh import de.bixilon.minosoft.gui.rendering.util.mesh.SimpleTextureMesh
import de.bixilon.minosoft.util.json.RGBColorSerializer import de.bixilon.minosoft.util.json.RGBColorSerializer
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2 import glm_.vec2.Vec2
import glm_.vec3.Vec3 import glm_.vec3.Vec3
import glm_.vec4.Vec4 import glm_.vec4.Vec4
@ -52,8 +51,8 @@ class HUDImageElement: HUDElement {
mesh.draw() mesh.draw()
} }
override fun prepare(matrix: Mat4) { override fun prepare() {
super.prepare(matrix) super.prepare()
if (mesh.state == Mesh.MeshStates.LOADED) { if (mesh.state == Mesh.MeshStates.LOADED) {
mesh.unload() mesh.unload()
mesh = SimpleTextureMesh() mesh = SimpleTextureMesh()
@ -69,7 +68,7 @@ class HUDImageElement: HUDElement {
), texture.texture, textureUV, tint) ), texture.texture, textureUV, tint)
} }
for (position in DRAW_ORDER) { for (position in DRAW_ORDER) {
addVertex((matrix * Vec4(positions[position], 1f, 1f)).xy, uvs[position]) addVertex((hudRenderer.orthographicMatrix * Vec4(positions[position], 1f, 1f)).xy, uvs[position])
} }
mesh.load() mesh.load()
} }

View File

@ -31,7 +31,6 @@ import de.bixilon.minosoft.gui.rendering.hud.elements.primitive.HUDSpacerElement
import de.bixilon.minosoft.gui.rendering.util.VecUtil.toVec2 import de.bixilon.minosoft.gui.rendering.util.VecUtil.toVec2
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import glm_.glm import glm_.glm
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2 import glm_.vec2.Vec2
class HUDTextElement : HUDElement { class HUDTextElement : HUDElement {
@ -39,11 +38,15 @@ class HUDTextElement : HUDElement {
private val alignment: HUDElementPositionAnchors private val alignment: HUDElementPositionAnchors
private var contents: String = "" private lateinit var contents: List<*>
private var currentText: ChatComponent? = null
var lastElement: HUDElement? = null
constructor(position: HUDElementPosition, size: HUDElementVec2, json: Map<String, Any>? = null, activeOnMenu: HUDMenus?) : super(position, size, (json?.get("z") as Double?)?.toInt() ?: 0, activeOnMenu) { constructor(position: HUDElementPosition, size: HUDElementVec2, json: Map<String, Any>? = null, activeOnMenu: HUDMenus?) : super(position, size, (json?.get("z") as Double?)?.toInt() ?: 0, activeOnMenu) {
alignment = json?.get("alignment")?.let { HUDElementPositionAnchors.of(it as String) } ?: HUDElementPositionAnchors.TOP_LEFT alignment = json?.get("alignment")?.let { HUDElementPositionAnchors.of(it as String) } ?: HUDElementPositionAnchors.TOP_LEFT
(json?.get("static_text") as String?)?.let { contents = it } contents = json?.get("content") as List<*>
} }
constructor(position: HUDElementPosition, size: HUDElementVec2, z: Int) : super(position, size, z) { constructor(position: HUDElementPosition, size: HUDElementVec2, z: Int) : super(position, size, z) {
@ -52,6 +55,7 @@ class HUDTextElement : HUDElement {
private fun renderChatComponent(chatComponent: ChatComponent) { private fun renderChatComponent(chatComponent: ChatComponent) {
elements.clear() elements.clear()
lastElement = null
val newLinePositions = mutableListOf<Int>() val newLinePositions = mutableListOf<Int>()
addChatComponent(chatComponent, newLinePositions) addChatComponent(chatComponent, newLinePositions)
newLinePositions += elements.size newLinePositions += elements.size
@ -72,7 +76,6 @@ class HUDTextElement : HUDElement {
} }
private fun addChars(chars: CharArray, color: RGBColor?, newLinePositions: MutableList<Int>): MutableList<Int> { private fun addChars(chars: CharArray, color: RGBColor?, newLinePositions: MutableList<Int>): MutableList<Int> {
var lastElement: HUDElement? = null
val maxRight = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_RIGHT).x val maxRight = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_RIGHT).x
val bottomLeftPosition = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_LEFT).toVec2 val bottomLeftPosition = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_LEFT).toVec2
for (char in chars) { for (char in chars) {
@ -145,10 +148,28 @@ class HUDTextElement : HUDElement {
} }
} }
override fun prepare(matrix: Mat4) { override fun prepare() {
renderChatComponent(BaseComponent(null, contents)) if (currentText == null) {
return
}
renderChatComponent(currentText!!)
for (element in elements) { for (element in elements) {
element.prepare(matrix) element.prepare()
}
}
fun getTextComponent(): ChatComponent {
return ChatComponent.of(contents, translator = hudRenderer.connection.version.localeManager.language, hudTextTranslator = hudRenderer.hudTextTranslator)
}
override fun update() {
if (!isEnabled) {
return
}
val newText = getTextComponent()
if (currentText != newText) {
currentText = newText
prepareNext = true
} }
} }

View File

@ -0,0 +1,47 @@
/*
* Minosoft
*
* Copyright (C) 2021 Lukas Eisenhauer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.hud.elements.text
import de.bixilon.minosoft.data.locale.minecraft.Translator
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.data.text.TextComponent
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
import de.bixilon.minosoft.util.MMath.round
class HUDTextTranslator(val connection: PlayConnection) : Translator {
override fun translate(key: String?, parent: TextComponent?, vararg data: Any?): ChatComponent {
if (key == null) {
return ChatComponent.of(key)
}
val resourceLocation = ResourceLocation(key)
if (resourceLocation !in insertRunnables) {
return ChatComponent.of(key)
}
val runnable = insertRunnables[resourceLocation]!! // checked before
return ChatComponent.of(runnable.invoke(connection))
}
companion object {
private val insertRunnables = mutableMapOf<ResourceLocation, (PlayConnection) -> String>()
init {
insertRunnables[ResourceLocation("minosoft:playerPosition_x")] = { connection ->
connection.player.position.x.round(2).toString()
}
}
}
}

View File

@ -13,6 +13,8 @@
package de.bixilon.minosoft.util package de.bixilon.minosoft.util
import glm_.func.common.floor
import glm_.glm
import glm_.vec2.Vec2i import glm_.vec2.Vec2i
import kotlin.math.floor import kotlin.math.floor
@ -99,4 +101,14 @@ object MMath {
} else { } else {
-1 -1
} }
fun Float.round(digits: Int): Float {
val multiplicationFactor = glm.pow(10, digits)
return (this * multiplicationFactor).floor / multiplicationFactor
}
fun Double.round(digits: Int): Double {
val multiplicationFactor = glm.pow(10, digits)
return (this * multiplicationFactor).floor / multiplicationFactor
}
} }