mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-13 17:37:58 -04:00
yggdrasil implementation, player data: read player properties correct
This commit is contained in:
parent
b55d24ca89
commit
d0307e9fb2
5
pom.xml
5
pom.xml
@ -444,5 +444,10 @@
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>0.10.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.luben</groupId>
|
||||
<artifactId>zstd-jni</artifactId>
|
||||
<version>1.5.0-4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -122,6 +122,8 @@ object Minosoft {
|
||||
|
||||
Util.forceClassInit(Eros::class.java)
|
||||
}
|
||||
taskWorker += Task(identifier = StartupTasks.LOAD_YGGDRASIL, executor = { YggdrasilUtil.load() })
|
||||
|
||||
|
||||
|
||||
taskWorker.work(START_UP_LATCH)
|
||||
|
@ -19,7 +19,7 @@ import de.bixilon.minosoft.data.entities.Poses
|
||||
import de.bixilon.minosoft.data.entities.entities.EntityMetaDataFunction
|
||||
import de.bixilon.minosoft.data.entities.entities.LivingEntity
|
||||
import de.bixilon.minosoft.data.player.Arms
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.data.player.tab.TabListItem
|
||||
import de.bixilon.minosoft.data.registries.entities.EntityType
|
||||
import de.bixilon.minosoft.data.world.World
|
||||
@ -35,7 +35,7 @@ abstract class PlayerEntity(
|
||||
position: Vec3d = Vec3d.EMPTY,
|
||||
rotation: EntityRotation = EntityRotation(0.0, 0.0),
|
||||
name: String = "TBA",
|
||||
properties: Map<String, PlayerProperty> = mapOf(),
|
||||
properties: PlayerProperties = PlayerProperties(),
|
||||
var tabListItem: TabListItem = TabListItem(name = name, gamemode = Gamemodes.SURVIVAL, properties = properties),
|
||||
) : LivingEntity(connection, entityType, position, rotation) {
|
||||
override val dimensions: Vec2
|
||||
|
@ -15,7 +15,7 @@ package de.bixilon.minosoft.data.entities.entities.player
|
||||
|
||||
import de.bixilon.minosoft.data.abilities.Gamemodes
|
||||
import de.bixilon.minosoft.data.entities.EntityRotation
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.data.player.tab.TabListItem
|
||||
import de.bixilon.minosoft.data.registries.ResourceLocation
|
||||
import de.bixilon.minosoft.data.registries.entities.EntityFactory
|
||||
@ -30,7 +30,7 @@ class RemotePlayerEntity(
|
||||
position: Vec3d = Vec3d.EMPTY,
|
||||
rotation: EntityRotation = EntityRotation(0.0, 0.0),
|
||||
name: String = "TBA",
|
||||
properties: Map<String, PlayerProperty> = mapOf(),
|
||||
properties: PlayerProperties = PlayerProperties(),
|
||||
tabListItem: TabListItem = TabListItem(name = name, gamemode = Gamemodes.SURVIVAL, properties = properties),
|
||||
) : PlayerEntity(connection, entityType, position, rotation, name, properties, tabListItem) {
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Minosoft
|
||||
* Copyright (C) 2020 Moritz Zwerger
|
||||
*
|
||||
* 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.data.player
|
||||
|
||||
class PlayerProperty(
|
||||
val key: String,
|
||||
val value: String,
|
||||
val signature: String? = null,
|
||||
) {
|
||||
val isSigned: Boolean
|
||||
get() = signature != null // ToDo check signature
|
||||
|
||||
override fun toString(): String {
|
||||
return "$key: $value"
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package de.bixilon.minosoft.data.player.properties
|
||||
|
||||
import de.bixilon.minosoft.data.player.properties.textures.PlayerTextures
|
||||
|
||||
class PlayerProperties(
|
||||
val textures: PlayerTextures? = null,
|
||||
)
|
@ -0,0 +1,29 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures
|
||||
|
||||
import de.bixilon.minosoft.util.KUtil.check
|
||||
import java.net.URL
|
||||
|
||||
open class PlayerTexture(
|
||||
val url: URL,
|
||||
) {
|
||||
init {
|
||||
url.check()
|
||||
|
||||
check(urlMatches(url, ALLOWED_DOMAINS) && !urlMatches(url, BLOCKED_DOMAINS)) { "URL hostname is not allowed!" }
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private val ALLOWED_DOMAINS = arrayOf(".minecraft.net", ".mojang.com")
|
||||
private val BLOCKED_DOMAINS = arrayOf("bugs.mojang.com", "education.minecraft.net", "feedback.minecraft.net")
|
||||
|
||||
private fun urlMatches(url: URL, domains: Array<String>): Boolean {
|
||||
for (checkURL in domains) {
|
||||
if (url.host.endsWith(checkURL)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.convertValue
|
||||
import de.bixilon.minosoft.util.KUtil.toLong
|
||||
import de.bixilon.minosoft.util.Util
|
||||
import de.bixilon.minosoft.util.YggdrasilUtil
|
||||
import de.bixilon.minosoft.util.json.Jackson
|
||||
import de.bixilon.minosoft.util.nbt.tag.NBTUtil.compoundCast
|
||||
import java.util.*
|
||||
|
||||
class PlayerTextures(
|
||||
val name: String?,
|
||||
val uuid: UUID?,
|
||||
val date: Date?,
|
||||
val skin: SkinPlayerTexture?,
|
||||
val cape: PlayerTexture?,
|
||||
val elytra: PlayerTexture?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun of(encoded: String, signature: String): PlayerTextures {
|
||||
check(YggdrasilUtil.verify(encoded, signature)) { "Texture signature is invalid!" }
|
||||
|
||||
val json: Map<String, Any> = Jackson.MAPPER.readValue(Base64.getDecoder().decode(encoded), Jackson.JSON_MAP_TYPE)
|
||||
|
||||
// Data also contains `signatureRequired`
|
||||
val textures = json["textures"]?.compoundCast()
|
||||
return PlayerTextures(
|
||||
name = json["profileName"]?.toString(),
|
||||
uuid = json["profileId"]?.toString()?.let { Util.getUUIDFromString(it) },
|
||||
date = json["timestamp"]?.toLong()?.let { Date(it) },
|
||||
skin = textures?.get("SKIN")?.compoundCast()?.let { return@let Jackson.MAPPER.convertValue(it) },
|
||||
cape = textures?.get("CAPE")?.compoundCast()?.let { return@let Jackson.MAPPER.convertValue(it) },
|
||||
elytra = textures?.get("ELYTRA")?.compoundCast()?.let { return@let Jackson.MAPPER.convertValue(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures
|
||||
|
||||
import de.bixilon.minosoft.data.player.properties.textures.metadata.SkinMetadata
|
||||
import java.net.URL
|
||||
|
||||
class SkinPlayerTexture(
|
||||
url: URL,
|
||||
val metadata: SkinMetadata = SkinMetadata(),
|
||||
) : PlayerTexture(url = url)
|
@ -0,0 +1,5 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures.metadata
|
||||
|
||||
data class SkinMetadata(
|
||||
val model: SkinModel = SkinModel.NORMAL,
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures.metadata
|
||||
|
||||
enum class SkinModel {
|
||||
SLIM,
|
||||
NORMAL,
|
||||
;
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
package de.bixilon.minosoft.data.player.tab
|
||||
|
||||
import de.bixilon.minosoft.data.abilities.Gamemodes
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.data.scoreboard.Team
|
||||
import de.bixilon.minosoft.data.text.ChatComponent
|
||||
import de.bixilon.minosoft.util.KUtil.nullCompare
|
||||
@ -24,7 +24,7 @@ data class TabListItem(
|
||||
var ping: Int = -1,
|
||||
var gamemode: Gamemodes = Gamemodes.SURVIVAL,
|
||||
var displayName: ChatComponent = ChatComponent.of(name),
|
||||
var properties: Map<String, PlayerProperty> = mutableMapOf(),
|
||||
var properties: PlayerProperties = PlayerProperties(),
|
||||
var team: Team? = null,
|
||||
) : Comparable<TabListItem> {
|
||||
val tabDisplayName: ChatComponent
|
||||
|
@ -14,7 +14,7 @@
|
||||
package de.bixilon.minosoft.data.player.tab
|
||||
|
||||
import de.bixilon.minosoft.data.abilities.Gamemodes
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.data.scoreboard.Team
|
||||
import de.bixilon.minosoft.data.text.ChatComponent
|
||||
|
||||
@ -24,7 +24,7 @@ data class TabListItemData(
|
||||
var gamemode: Gamemodes? = null,
|
||||
var hasDisplayName: Boolean? = null,
|
||||
var displayName: ChatComponent? = null,
|
||||
val properties: Map<String, PlayerProperty>? = null,
|
||||
val properties: PlayerProperties? = null,
|
||||
var remove: Boolean = false, // used for legacy tab list
|
||||
var team: Team? = null,
|
||||
var removeFromTeam: Boolean = false,
|
||||
|
@ -20,6 +20,8 @@ import de.bixilon.minosoft.gui.rendering.util.vec.vec2.Vec2iUtil.toVec2i
|
||||
import de.bixilon.minosoft.util.KUtil.mapCast
|
||||
import de.bixilon.minosoft.util.KUtil.toInt
|
||||
import de.bixilon.minosoft.util.KUtil.toResourceLocation
|
||||
import glm_.vec2.Vec2
|
||||
import glm_.vec2.Vec2i
|
||||
|
||||
class HUDAtlasManager(private val hudRenderer: HUDRenderer) {
|
||||
private lateinit var elements: Map<ResourceLocation, HUDAtlasElement>
|
||||
@ -79,8 +81,8 @@ class HUDAtlasManager(private val hudRenderer: HUDRenderer) {
|
||||
|
||||
fun postInit() {
|
||||
for (element in elements.values) {
|
||||
element.uvStart = element.texture.singlePixelSize * element.start
|
||||
element.uvEnd = element.texture.singlePixelSize * element.end
|
||||
element.uvStart = ATLAS_SINGLE_PIXEL_SIZE * element.start
|
||||
element.uvEnd = ATLAS_SINGLE_PIXEL_SIZE * element.end
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,5 +96,8 @@ class HUDAtlasManager(private val hudRenderer: HUDRenderer) {
|
||||
|
||||
companion object {
|
||||
private val ATLAS_DATA = "minosoft:mapping/atlas.json".toResourceLocation()
|
||||
|
||||
private val ATLAS_SIZE = Vec2i(256, 256)
|
||||
private val ATLAS_SINGLE_PIXEL_SIZE = Vec2(1.0f) / ATLAS_SIZE
|
||||
}
|
||||
}
|
||||
|
@ -198,8 +198,8 @@ class OpenGLTextureArray(
|
||||
|
||||
|
||||
companion object {
|
||||
val TEXTURE_RESOLUTION_ID_MAP = intArrayOf(16, 32, 64, 128, 256, 512, 1024) // A 12x12 texture will be saved in texture id 0 (in 0 are only 16x16 textures). Animated textures get split
|
||||
const val TEXTURE_MAX_RESOLUTION = 1024
|
||||
val TEXTURE_RESOLUTION_ID_MAP = intArrayOf(16, 32, 64, 128, 256, 512, TEXTURE_MAX_RESOLUTION) // A 12x12 texture will be saved in texture id 0 (in 0 are only 16x16 textures). Animated textures get split
|
||||
const val MAX_MIPMAP_LEVELS = 5
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import de.bixilon.minosoft.data.entities.EntityRotation
|
||||
import de.bixilon.minosoft.data.entities.entities.player.PlayerEntity
|
||||
import de.bixilon.minosoft.data.entities.entities.player.RemotePlayerEntity
|
||||
import de.bixilon.minosoft.data.entities.meta.EntityMetaData
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.modding.event.events.EntitySpawnEvent
|
||||
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
|
||||
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
|
||||
@ -38,15 +38,11 @@ class PlayerEntitySpawnS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
|
||||
entityId = buffer.readVarInt()
|
||||
var name = "TBA"
|
||||
|
||||
val properties: MutableMap<String, PlayerProperty> = mutableMapOf()
|
||||
var properties = PlayerProperties()
|
||||
if (buffer.versionId < ProtocolVersions.V_14W21A) {
|
||||
name = buffer.readString()
|
||||
entityUUID = buffer.readUUIDString()
|
||||
val length = buffer.readVarInt()
|
||||
for (i in 0 until length) {
|
||||
val property = PlayerProperty(buffer.readString(), buffer.readString(), buffer.readString())
|
||||
properties[property.key] = property
|
||||
}
|
||||
properties = buffer.readPlayerProperties()
|
||||
} else {
|
||||
entityUUID = buffer.readUUID()
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ package de.bixilon.minosoft.protocol.packets.s2c.play
|
||||
|
||||
import de.bixilon.minosoft.data.abilities.Gamemodes
|
||||
import de.bixilon.minosoft.data.entities.entities.player.PlayerEntity
|
||||
import de.bixilon.minosoft.data.player.PlayerProperty
|
||||
import de.bixilon.minosoft.data.player.tab.TabListItem
|
||||
import de.bixilon.minosoft.data.player.tab.TabListItemData
|
||||
import de.bixilon.minosoft.modding.event.events.TabListEntryChangeEvent
|
||||
@ -60,15 +59,7 @@ class TabListDataS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
|
||||
when (action) {
|
||||
TabListItemActions.ADD -> {
|
||||
val name = buffer.readString()
|
||||
val playerProperties: MutableMap<String, PlayerProperty> = mutableMapOf()
|
||||
for (index in 0 until buffer.readVarInt()) {
|
||||
val property = PlayerProperty(
|
||||
buffer.readString(),
|
||||
buffer.readString(),
|
||||
buffer.readOptional { buffer.readString() },
|
||||
)
|
||||
playerProperties[property.key] = property
|
||||
}
|
||||
val properties = buffer.readPlayerProperties()
|
||||
val gamemode = Gamemodes[buffer.readVarInt()]
|
||||
val ping = buffer.readVarInt()
|
||||
val hasDisplayName = buffer.readBoolean()
|
||||
@ -79,7 +70,7 @@ class TabListDataS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
|
||||
}
|
||||
data = TabListItemData(
|
||||
name = name,
|
||||
properties = playerProperties,
|
||||
properties = properties,
|
||||
gamemode = gamemode,
|
||||
ping = ping,
|
||||
hasDisplayName = hasDisplayName,
|
||||
|
@ -14,6 +14,8 @@ package de.bixilon.minosoft.protocol.protocol
|
||||
|
||||
import de.bixilon.minosoft.data.entities.meta.EntityMetaData
|
||||
import de.bixilon.minosoft.data.inventory.ItemStack
|
||||
import de.bixilon.minosoft.data.player.properties.PlayerProperties
|
||||
import de.bixilon.minosoft.data.player.properties.textures.PlayerTextures
|
||||
import de.bixilon.minosoft.data.registries.biomes.Biome
|
||||
import de.bixilon.minosoft.data.registries.particle.ParticleType
|
||||
import de.bixilon.minosoft.data.registries.particle.data.BlockParticleData
|
||||
@ -214,4 +216,27 @@ class PlayInByteBuffer : InByteBuffer {
|
||||
fun readEntityIdArray(length: Int = readVarInt()): Array<Int> {
|
||||
return readArray(length) { readEntityId() }
|
||||
}
|
||||
|
||||
|
||||
fun readPlayerProperties(): PlayerProperties {
|
||||
var textures: PlayerTextures? = null
|
||||
for (i in 0 until readVarInt()) {
|
||||
val name = readString()
|
||||
val value = readString()
|
||||
val signature = if (versionId < V_14W21A) {
|
||||
readString()
|
||||
} else {
|
||||
readOptional { readString() }
|
||||
}
|
||||
when (name) {
|
||||
"textures" -> {
|
||||
check(textures == null) { "Textures duplicated" }
|
||||
textures = PlayerTextures.of(value, signature ?: throw IllegalArgumentException("Texture data needs to be signed!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return PlayerProperties(
|
||||
textures = textures,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import glm_.vec4.Vec4t
|
||||
import sun.misc.Unsafe
|
||||
import java.io.*
|
||||
import java.lang.reflect.Field
|
||||
import java.net.URL
|
||||
import java.nio.ByteBuffer
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -558,4 +559,9 @@ object KUtil {
|
||||
|
||||
val Locale.fullName: String
|
||||
get() = language + "_" + country.ifEmpty { language.uppercase() }
|
||||
|
||||
|
||||
fun URL.check() {
|
||||
check(this.protocol == "http" || this.protocol == "https") { "Url is not a web address" }
|
||||
}
|
||||
}
|
||||
|
33
src/main/java/de/bixilon/minosoft/util/YggdrasilUtil.kt
Normal file
33
src/main/java/de/bixilon/minosoft/util/YggdrasilUtil.kt
Normal file
@ -0,0 +1,33 @@
|
||||
package de.bixilon.minosoft.util
|
||||
|
||||
import de.bixilon.minosoft.Minosoft
|
||||
import de.bixilon.minosoft.util.KUtil.toResourceLocation
|
||||
import java.security.KeyFactory
|
||||
import java.security.PublicKey
|
||||
import java.security.Signature
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import java.util.*
|
||||
|
||||
|
||||
object YggdrasilUtil {
|
||||
lateinit var PUBLIC_KEY: PublicKey
|
||||
private set
|
||||
|
||||
fun load() {
|
||||
check(!this::PUBLIC_KEY.isInitialized) { "Already loaded!" }
|
||||
val spec = X509EncodedKeySpec(Minosoft.MINOSOFT_ASSETS_MANAGER["minosoft:mojang/yggdrasil_session_pubkey.der".toResourceLocation()].readAllBytes())
|
||||
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
|
||||
PUBLIC_KEY = keyFactory.generatePublic(spec)
|
||||
}
|
||||
|
||||
fun verify(data: ByteArray, signature: ByteArray): Boolean {
|
||||
val signatureInstance = Signature.getInstance("SHA1withRSA")
|
||||
signatureInstance.initVerify(PUBLIC_KEY)
|
||||
signatureInstance.update(data)
|
||||
return signatureInstance.verify(signature)
|
||||
}
|
||||
|
||||
fun verify(data: String, signature: String): Boolean {
|
||||
return verify(data.toByteArray(), Base64.getDecoder().decode(signature))
|
||||
}
|
||||
}
|
@ -2,14 +2,19 @@ package de.bixilon.minosoft.util.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.MapperFeature
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.databind.type.MapType
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinFeature
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
|
||||
object Jackson {
|
||||
val MAPPER = ObjectMapper()
|
||||
val MAPPER = JsonMapper.builder()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE)
|
||||
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
|
||||
.build()
|
||||
.registerModule(KotlinModule.Builder()
|
||||
.withReflectionCacheSize(512)
|
||||
.configure(KotlinFeature.NullToEmptyCollection, false)
|
||||
@ -21,8 +26,6 @@ object Jackson {
|
||||
.registerModule(ResourceLocationSerializer)
|
||||
.registerModule(RGBColorSerializer)
|
||||
.registerModule(ChatComponentColorSerializer)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE)
|
||||
.setDefaultMergeable(true)
|
||||
|
||||
|
||||
|
@ -13,6 +13,8 @@
|
||||
|
||||
package de.bixilon.minosoft.util.nbt.tag
|
||||
|
||||
import de.bixilon.minosoft.util.KUtil.nullCast
|
||||
|
||||
object NBTUtil {
|
||||
|
||||
fun compound(): MutableMap<String, Any> {
|
||||
@ -33,11 +35,7 @@ object NBTUtil {
|
||||
}
|
||||
|
||||
fun Any?.compoundCast(): MutableMap<String, Any>? {
|
||||
try {
|
||||
return this as MutableMap<String, Any>
|
||||
} catch (ignored: ClassCastException) {
|
||||
}
|
||||
return null
|
||||
return this.nullCast()
|
||||
}
|
||||
|
||||
fun Any?.asCompound(): MutableMap<String, Any> {
|
||||
|
@ -24,5 +24,6 @@ enum class StartupTasks {
|
||||
INITIALIZE_JAVAFX,
|
||||
X_START_ON_FIRST_THREAD_WARNING,
|
||||
FILE_WATCHER,
|
||||
LOAD_YGGDRASIL,
|
||||
;
|
||||
}
|
||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user