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.events.ClickEvent
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.util.KUtil.nullCast
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
get() {
val stringBuilder = StringBuilder()

View File

@ -17,6 +17,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
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.ObservableList
import javafx.scene.Node
@ -57,7 +58,7 @@ interface ChatComponent {
companion object {
@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) {
return BaseComponent()
}
@ -75,6 +76,16 @@ interface ChatComponent {
}
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
else -> raw.toString()
}

View File

@ -35,6 +35,16 @@ class ClickEvent {
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 {
OPEN_URL,
RUN_COMMAND,

View File

@ -45,6 +45,22 @@ class HoverEvent {
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 {
SHOW_TEXT,
SHOW_ITEM,

View File

@ -46,5 +46,10 @@ class EntityHoverData(
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.hud.atlas.HUDAtlasElement
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.shader.Shader
import de.bixilon.minosoft.modding.event.CallbackEventInvoker
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
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_.mat4x4.Mat4
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 val enabledHUDElements: MutableMap<ResourceLocation, HUDElement> = mutableMapOf()
private val enabledHUDElements = mutableMapOf<ResourceLocation, HUDElement>()
private val hudShader = Shader(
renderWindow = renderWindow,
resourceLocation = ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "hud"),
@ -42,13 +45,17 @@ class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow
var orthographicMatrix: Mat4 = Mat4()
private set
val hudTextTranslator = HUDTextTranslator(connection)
var hudEnabled = true
var currentMenu: HUDMenus = HUDMenus.IN_GAME
set(value) {
if (field == value) {
return
}
prepareNext = true
for (element in hudElements.values) {
element.prepareNext = true
}
field = value
}
@ -63,7 +70,15 @@ class HUDRenderer(val connection: PlayConnection, val renderWindow: RenderWindow
connection.registerEvent(CallbackEventInvoker.of<ScreenResizeEvent> {
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() {
if (RenderConstants.RENDER_HUD && hudEnabled) {
if (prepareNext) {
for (hudElement in hudElements.values) {
if (hudElement.isEnabled) {
hudElement.prepare(orthographicMatrix)
}
}
prepareNext = false
}
hudShader.use()
for (hudElement in enabledHUDElements.values) {
if (hudElement.isEnabled) {
if (hudElement.prepareNext) {
hudElement.prepare()
}
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.HUDElementVec2
import de.bixilon.minosoft.gui.rendering.util.VecUtil.toVec2
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2i
abstract class HUDElement(
@ -30,8 +29,11 @@ abstract class HUDElement(
val isEnabled: Boolean get() = activeOnMenu?.let {
hudRenderer.currentMenu == it
} ?: true
lateinit var hudRenderer: HUDRenderer
var prepareNext = true
open fun init(hudRenderer: HUDRenderer) {
this.hudRenderer = hudRenderer
position.init()
@ -41,7 +43,9 @@ abstract class HUDElement(
open fun draw() {}
open fun prepare(matrix: Mat4) {}
open fun prepare() {
prepareNext = false
}
fun getPositionAtAnchor(anchor: HUDElementPositionAnchors): Vec2i {
val realSize = size.getRealVector(hudRenderer).toVec2
@ -49,6 +53,8 @@ abstract class HUDElement(
return realPosition - realSize * position.anchor.positionTransform + realSize * anchor.positionTransform
}
open fun checkToPrepare() {}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is HUDElement) return false
@ -68,4 +74,6 @@ abstract class HUDElement(
result = 31 * result + hudRenderer.hashCode()
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.SimpleTextureMesh
import de.bixilon.minosoft.util.json.RGBColorSerializer
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2
import glm_.vec3.Vec3
import glm_.vec4.Vec4
@ -52,8 +51,8 @@ class HUDImageElement: HUDElement {
mesh.draw()
}
override fun prepare(matrix: Mat4) {
super.prepare(matrix)
override fun prepare() {
super.prepare()
if (mesh.state == Mesh.MeshStates.LOADED) {
mesh.unload()
mesh = SimpleTextureMesh()
@ -69,7 +68,7 @@ class HUDImageElement: HUDElement {
), texture.texture, textureUV, tint)
}
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()
}

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.protocol.protocol.ProtocolDefinition
import glm_.glm
import glm_.mat4x4.Mat4
import glm_.vec2.Vec2
class HUDTextElement : HUDElement {
@ -39,11 +38,15 @@ class HUDTextElement : HUDElement {
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) {
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) {
@ -52,6 +55,7 @@ class HUDTextElement : HUDElement {
private fun renderChatComponent(chatComponent: ChatComponent) {
elements.clear()
lastElement = null
val newLinePositions = mutableListOf<Int>()
addChatComponent(chatComponent, newLinePositions)
newLinePositions += elements.size
@ -72,7 +76,6 @@ class HUDTextElement : HUDElement {
}
private fun addChars(chars: CharArray, color: RGBColor?, newLinePositions: MutableList<Int>): MutableList<Int> {
var lastElement: HUDElement? = null
val maxRight = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_RIGHT).x
val bottomLeftPosition = getPositionAtAnchor(HUDElementPositionAnchors.BOTTOM_LEFT).toVec2
for (char in chars) {
@ -145,10 +148,28 @@ class HUDTextElement : HUDElement {
}
}
override fun prepare(matrix: Mat4) {
renderChatComponent(BaseComponent(null, contents))
override fun prepare() {
if (currentText == null) {
return
}
renderChatComponent(currentText!!)
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
import glm_.func.common.floor
import glm_.glm
import glm_.vec2.Vec2i
import kotlin.math.floor
@ -99,4 +101,14 @@ object MMath {
} else {
-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
}
}