mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-17 11:24:56 -04:00
split message signer
This commit is contained in:
parent
2a53b0627d
commit
b92e729f54
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Minosoft
|
* Minosoft
|
||||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
* Copyright (C) 2020-2023 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 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.
|
||||||
*
|
*
|
||||||
@ -25,6 +25,6 @@ data class CommandStackEntry(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun sign(connection: PlayConnection, chain: MessageChain, key: PrivateKey, salt: Long, time: Instant): ByteArray {
|
fun sign(connection: PlayConnection, chain: MessageChain, key: PrivateKey, salt: Long, time: Instant): ByteArray {
|
||||||
return chain.signMessage(connection.version, key, data.toString(), null, salt, connection.player.uuid, time, LastSeenMessageList(emptyArray()))
|
return chain.signMessage(key, data.toString(), null, salt, connection.player.uuid, time, LastSeenMessageList(emptyArray()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Minosoft
|
* Minosoft
|
||||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
* Copyright (C) 2020-2023 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 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.
|
||||||
*
|
*
|
||||||
@ -13,68 +13,17 @@
|
|||||||
|
|
||||||
package de.bixilon.minosoft.data.chat.signature
|
package de.bixilon.minosoft.data.chat.signature
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.io.JsonStringEncoder
|
import de.bixilon.minosoft.data.chat.signature.signer.MessageSigner
|
||||||
import com.google.common.hash.Hashing
|
|
||||||
import com.google.common.primitives.Longs
|
|
||||||
import de.bixilon.minosoft.data.text.ChatComponent
|
import de.bixilon.minosoft.data.text.ChatComponent
|
||||||
import de.bixilon.minosoft.protocol.ProtocolUtil.encodeNetwork
|
|
||||||
import de.bixilon.minosoft.protocol.protocol.OutByteBuffer
|
|
||||||
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
|
|
||||||
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
|
||||||
import de.bixilon.minosoft.protocol.versions.Version
|
import de.bixilon.minosoft.protocol.versions.Version
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MessageChain {
|
class MessageChain(version: Version) {
|
||||||
private var previous: ByteArray? = null
|
val signer = MessageSigner.forVersion(version)
|
||||||
|
|
||||||
fun signMessage(version: Version, privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray {
|
fun signMessage(privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray {
|
||||||
val signature = CryptManager.createSignature(version)
|
return signer.signMessage(privateKey, message, preview, salt, sender, time, lastSeen)
|
||||||
|
|
||||||
signature.initSign(privateKey)
|
|
||||||
|
|
||||||
if (version < ProtocolVersions.V_1_19_1_PRE4) {
|
|
||||||
signature.update(Longs.toByteArray(salt))
|
|
||||||
signature.update(Longs.toByteArray(sender.mostSignificantBits))
|
|
||||||
signature.update(Longs.toByteArray(sender.leastSignificantBits))
|
|
||||||
signature.update(Longs.toByteArray(time.epochSecond))
|
|
||||||
signature.update(message.getSignatureBytes())
|
|
||||||
} else {
|
|
||||||
val buffer = OutByteBuffer()
|
|
||||||
buffer.writeLong(salt)
|
|
||||||
buffer.writeLong(time.epochSecond)
|
|
||||||
if (version.versionId >= ProtocolVersions.V_1_19_2) { // ToDo: This changed somewhere after 1.19.1-pre5
|
|
||||||
buffer.writeBareString(message)
|
|
||||||
} else {
|
|
||||||
buffer.writeBareByteArray(message.getSignatureBytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version.versionId >= ProtocolVersions.V_1_19_1_PRE5) {
|
|
||||||
buffer.writeByte(0x46)
|
|
||||||
// ToDo: send preview text (optional)
|
|
||||||
|
|
||||||
for (entry in lastSeen.messages) {
|
|
||||||
buffer.writeByte(0x46)
|
|
||||||
buffer.writeUUID(entry.profile)
|
|
||||||
buffer.writeBareByteArray(entry.signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val hash = Hashing.sha256().hashBytes(buffer.toArray()).asBytes()
|
|
||||||
|
|
||||||
previous?.let { signature.update(it) }
|
|
||||||
signature.update(Longs.toByteArray(sender.mostSignificantBits))
|
|
||||||
signature.update(Longs.toByteArray(sender.leastSignificantBits))
|
|
||||||
signature.update(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
val singed = signature.sign()
|
|
||||||
this.previous = singed
|
|
||||||
|
|
||||||
return singed
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.getSignatureBytes(): ByteArray {
|
|
||||||
return """{"text":"${String(JsonStringEncoder.getInstance().quoteAsString(this))}"}""".encodeNetwork()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Minosoft
|
||||||
|
* Copyright (C) 2020-2023 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.chat.signature.signer
|
||||||
|
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.LastSeenMessageList
|
||||||
|
import de.bixilon.minosoft.data.text.ChatComponent
|
||||||
|
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
|
||||||
|
import de.bixilon.minosoft.protocol.versions.Version
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface MessageSigner {
|
||||||
|
|
||||||
|
fun signMessage(privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun forVersion(version: Version): MessageSigner {
|
||||||
|
if (version < ProtocolVersions.V_1_19_1_PRE4) {
|
||||||
|
return MessageSigner1(version)
|
||||||
|
}
|
||||||
|
if (version < ProtocolVersions.V_22W42A) {
|
||||||
|
return MessageSigner2(version)
|
||||||
|
}
|
||||||
|
return MessageSigner3(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Minosoft
|
||||||
|
* Copyright (C) 2020-2023 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.chat.signature.signer
|
||||||
|
|
||||||
|
import com.google.common.primitives.Longs
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.LastSeenMessageList
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.signer.MessageSigningUtil.getSignatureBytes
|
||||||
|
import de.bixilon.minosoft.data.text.ChatComponent
|
||||||
|
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||||
|
import de.bixilon.minosoft.protocol.versions.Version
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MessageSigner1(
|
||||||
|
private val version: Version,
|
||||||
|
) : MessageSigner {
|
||||||
|
|
||||||
|
override fun signMessage(privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray {
|
||||||
|
val signature = CryptManager.createSignature(version)
|
||||||
|
|
||||||
|
signature.initSign(privateKey)
|
||||||
|
|
||||||
|
signature.update(Longs.toByteArray(salt))
|
||||||
|
signature.update(Longs.toByteArray(sender.mostSignificantBits))
|
||||||
|
signature.update(Longs.toByteArray(sender.leastSignificantBits))
|
||||||
|
signature.update(Longs.toByteArray(time.epochSecond))
|
||||||
|
signature.update(message.getSignatureBytes())
|
||||||
|
|
||||||
|
return signature.sign()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Minosoft
|
||||||
|
* Copyright (C) 2020-2023 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.chat.signature.signer
|
||||||
|
|
||||||
|
import com.google.common.hash.Hashing
|
||||||
|
import com.google.common.primitives.Longs
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.LastSeenMessageList
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.signer.MessageSigningUtil.getSignatureBytes
|
||||||
|
import de.bixilon.minosoft.data.text.ChatComponent
|
||||||
|
import de.bixilon.minosoft.protocol.protocol.OutByteBuffer
|
||||||
|
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
|
||||||
|
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
|
||||||
|
import de.bixilon.minosoft.protocol.versions.Version
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MessageSigner2(
|
||||||
|
private val version: Version,
|
||||||
|
) : MessageSigner {
|
||||||
|
private var previous: ByteArray? = null
|
||||||
|
|
||||||
|
override fun signMessage(privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray {
|
||||||
|
val signature = CryptManager.createSignature(version)
|
||||||
|
|
||||||
|
signature.initSign(privateKey)
|
||||||
|
|
||||||
|
|
||||||
|
val buffer = OutByteBuffer()
|
||||||
|
buffer.writeLong(salt)
|
||||||
|
buffer.writeLong(time.epochSecond)
|
||||||
|
if (version.versionId >= ProtocolVersions.V_1_19_2) { // ToDo: This changed somewhere after 1.19.1-pre5
|
||||||
|
buffer.writeBareString(message)
|
||||||
|
} else {
|
||||||
|
buffer.writeBareByteArray(message.getSignatureBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.versionId >= ProtocolVersions.V_1_19_1_PRE5) {
|
||||||
|
buffer.writeByte(0x46)
|
||||||
|
// ToDo: send preview text (optional)
|
||||||
|
|
||||||
|
for (entry in lastSeen.messages) {
|
||||||
|
buffer.writeByte(0x46)
|
||||||
|
buffer.writeUUID(entry.profile)
|
||||||
|
buffer.writeBareByteArray(entry.signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hash = Hashing.sha256().hashBytes(buffer.toArray()).asBytes()
|
||||||
|
|
||||||
|
previous?.let { signature.update(it) }
|
||||||
|
signature.update(Longs.toByteArray(sender.mostSignificantBits))
|
||||||
|
signature.update(Longs.toByteArray(sender.leastSignificantBits))
|
||||||
|
signature.update(hash)
|
||||||
|
|
||||||
|
return signature.sign()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Minosoft
|
||||||
|
* Copyright (C) 2020-2023 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.chat.signature.signer
|
||||||
|
|
||||||
|
import de.bixilon.minosoft.data.chat.signature.LastSeenMessageList
|
||||||
|
import de.bixilon.minosoft.data.text.ChatComponent
|
||||||
|
import de.bixilon.minosoft.protocol.versions.Version
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MessageSigner3(
|
||||||
|
private val version: Version,
|
||||||
|
) : MessageSigner {
|
||||||
|
|
||||||
|
override fun signMessage(privateKey: PrivateKey, message: String, preview: ChatComponent?, salt: Long, sender: UUID, time: Instant, lastSeen: LastSeenMessageList): ByteArray {
|
||||||
|
TODO("Not yet implemented!")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Minosoft
|
||||||
|
* Copyright (C) 2020-2023 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.chat.signature.signer
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.io.JsonStringEncoder
|
||||||
|
import de.bixilon.minosoft.protocol.ProtocolUtil.encodeNetwork
|
||||||
|
|
||||||
|
object MessageSigningUtil {
|
||||||
|
|
||||||
|
fun String.getSignatureBytes(): ByteArray {
|
||||||
|
return """{"text":"${String(JsonStringEncoder.getInstance().quoteAsString(this))}"}""".encodeNetwork()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Minosoft
|
* Minosoft
|
||||||
* Copyright (C) 2020-2022 Moritz Zwerger
|
* Copyright (C) 2020-2023 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 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.
|
||||||
*
|
*
|
||||||
@ -47,19 +47,19 @@ import java.time.Instant
|
|||||||
class ConnectionUtil(
|
class ConnectionUtil(
|
||||||
private val connection: PlayConnection,
|
private val connection: PlayConnection,
|
||||||
) {
|
) {
|
||||||
private val chain = MessageChain()
|
private val chain = MessageChain(connection.version)
|
||||||
private val random = SecureRandom()
|
private val random = SecureRandom()
|
||||||
|
|
||||||
fun sendDebugMessage(message: Any) {
|
fun sendDebugMessage(message: Any) {
|
||||||
val component = BaseComponent(RenderConstants.DEBUG_MESSAGES_PREFIX, ChatComponent.of(message).apply { this.setFallbackColor(ChatColors.BLUE) })
|
val component = BaseComponent(RenderConstants.DEBUG_MESSAGES_PREFIX, ChatComponent.of(message).apply { this.setFallbackColor(ChatColors.BLUE) })
|
||||||
connection.fire(InternalMessageReceiveEvent(connection, InternalChatMessage(component)))
|
connection.events.fire(InternalMessageReceiveEvent(connection, InternalChatMessage(component)))
|
||||||
Log.log(LogMessageType.CHAT_IN, LogLevels.INFO) { component }
|
Log.log(LogMessageType.CHAT_IN, LogLevels.INFO) { component }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendInternal(message: Any) {
|
fun sendInternal(message: Any) {
|
||||||
val component = ChatComponent.of(message)
|
val component = ChatComponent.of(message)
|
||||||
val prefixed = BaseComponent(RenderConstants.INTERNAL_MESSAGES_PREFIX, component)
|
val prefixed = BaseComponent(RenderConstants.INTERNAL_MESSAGES_PREFIX, component)
|
||||||
connection.fire(InternalMessageReceiveEvent(connection, InternalChatMessage(if (connection.profiles.gui.chat.internal.hidden) prefixed else component)))
|
connection.events.fire(InternalMessageReceiveEvent(connection, InternalChatMessage(if (connection.profiles.gui.chat.internal.hidden) prefixed else component)))
|
||||||
Log.log(LogMessageType.CHAT_IN, LogLevels.INFO) { prefixed }
|
Log.log(LogMessageType.CHAT_IN, LogLevels.INFO) { prefixed }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class ConnectionUtil(
|
|||||||
if (message.length > connection.version.maxChatMessageSize) {
|
if (message.length > connection.version.maxChatMessageSize) {
|
||||||
throw IllegalArgumentException("Message length (${message.length} can not exceed ${connection.version.maxChatMessageSize})")
|
throw IllegalArgumentException("Message length (${message.length} can not exceed ${connection.version.maxChatMessageSize})")
|
||||||
}
|
}
|
||||||
if (connection.fire(ChatMessageSendEvent(connection, message))) {
|
if (connection.events.fire(ChatMessageSendEvent(connection, message))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.log(LogMessageType.CHAT_OUT) { message }
|
Log.log(LogMessageType.CHAT_OUT) { message }
|
||||||
@ -97,7 +97,7 @@ class ConnectionUtil(
|
|||||||
val acknowledgement = Acknowledgement.EMPTY
|
val acknowledgement = Acknowledgement.EMPTY
|
||||||
|
|
||||||
val signature: ByteArray? = if (connection.network.encrypted) {
|
val signature: ByteArray? = if (connection.network.encrypted) {
|
||||||
chain.signMessage(connection.version, privateKey, message, null, salt, uuid, time, acknowledgement.lastSeen)
|
chain.signMessage(privateKey, message, null, salt, uuid, time, acknowledgement.lastSeen)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ class ConnectionUtil(
|
|||||||
connection.world.particleRenderer?.removeAllParticles()
|
connection.world.particleRenderer?.removeAllParticles()
|
||||||
connection.player.openedContainer?.let {
|
connection.player.openedContainer?.let {
|
||||||
connection.player.openedContainer = null
|
connection.player.openedContainer = null
|
||||||
connection.fire(ContainerCloseEvent(connection, it.id ?: -1, it))
|
connection.events.fire(ContainerCloseEvent(connection, it.id ?: -1, it))
|
||||||
}
|
}
|
||||||
connection.player.healthCondition = HealthCondition()
|
connection.player.healthCondition = HealthCondition()
|
||||||
connection.world.time = WorldTime()
|
connection.world.time = WorldTime()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user